@gajae-code/coding-agent 0.11.6 → 0.11.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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +70 -7
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +68 -20
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -89
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -12,10 +12,18 @@ import {
|
|
|
12
12
|
openRecoveryFsRoot,
|
|
13
13
|
type RecoveryFsRoot,
|
|
14
14
|
renameNoReplacePath,
|
|
15
|
+
repairOwnerOnlyPathSecurityExpected,
|
|
15
16
|
snapshotDirectoryTree,
|
|
16
17
|
verifyOwnerOnlyFdSecurity,
|
|
17
18
|
verifyOwnerOnlyPathSecurity,
|
|
19
|
+
verifyOwnerOnlyPathSecurityExpected,
|
|
18
20
|
} from "@gajae-code/natives";
|
|
21
|
+
import {
|
|
22
|
+
classifyNativePublishOutcome,
|
|
23
|
+
formatNativePublishDiagnostic,
|
|
24
|
+
mayCleanCurrentStaging,
|
|
25
|
+
type NativePublishOutcome,
|
|
26
|
+
} from "./native-publish-outcome";
|
|
19
27
|
|
|
20
28
|
export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
21
29
|
export const MANAGED_ARTIFACT_MAX_FILES = 10_000;
|
|
@@ -26,6 +34,84 @@ const LOCK_HEARTBEAT_MS = 10_000;
|
|
|
26
34
|
const LOCK_WAIT_MS = 5_000;
|
|
27
35
|
|
|
28
36
|
const LOCK_STALE_RECHECK_MS = 100;
|
|
37
|
+
export class ManagedPublishError extends Error {
|
|
38
|
+
readonly classification:
|
|
39
|
+
| "destination_conflict"
|
|
40
|
+
| "atomic_unavailable"
|
|
41
|
+
| "invalid_request"
|
|
42
|
+
| "durability_not_provable"
|
|
43
|
+
| "identity_mismatch"
|
|
44
|
+
| "io_error"
|
|
45
|
+
| "durability_failed";
|
|
46
|
+
readonly diagnostic: string;
|
|
47
|
+
|
|
48
|
+
constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
|
|
49
|
+
super(classification);
|
|
50
|
+
this.name = "ManagedPublishError";
|
|
51
|
+
this.classification = classification;
|
|
52
|
+
this.diagnostic = formatNativePublishDiagnostic(outcome);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
|
|
57
|
+
const classification =
|
|
58
|
+
outcome.reason === "destination_exists"
|
|
59
|
+
? "destination_conflict"
|
|
60
|
+
: outcome.reason === "atomic_unavailable"
|
|
61
|
+
? "atomic_unavailable"
|
|
62
|
+
: outcome.reason === "invalid_request"
|
|
63
|
+
? "invalid_request"
|
|
64
|
+
: outcome.reason === "durability_not_provable"
|
|
65
|
+
? "durability_not_provable"
|
|
66
|
+
: outcome.reason === "identity_violation"
|
|
67
|
+
? "identity_mismatch"
|
|
68
|
+
: outcome.reason === "io_failure"
|
|
69
|
+
? "io_error"
|
|
70
|
+
: "durability_failed";
|
|
71
|
+
return new ManagedPublishError(classification, outcome);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
|
|
75
|
+
function sameDirectoryTreeSnapshotAfterRename(
|
|
76
|
+
left: NativeDirectoryTreeSnapshot,
|
|
77
|
+
right: NativeDirectoryTreeSnapshot,
|
|
78
|
+
): boolean {
|
|
79
|
+
return (
|
|
80
|
+
left.rootDev === right.rootDev &&
|
|
81
|
+
left.rootIno === right.rootIno &&
|
|
82
|
+
left.entries.length === right.entries.length &&
|
|
83
|
+
left.entries.every((entry, index) => {
|
|
84
|
+
const other = right.entries[index];
|
|
85
|
+
return (
|
|
86
|
+
other !== undefined &&
|
|
87
|
+
entry.relativePath === other.relativePath &&
|
|
88
|
+
entry.kind === other.kind &&
|
|
89
|
+
entry.dev === other.dev &&
|
|
90
|
+
entry.ino === other.ino &&
|
|
91
|
+
entry.size === other.size &&
|
|
92
|
+
entry.mtimeNs === other.mtimeNs &&
|
|
93
|
+
(entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
|
|
94
|
+
entry.sha256 === other.sha256
|
|
95
|
+
);
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class ManagedTreeMoveOutcomeError extends Error {
|
|
101
|
+
constructor(
|
|
102
|
+
message: string,
|
|
103
|
+
readonly stagingCleanupSafe: boolean,
|
|
104
|
+
) {
|
|
105
|
+
super(message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Cleanup is safe only when the native move reports that it did not commit. */
|
|
110
|
+
export function mayCleanManagedTreeStaging(error: unknown): boolean {
|
|
111
|
+
return !(error instanceof ManagedTreeMoveOutcomeError) || error.stagingCleanupSafe;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type ManagedSessionSecurityPolicy = "default" | "windows-existing-verify-first";
|
|
29
115
|
|
|
30
116
|
export type ManagedStorageFailure =
|
|
31
117
|
| "migration_busy"
|
|
@@ -35,6 +121,8 @@ export type ManagedStorageFailure =
|
|
|
35
121
|
| "source_changed"
|
|
36
122
|
| "unsafe_artifacts"
|
|
37
123
|
| "durability_failed"
|
|
124
|
+
| "atomic_unavailable"
|
|
125
|
+
| "durability_not_provable"
|
|
38
126
|
| "migration_retired"
|
|
39
127
|
| "managed_storage_unsupported";
|
|
40
128
|
|
|
@@ -93,6 +181,9 @@ export function validateNativeSecurityResult(
|
|
|
93
181
|
) {
|
|
94
182
|
throw new Error("Malformed ACL security failure evidence");
|
|
95
183
|
}
|
|
184
|
+
if (operation === "verify" && result.operation !== "query") {
|
|
185
|
+
throw new Error("Verify failure unexpectedly reports ACL mutation");
|
|
186
|
+
}
|
|
96
187
|
} else if (result.operation !== undefined || result.attribute !== undefined) {
|
|
97
188
|
throw new Error("Unexpected ACL fields on owner-only security failure");
|
|
98
189
|
}
|
|
@@ -179,6 +270,40 @@ export interface ManagedDirectoryRoot {
|
|
|
179
270
|
readonly ino: bigint;
|
|
180
271
|
}
|
|
181
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Establish the first managed directory beneath a potentially shared ancestor.
|
|
275
|
+
* The ancestor is used only to create the managed chain; every component from
|
|
276
|
+
* `configuredRoot` downward is independently type, symlink, owner, mode, and
|
|
277
|
+
* ACL checked. This deliberately does not impose owner-only policy on `/tmp`.
|
|
278
|
+
*/
|
|
279
|
+
export function prepareManagedDirectoryRoot(
|
|
280
|
+
configuredRoot: string,
|
|
281
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
282
|
+
): ManagedDirectoryRoot {
|
|
283
|
+
const target = path.resolve(configuredRoot);
|
|
284
|
+
const missing: string[] = [];
|
|
285
|
+
let existing = target;
|
|
286
|
+
for (;;) {
|
|
287
|
+
try {
|
|
288
|
+
assertSafeDirectory(existing);
|
|
289
|
+
break;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
292
|
+
const parent = path.dirname(existing);
|
|
293
|
+
if (parent === existing) throw new Error(`Managed root is unavailable: ${configuredRoot}`);
|
|
294
|
+
missing.unshift(path.basename(existing));
|
|
295
|
+
existing = parent;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
let current = fs.realpathSync.native(existing);
|
|
299
|
+
for (const component of missing) {
|
|
300
|
+
current = path.join(current, component);
|
|
301
|
+
const created = ensureDirectoryComponent(current);
|
|
302
|
+
secureManagedDirectory(current, created, policy);
|
|
303
|
+
}
|
|
304
|
+
if (missing.length === 0) secureManagedDirectory(current, false, policy);
|
|
305
|
+
return managedDirectoryRoot(current);
|
|
306
|
+
}
|
|
182
307
|
export function managedDirectoryRoot(configuredRoot: string): ManagedDirectoryRoot {
|
|
183
308
|
const canonicalPath = fs.realpathSync.native(configuredRoot);
|
|
184
309
|
const stat = fs.lstatSync(canonicalPath, { bigint: true });
|
|
@@ -216,14 +341,16 @@ export function retainManagedDirectoryAuthority(
|
|
|
216
341
|
}
|
|
217
342
|
}
|
|
218
343
|
|
|
219
|
-
function ensureDirectoryComponent(pathname: string):
|
|
344
|
+
function ensureDirectoryComponent(pathname: string): boolean {
|
|
345
|
+
let created = false;
|
|
220
346
|
try {
|
|
221
347
|
fs.mkdirSync(pathname, { mode: 0o700 });
|
|
348
|
+
created = true;
|
|
222
349
|
} catch (error) {
|
|
223
350
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
224
351
|
}
|
|
225
352
|
assertSafeDirectory(pathname);
|
|
226
|
-
|
|
353
|
+
return created;
|
|
227
354
|
}
|
|
228
355
|
|
|
229
356
|
export function assertManagedDirectoryRoot(root: ManagedDirectoryRoot): void {
|
|
@@ -263,10 +390,66 @@ function secure(pathname: string, kind: "directory" | "file"): void {
|
|
|
263
390
|
if (!verified.ok) throw securityError(pathname, verified);
|
|
264
391
|
}
|
|
265
392
|
|
|
393
|
+
function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boolean {
|
|
394
|
+
return process.platform === "win32" && policy === "windows-existing-verify-first";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
|
|
398
|
+
const current = fs.lstatSync(pathname, { bigint: true });
|
|
399
|
+
if (current.isSymbolicLink()) throw new Error("reparse_point");
|
|
400
|
+
const expectedKind = kind === "directory" ? current.isDirectory() : current.isFile();
|
|
401
|
+
if (!expectedKind) throw new Error(kind === "directory" ? "not_directory" : "not_file");
|
|
402
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) throw new Error("identity_mismatch");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function verifyExistingManagedPathSecurity(
|
|
406
|
+
pathname: string,
|
|
407
|
+
kind: "directory" | "file",
|
|
408
|
+
expected: fs.BigIntStats,
|
|
409
|
+
): void {
|
|
410
|
+
const verified = validateNativeSecurityResult(
|
|
411
|
+
verifyOwnerOnlyPathSecurityExpected(pathname, kind, expected.dev, expected.ino),
|
|
412
|
+
"verify",
|
|
413
|
+
kind,
|
|
414
|
+
);
|
|
415
|
+
assertManagedPathIdentity(pathname, kind, expected);
|
|
416
|
+
if (!verified.ok) throw securityError(pathname, verified);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
|
|
420
|
+
const named = fs.lstatSync(pathname, { bigint: true });
|
|
421
|
+
const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
|
|
422
|
+
if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
|
|
423
|
+
const verified = validateNativeSecurityResult(
|
|
424
|
+
verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
|
|
425
|
+
"verify",
|
|
426
|
+
kind,
|
|
427
|
+
);
|
|
428
|
+
assertManagedPathIdentity(pathname, kind, named);
|
|
429
|
+
if (verified.ok) return;
|
|
430
|
+
if (verified.code !== "acl_verify_failed") throw securityError(pathname, verified);
|
|
431
|
+
const repaired = validateNativeSecurityResult(
|
|
432
|
+
repairOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
|
|
433
|
+
"verify",
|
|
434
|
+
kind,
|
|
435
|
+
);
|
|
436
|
+
if (!repaired.ok) throw securityError(pathname, repaired);
|
|
437
|
+
assertManagedPathIdentity(pathname, kind, named);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function secureManagedDirectory(pathname: string, created: boolean, policy: ManagedSessionSecurityPolicy): void {
|
|
441
|
+
if (!created && windowsExistingVerifyFirst(policy)) {
|
|
442
|
+
secureExistingManagedDirectory(pathname, "directory");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
secure(pathname, "directory");
|
|
446
|
+
}
|
|
447
|
+
|
|
266
448
|
/** Internal retained-root capability for one managed descendant subtree. */
|
|
267
449
|
export class ManagedSessionDescendantStore {
|
|
268
450
|
readonly #root: ManagedDirectoryRoot;
|
|
269
451
|
readonly #baseDir: string;
|
|
452
|
+
readonly #policy: ManagedSessionSecurityPolicy;
|
|
270
453
|
readonly #authority: RecoveryFsRoot | undefined;
|
|
271
454
|
readonly #authorityBaseDir: string;
|
|
272
455
|
readonly #subtreeRoot: ManagedDirectoryRoot;
|
|
@@ -275,32 +458,31 @@ export class ManagedSessionDescendantStore {
|
|
|
275
458
|
root: ManagedDirectoryRoot,
|
|
276
459
|
baseDir: string,
|
|
277
460
|
retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
|
|
461
|
+
policy?: ManagedSessionSecurityPolicy,
|
|
278
462
|
) {
|
|
279
|
-
assertManagedDirectoryRoot(root);
|
|
280
463
|
managedRelativePath(root, baseDir);
|
|
464
|
+
|
|
281
465
|
this.#root = root;
|
|
282
466
|
this.#baseDir = path.resolve(baseDir);
|
|
467
|
+
this.#policy = policy ?? "default";
|
|
283
468
|
this.#authorityBaseDir = retained?.authorityBaseDir ?? this.#baseDir;
|
|
284
469
|
if (retained) {
|
|
285
470
|
const relative = path.relative(retained.authorityBaseDir, this.#baseDir).split(path.sep).join("/");
|
|
286
471
|
const captured = retained.authority.snapshotManagedTree(relative);
|
|
287
472
|
if (!captured.ok || !captured.snapshot)
|
|
288
473
|
throw new Error(captured.code ?? "Managed subtree identity unavailable");
|
|
289
|
-
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
290
|
-
if (
|
|
291
|
-
captured.snapshot.rootDev !== subtreeStat.dev.toString() ||
|
|
292
|
-
captured.snapshot.rootIno !== subtreeStat.ino.toString()
|
|
293
|
-
)
|
|
294
|
-
throw new Error("Managed descendant root identity changed");
|
|
295
474
|
this.#subtreeRoot = Object.freeze({
|
|
296
475
|
canonicalPath: this.#baseDir,
|
|
297
|
-
dev:
|
|
298
|
-
ino:
|
|
476
|
+
dev: BigInt(captured.snapshot.rootDev),
|
|
477
|
+
ino: BigInt(captured.snapshot.rootIno),
|
|
299
478
|
});
|
|
300
479
|
this.#authority = retained.authority;
|
|
480
|
+
this.#assertBound();
|
|
481
|
+
|
|
301
482
|
return;
|
|
302
483
|
}
|
|
303
|
-
|
|
484
|
+
assertManagedDirectoryRoot(root);
|
|
485
|
+
ensureManagedDirectory(this.#baseDir, root, this.#policy);
|
|
304
486
|
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
305
487
|
this.#subtreeRoot = Object.freeze({ canonicalPath: this.#baseDir, dev: subtreeStat.dev, ino: subtreeStat.ino });
|
|
306
488
|
if (process.platform === "linux") {
|
|
@@ -332,19 +514,28 @@ export class ManagedSessionDescendantStore {
|
|
|
332
514
|
return this.#subtreeRoot;
|
|
333
515
|
}
|
|
334
516
|
|
|
517
|
+
get securityPolicy(): ManagedSessionSecurityPolicy {
|
|
518
|
+
return this.#policy;
|
|
519
|
+
}
|
|
520
|
+
|
|
335
521
|
deriveSubtree(relativePath: string): ManagedSessionDescendantStore {
|
|
336
522
|
const child = this.ensureDirectory(relativePath);
|
|
337
523
|
const resolved = this.#resolve(relativePath);
|
|
338
|
-
if (!this.#authority) return new ManagedSessionDescendantStore(this.#root, resolved);
|
|
524
|
+
if (!this.#authority) return new ManagedSessionDescendantStore(this.#root, resolved, undefined, this.#policy);
|
|
339
525
|
const retainedChild = this.#authority.retainManagedDirectory(
|
|
340
526
|
this.#relative(resolved),
|
|
341
527
|
child.dev.toString(),
|
|
342
528
|
child.ino.toString(),
|
|
343
529
|
);
|
|
344
|
-
return new ManagedSessionDescendantStore(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
530
|
+
return new ManagedSessionDescendantStore(
|
|
531
|
+
this.#root,
|
|
532
|
+
resolved,
|
|
533
|
+
{
|
|
534
|
+
authority: retainedChild,
|
|
535
|
+
authorityBaseDir: resolved,
|
|
536
|
+
},
|
|
537
|
+
this.#policy,
|
|
538
|
+
);
|
|
348
539
|
}
|
|
349
540
|
|
|
350
541
|
retainAuthority(): RecoveryFsRoot | undefined {
|
|
@@ -371,12 +562,19 @@ export class ManagedSessionDescendantStore {
|
|
|
371
562
|
return;
|
|
372
563
|
}
|
|
373
564
|
} else {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
565
|
+
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
566
|
+
if (!named.isDirectory() || named.isSymbolicLink())
|
|
567
|
+
throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
|
|
568
|
+
if (windowsExistingVerifyFirst(this.#policy))
|
|
569
|
+
verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
|
|
570
|
+
else {
|
|
571
|
+
const verified = validateNativeSecurityResult(
|
|
572
|
+
verifyOwnerOnlyPathSecurity(this.#baseDir, "directory"),
|
|
573
|
+
"verify",
|
|
574
|
+
"directory",
|
|
575
|
+
);
|
|
576
|
+
if (!verified.ok) throw securityError(this.#baseDir, verified);
|
|
577
|
+
}
|
|
380
578
|
}
|
|
381
579
|
this.#assertBound();
|
|
382
580
|
}
|
|
@@ -421,7 +619,7 @@ export class ManagedSessionDescendantStore {
|
|
|
421
619
|
ino: BigInt(ensured.identity.ino),
|
|
422
620
|
});
|
|
423
621
|
}
|
|
424
|
-
ensureManagedDirectory(this.#resolve(relativePath), this.#root);
|
|
622
|
+
ensureManagedDirectory(this.#resolve(relativePath), this.#root, this.#policy);
|
|
425
623
|
const named = fs.lstatSync(this.#resolve(relativePath), { bigint: true });
|
|
426
624
|
return Object.freeze({ canonicalPath: this.#resolve(relativePath), dev: named.dev, ino: named.ino });
|
|
427
625
|
}
|
|
@@ -434,13 +632,13 @@ export class ManagedSessionDescendantStore {
|
|
|
434
632
|
this.#assertBound();
|
|
435
633
|
return;
|
|
436
634
|
}
|
|
437
|
-
await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root);
|
|
635
|
+
await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root, this.#policy);
|
|
438
636
|
}
|
|
439
637
|
|
|
440
638
|
publishNoReplaceSync(relativePath: string, bytes: Uint8Array): void {
|
|
441
639
|
const resolved = this.#resolve(relativePath);
|
|
442
640
|
if (!this.#authority) {
|
|
443
|
-
publishManagedFileNoReplaceSync(resolved, bytes, this.#root);
|
|
641
|
+
publishManagedFileNoReplaceSync(resolved, bytes, this.#root, this.#policy);
|
|
444
642
|
this.#assertBound();
|
|
445
643
|
return;
|
|
446
644
|
}
|
|
@@ -453,7 +651,7 @@ export class ManagedSessionDescendantStore {
|
|
|
453
651
|
this.#assertBound();
|
|
454
652
|
const resolved = this.#resolve(relativePath);
|
|
455
653
|
if (!this.#authority) {
|
|
456
|
-
await replaceManagedFile(resolved, bytes, this.#subtreeRoot);
|
|
654
|
+
await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
457
655
|
this.#assertBound();
|
|
458
656
|
return;
|
|
459
657
|
}
|
|
@@ -466,7 +664,7 @@ export class ManagedSessionDescendantStore {
|
|
|
466
664
|
this.#assertBound();
|
|
467
665
|
const resolved = this.#resolve(relativePath);
|
|
468
666
|
if (!this.#authority) {
|
|
469
|
-
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot);
|
|
667
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
470
668
|
this.#assertBound();
|
|
471
669
|
return;
|
|
472
670
|
}
|
|
@@ -477,7 +675,7 @@ export class ManagedSessionDescendantStore {
|
|
|
477
675
|
appendSync(relativePath: string, bytes: Uint8Array): void {
|
|
478
676
|
this.#assertBound();
|
|
479
677
|
const resolved = this.#resolve(relativePath);
|
|
480
|
-
|
|
678
|
+
let existing = this.readExpected(relativePath);
|
|
481
679
|
if (!existing) throw new Error("managed_append_missing");
|
|
482
680
|
if (this.#authority) {
|
|
483
681
|
const appended = this.#authority.appendManaged(
|
|
@@ -494,20 +692,49 @@ export class ManagedSessionDescendantStore {
|
|
|
494
692
|
this.#assertBound();
|
|
495
693
|
return;
|
|
496
694
|
}
|
|
695
|
+
// On Darwin, the first write-append open can change only ctime (e.g. com.apple.provenance)
|
|
696
|
+
// while leaving dev/ino/size/mtime/content intact. Allow one bounded refresh+retry for that
|
|
697
|
+
// transition only; any other identity change or a second ctime transition stays fail-closed.
|
|
698
|
+
// See https://github.com/Yeachan-Heo/gajae-code/issues/2944
|
|
497
699
|
let fd: number | undefined;
|
|
700
|
+
let darwinCtimeRefreshConsumed = false;
|
|
498
701
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
702
|
+
for (;;) {
|
|
703
|
+
fd = fs.openSync(resolved, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW);
|
|
704
|
+
const before = identity(fs.fstatSync(fd, { bigint: true }));
|
|
705
|
+
if (!sameIdentity(before, existing.identity)) {
|
|
706
|
+
const allowDarwinCtimeRefresh =
|
|
707
|
+
process.platform === "darwin" &&
|
|
708
|
+
!darwinCtimeRefreshConsumed &&
|
|
709
|
+
isCtimeOnlyIdentityMismatch(before, existing.identity);
|
|
710
|
+
if (!allowDarwinCtimeRefresh) throw new Error("identity_mismatch");
|
|
711
|
+
fs.closeSync(fd);
|
|
712
|
+
fd = undefined;
|
|
713
|
+
darwinCtimeRefreshConsumed = true;
|
|
714
|
+
const original = existing;
|
|
715
|
+
const refreshed = this.readExpected(relativePath);
|
|
716
|
+
if (
|
|
717
|
+
!refreshed ||
|
|
718
|
+
!sameStableIdentityIgnoringCtime(refreshed.identity, original.identity) ||
|
|
719
|
+
refreshed.identity.sha256 !== original.identity.sha256 ||
|
|
720
|
+
!refreshed.bytes.equals(original.bytes)
|
|
721
|
+
) {
|
|
722
|
+
throw new Error("identity_mismatch");
|
|
723
|
+
}
|
|
724
|
+
existing = refreshed;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
728
|
+
let offset = 0;
|
|
729
|
+
while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
730
|
+
fs.fsyncSync(fd);
|
|
731
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
732
|
+
const after = identity(fs.fstatSync(fd, { bigint: true }));
|
|
733
|
+
const named = identity(fs.lstatSync(resolved, { bigint: true }));
|
|
734
|
+
if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
|
|
735
|
+
throw new Error("identity_mismatch");
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
511
738
|
} finally {
|
|
512
739
|
if (fd !== undefined) fs.closeSync(fd);
|
|
513
740
|
}
|
|
@@ -528,6 +755,7 @@ export class ManagedSessionDescendantStore {
|
|
|
528
755
|
if (!created.ok) throw new Error(created.code ?? "managed_publish_failed");
|
|
529
756
|
if (!created.identity) throw new Error("managed_publish_identity_unavailable");
|
|
530
757
|
let captured: ReturnType<RecoveryFsRoot["readManaged"]> | undefined;
|
|
758
|
+
let published: unknown;
|
|
531
759
|
try {
|
|
532
760
|
captured = this.#authority.readManaged(temporary);
|
|
533
761
|
if (!captured.ok || !captured.identity || !captured.data)
|
|
@@ -553,7 +781,7 @@ export class ManagedSessionDescendantStore {
|
|
|
553
781
|
digest,
|
|
554
782
|
);
|
|
555
783
|
if (!synced.ok) throw new Error(synced.code ?? "managed_publish_fsync_failed");
|
|
556
|
-
|
|
784
|
+
published = this.#authority.renameManagedFileNoReplace(
|
|
557
785
|
temporary,
|
|
558
786
|
relative,
|
|
559
787
|
captured.identity.dev,
|
|
@@ -563,35 +791,14 @@ export class ManagedSessionDescendantStore {
|
|
|
563
791
|
captured.identity.ctimeNs,
|
|
564
792
|
digest,
|
|
565
793
|
);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
published.code === "already_exists"
|
|
569
|
-
? "destination_conflict"
|
|
570
|
-
: (published.code ?? "managed_publish_failed"),
|
|
571
|
-
);
|
|
794
|
+
const outcome = classifyNativePublishOutcome(published, "retained_file");
|
|
795
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
572
796
|
} catch (error) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
installed.identity &&
|
|
579
|
-
installed.data &&
|
|
580
|
-
installed.identity.dev === captured.identity.dev &&
|
|
581
|
-
installed.identity.ino === captured.identity.ino &&
|
|
582
|
-
(installed.identity.sha256 ?? createHash("sha256").update(installed.data).digest("hex")) === digest
|
|
583
|
-
) {
|
|
584
|
-
const removed = this.#authority.removeManaged(
|
|
585
|
-
relative,
|
|
586
|
-
installed.identity.dev,
|
|
587
|
-
installed.identity.ino,
|
|
588
|
-
installed.identity.size,
|
|
589
|
-
installed.identity.mtimeNs,
|
|
590
|
-
installed.identity.ctimeNs,
|
|
591
|
-
digest,
|
|
592
|
-
);
|
|
593
|
-
if (!removed.ok) throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
594
|
-
}
|
|
797
|
+
// A committed or unknown native outcome is evidence, not authorization to
|
|
798
|
+
// probe or remove the destination. Only a validated pre-mutation result
|
|
799
|
+
// may clean this operation's still-named staging object.
|
|
800
|
+
const outcome = captured ? classifyNativePublishOutcome(published, "retained_file") : undefined;
|
|
801
|
+
if (captured?.ok && captured.identity && captured.data && outcome && mayCleanCurrentStaging(outcome)) {
|
|
595
802
|
const staged = this.#authority.readManaged(temporary);
|
|
596
803
|
if (
|
|
597
804
|
staged.ok &&
|
|
@@ -609,7 +816,8 @@ export class ManagedSessionDescendantStore {
|
|
|
609
816
|
staged.identity.ctimeNs,
|
|
610
817
|
staged.identity.sha256 ?? createHash("sha256").update(staged.data).digest("hex"),
|
|
611
818
|
);
|
|
612
|
-
if (!removed.ok
|
|
819
|
+
if (!removed.ok && removed.code !== "cleanup_pending")
|
|
820
|
+
throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
613
821
|
}
|
|
614
822
|
}
|
|
615
823
|
throw error;
|
|
@@ -682,7 +890,17 @@ export class ManagedSessionDescendantStore {
|
|
|
682
890
|
sha256: expected.identity.sha256,
|
|
683
891
|
quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
|
|
684
892
|
});
|
|
685
|
-
if (
|
|
893
|
+
if (
|
|
894
|
+
!removed.ok &&
|
|
895
|
+
!(
|
|
896
|
+
removed.code === "cleanup_pending" &&
|
|
897
|
+
(removed.detachedPath ??
|
|
898
|
+
removed.retainedSuccessorPath ??
|
|
899
|
+
removed.retainedPlaceholderPath ??
|
|
900
|
+
removed.retainedUnknownPath) !== undefined
|
|
901
|
+
)
|
|
902
|
+
)
|
|
903
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
686
904
|
this.#assertBound();
|
|
687
905
|
return;
|
|
688
906
|
}
|
|
@@ -695,7 +913,8 @@ export class ManagedSessionDescendantStore {
|
|
|
695
913
|
expected.identity.ctimeNs.toString(),
|
|
696
914
|
expected.identity.sha256,
|
|
697
915
|
);
|
|
698
|
-
if (!removed.ok
|
|
916
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
917
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
699
918
|
this.#assertBound();
|
|
700
919
|
}
|
|
701
920
|
/** Read and remove one managed descendant through retained authority. */
|
|
@@ -719,7 +938,8 @@ export class ManagedSessionDescendantStore {
|
|
|
719
938
|
existing.identity.ctimeNs,
|
|
720
939
|
existing.identity.sha256 ?? createHash("sha256").update(existing.data).digest("hex"),
|
|
721
940
|
);
|
|
722
|
-
if (!removed.ok
|
|
941
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
942
|
+
throw new Error(removed.code ?? "managed_consume_failed");
|
|
723
943
|
this.#assertBound();
|
|
724
944
|
return existing.data;
|
|
725
945
|
}
|
|
@@ -805,92 +1025,31 @@ export class ManagedSessionDescendantStore {
|
|
|
805
1025
|
sourceRelativePath: string,
|
|
806
1026
|
destinationRelativePath: string,
|
|
807
1027
|
expected: NativeDirectoryTreeSnapshot,
|
|
808
|
-
):
|
|
1028
|
+
): NativeDirectoryTreeSnapshot {
|
|
809
1029
|
this.#assertBound();
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
try {
|
|
816
|
-
this.ensureDirectory(destinationRelativePath);
|
|
817
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
818
|
-
for (const entry of expected.entries) {
|
|
819
|
-
if (entry.relativePath === "") continue;
|
|
820
|
-
const target = path.posix.join(destinationRelativePath.replaceAll(path.sep, "/"), entry.relativePath);
|
|
821
|
-
if (entry.kind === "directory") {
|
|
822
|
-
this.ensureDirectory(target);
|
|
823
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
824
|
-
continue;
|
|
825
|
-
}
|
|
826
|
-
const sourceEntry = this.readExpected(
|
|
827
|
-
path.posix.join(sourceRelativePath.replaceAll(path.sep, "/"), entry.relativePath),
|
|
828
|
-
);
|
|
829
|
-
if (
|
|
830
|
-
!sourceEntry ||
|
|
831
|
-
sourceEntry.identity.dev.toString() !== entry.dev ||
|
|
832
|
-
sourceEntry.identity.ino.toString() !== entry.ino ||
|
|
833
|
-
sourceEntry.identity.size.toString() !== entry.size ||
|
|
834
|
-
sourceEntry.identity.mtimeNs.toString() !== entry.mtimeNs ||
|
|
835
|
-
sourceEntry.identity.sha256 !== entry.sha256
|
|
836
|
-
)
|
|
837
|
-
throw new Error("artifact_source_changed");
|
|
838
|
-
this.publishNoReplaceSync(target, sourceEntry.bytes);
|
|
839
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
840
|
-
}
|
|
841
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
842
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
843
|
-
tree.entries.map(entry => ({
|
|
844
|
-
relativePath: entry.relativePath,
|
|
845
|
-
kind: entry.kind,
|
|
846
|
-
size: entry.size,
|
|
847
|
-
sha256: entry.sha256,
|
|
848
|
-
}));
|
|
849
|
-
if (JSON.stringify(comparable(destinationSnapshot)) !== JSON.stringify(comparable(expected)))
|
|
850
|
-
throw new Error("artifact_destination_mismatch");
|
|
851
|
-
const terminalSource = snapshotDirectoryTree(source);
|
|
852
|
-
if (
|
|
853
|
-
!terminalSource.ok ||
|
|
854
|
-
!terminalSource.snapshot ||
|
|
855
|
-
JSON.stringify(terminalSource.snapshot) !== JSON.stringify(expected)
|
|
1030
|
+
const moved = this.#authority
|
|
1031
|
+
? this.#authority.renameManagedTreeNoReplace(
|
|
1032
|
+
this.#relative(this.#resolve(sourceRelativePath)),
|
|
1033
|
+
this.#relative(this.#resolve(destinationRelativePath)),
|
|
1034
|
+
expected,
|
|
856
1035
|
)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
this.#relative(this.#resolve(destinationRelativePath)),
|
|
873
|
-
expected,
|
|
874
|
-
);
|
|
875
|
-
if (!moved.ok) {
|
|
876
|
-
try {
|
|
877
|
-
const installed = this.captureTree(destinationRelativePath);
|
|
878
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
879
|
-
tree.entries.map(entry => ({
|
|
880
|
-
relativePath: entry.relativePath,
|
|
881
|
-
kind: entry.kind,
|
|
882
|
-
size: entry.size,
|
|
883
|
-
sha256: entry.sha256,
|
|
884
|
-
}));
|
|
885
|
-
if (JSON.stringify(comparable(installed)) !== JSON.stringify(comparable(expected)))
|
|
886
|
-
throw new Error("managed_publish_reconcile_failed");
|
|
887
|
-
this.removeTreeExpected(destinationRelativePath, installed);
|
|
888
|
-
} catch (error) {
|
|
889
|
-
if (!(error instanceof Error) || error.message !== "not_found") throw error;
|
|
890
|
-
}
|
|
891
|
-
throw new Error(moved.code ?? "managed_move_failed");
|
|
892
|
-
}
|
|
1036
|
+
: renameNoReplacePath(this.#resolve(sourceRelativePath), this.#resolve(destinationRelativePath));
|
|
1037
|
+
const outcome = classifyNativePublishOutcome(moved, this.#authority ? "retained_tree" : "direct_rename");
|
|
1038
|
+
if (!outcome.ok)
|
|
1039
|
+
throw new ManagedTreeMoveOutcomeError(publishFailure(outcome).message, mayCleanCurrentStaging(outcome));
|
|
1040
|
+
const movedSnapshot = this.#authority
|
|
1041
|
+
? this.#authority.snapshotManagedTree(this.#relative(this.#resolve(destinationRelativePath)))
|
|
1042
|
+
: snapshotDirectoryTree(this.#resolve(destinationRelativePath));
|
|
1043
|
+
if (
|
|
1044
|
+
!movedSnapshot.ok ||
|
|
1045
|
+
!movedSnapshot.snapshot ||
|
|
1046
|
+
!sameDirectoryTreeSnapshotAfterRename(movedSnapshot.snapshot, expected)
|
|
1047
|
+
)
|
|
1048
|
+
throw new ManagedTreeMoveOutcomeError("artifact_destination_mismatch", false);
|
|
1049
|
+
if (!this.#authority) fsyncDirectory(this.#baseDir);
|
|
1050
|
+
|
|
893
1051
|
this.#assertBound();
|
|
1052
|
+
return movedSnapshot.snapshot;
|
|
894
1053
|
}
|
|
895
1054
|
|
|
896
1055
|
removeTreeExpected(relativePath: string, expected: NativeDirectoryTreeSnapshot): void {
|
|
@@ -1018,6 +1177,24 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
|
|
|
1018
1177
|
);
|
|
1019
1178
|
}
|
|
1020
1179
|
|
|
1180
|
+
/** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
|
|
1181
|
+
function sameStableIdentityIgnoringCtime(
|
|
1182
|
+
left: ManagedFileSnapshot["identity"],
|
|
1183
|
+
right: ManagedFileSnapshot["identity"],
|
|
1184
|
+
): boolean {
|
|
1185
|
+
return (
|
|
1186
|
+
left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** True when observed identity matches expected on every field except ctimeNs. */
|
|
1191
|
+
function isCtimeOnlyIdentityMismatch(
|
|
1192
|
+
observed: ManagedFileSnapshot["identity"],
|
|
1193
|
+
expected: ManagedFileSnapshot["identity"],
|
|
1194
|
+
): boolean {
|
|
1195
|
+
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1021
1198
|
function parseLock(pathname: string): LockRecord | undefined {
|
|
1022
1199
|
try {
|
|
1023
1200
|
const stat = fs.lstatSync(pathname);
|
|
@@ -1060,25 +1237,43 @@ function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean
|
|
|
1060
1237
|
}
|
|
1061
1238
|
|
|
1062
1239
|
/** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
|
|
1063
|
-
export function ensureManagedDirectory(
|
|
1240
|
+
export function ensureManagedDirectory(
|
|
1241
|
+
pathname: string,
|
|
1242
|
+
root?: ManagedDirectoryRoot,
|
|
1243
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1244
|
+
): void {
|
|
1064
1245
|
if (!root) {
|
|
1246
|
+
let existed = false;
|
|
1247
|
+
try {
|
|
1248
|
+
const named = fs.lstatSync(pathname);
|
|
1249
|
+
existed = true;
|
|
1250
|
+
if (!named.isDirectory() || named.isSymbolicLink()) throw new Error(`Unsafe managed directory: ${pathname}`);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1253
|
+
}
|
|
1065
1254
|
fs.mkdirSync(pathname, { recursive: true, mode: 0o700 });
|
|
1066
1255
|
assertSafeDirectory(pathname);
|
|
1067
|
-
|
|
1256
|
+
if (existed && windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(pathname, "directory");
|
|
1257
|
+
else secure(pathname, "directory");
|
|
1068
1258
|
return;
|
|
1069
1259
|
}
|
|
1070
1260
|
ensureManagedRoot(root);
|
|
1261
|
+
const components = managedRelativePath(root, pathname);
|
|
1262
|
+
if (components.length === 0) {
|
|
1263
|
+
if (windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(root.canonicalPath, "directory");
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1071
1266
|
let current = root.canonicalPath;
|
|
1072
|
-
|
|
1267
|
+
if (windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(current, "directory");
|
|
1268
|
+
else secure(current, "directory");
|
|
1269
|
+
for (const component of components) {
|
|
1073
1270
|
// Re-inspect the captured root or already-secured descendant before every
|
|
1074
1271
|
// descent; a replaced component cannot grant authority to the next one.
|
|
1075
1272
|
if (current === root.canonicalPath) assertManagedDirectoryRoot(root);
|
|
1076
|
-
else
|
|
1077
|
-
assertSafeDirectory(current);
|
|
1078
|
-
secure(current, "directory");
|
|
1079
|
-
}
|
|
1273
|
+
else assertSafeDirectory(current);
|
|
1080
1274
|
current = path.join(current, component);
|
|
1081
|
-
ensureDirectoryComponent(current);
|
|
1275
|
+
const created = ensureDirectoryComponent(current);
|
|
1276
|
+
secureManagedDirectory(current, created, policy);
|
|
1082
1277
|
}
|
|
1083
1278
|
}
|
|
1084
1279
|
|
|
@@ -1123,15 +1318,17 @@ export async function publishManagedFileNoReplace(
|
|
|
1123
1318
|
bytes: Uint8Array,
|
|
1124
1319
|
assertOwned?: () => void,
|
|
1125
1320
|
root?: ManagedDirectoryRoot,
|
|
1321
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1126
1322
|
): Promise<void> {
|
|
1127
1323
|
const parent = path.dirname(destination);
|
|
1128
|
-
ensureManagedDirectory(parent, root);
|
|
1324
|
+
ensureManagedDirectory(parent, root, policy);
|
|
1129
1325
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1130
1326
|
let fd: number | undefined;
|
|
1131
1327
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1132
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1133
|
-
let committed = false;
|
|
1134
1328
|
let failure: unknown;
|
|
1329
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1330
|
+
let renameAttempted = false;
|
|
1331
|
+
|
|
1135
1332
|
try {
|
|
1136
1333
|
assertOwned?.();
|
|
1137
1334
|
fd = fs.openSync(
|
|
@@ -1146,17 +1343,13 @@ export async function publishManagedFileNoReplace(
|
|
|
1146
1343
|
secureFileDescriptor(staging, fd, "verify");
|
|
1147
1344
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1148
1345
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1149
|
-
stagingSnapshot = {
|
|
1150
|
-
bytes: Buffer.from(bytes),
|
|
1151
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1152
|
-
};
|
|
1153
1346
|
assertOwned?.();
|
|
1154
1347
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1348
|
+
renameAttempted = true;
|
|
1349
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1350
|
+
|
|
1351
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1352
|
+
|
|
1160
1353
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1161
1354
|
if (
|
|
1162
1355
|
!named.isFile() ||
|
|
@@ -1171,25 +1364,12 @@ export async function publishManagedFileNoReplace(
|
|
|
1171
1364
|
fd = undefined;
|
|
1172
1365
|
|
|
1173
1366
|
fsyncDirectory(parent);
|
|
1174
|
-
committed = true;
|
|
1175
1367
|
} catch (error) {
|
|
1176
1368
|
failure = error;
|
|
1177
1369
|
} finally {
|
|
1178
1370
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1182
|
-
if (
|
|
1183
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1184
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1185
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1186
|
-
)
|
|
1187
|
-
await unlinkManagedFileVerified(destination, installed.identity);
|
|
1188
|
-
} catch (error) {
|
|
1189
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
if (stagingIdentity) {
|
|
1371
|
+
|
|
1372
|
+
if (stagingIdentity && (!renameAttempted || (outcome && mayCleanCurrentStaging(outcome)))) {
|
|
1193
1373
|
await fsp
|
|
1194
1374
|
.lstat(staging, { bigint: true })
|
|
1195
1375
|
.then(stat => {
|
|
@@ -1208,15 +1388,16 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1208
1388
|
destination: string,
|
|
1209
1389
|
bytes: Uint8Array,
|
|
1210
1390
|
root?: ManagedDirectoryRoot,
|
|
1391
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1211
1392
|
): void {
|
|
1212
1393
|
const parent = path.dirname(destination);
|
|
1213
|
-
ensureManagedDirectory(parent, root);
|
|
1394
|
+
ensureManagedDirectory(parent, root, policy);
|
|
1214
1395
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1215
1396
|
let fd: number | undefined;
|
|
1216
1397
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1217
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1218
|
-
let committed = false;
|
|
1219
1398
|
let failure: unknown;
|
|
1399
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1400
|
+
|
|
1220
1401
|
try {
|
|
1221
1402
|
fd = fs.openSync(
|
|
1222
1403
|
staging,
|
|
@@ -1230,16 +1411,10 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1230
1411
|
secureFileDescriptor(staging, fd, "verify");
|
|
1231
1412
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1232
1413
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1233
|
-
stagingSnapshot = {
|
|
1234
|
-
bytes: Buffer.from(bytes),
|
|
1235
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1236
|
-
};
|
|
1237
1414
|
|
|
1238
|
-
|
|
1239
|
-
if (!
|
|
1240
|
-
|
|
1241
|
-
throw new Error("durability_failed");
|
|
1242
|
-
}
|
|
1415
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1416
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1417
|
+
|
|
1243
1418
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1244
1419
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
1245
1420
|
throw new Error("destination_identity_changed");
|
|
@@ -1248,35 +1423,12 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1248
1423
|
fs.closeSync(fd);
|
|
1249
1424
|
fd = undefined;
|
|
1250
1425
|
fsyncDirectory(parent);
|
|
1251
|
-
committed = true;
|
|
1252
1426
|
} catch (error) {
|
|
1253
1427
|
failure = error;
|
|
1254
1428
|
} finally {
|
|
1255
1429
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1256
1430
|
}
|
|
1257
|
-
if (stagingIdentity) {
|
|
1258
|
-
if (!committed && stagingSnapshot) {
|
|
1259
|
-
try {
|
|
1260
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1261
|
-
if (
|
|
1262
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1263
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1264
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1265
|
-
) {
|
|
1266
|
-
const removed = exactUnlink(destination, {
|
|
1267
|
-
dev: installed.identity.dev,
|
|
1268
|
-
ino: installed.identity.ino,
|
|
1269
|
-
size: BigInt(installed.identity.size),
|
|
1270
|
-
mtimeNs: installed.identity.mtimeNs,
|
|
1271
|
-
sha256: installed.identity.sha256,
|
|
1272
|
-
quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
|
|
1273
|
-
});
|
|
1274
|
-
if (!removed.ok) failure ??= new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
1275
|
-
}
|
|
1276
|
-
} catch (error) {
|
|
1277
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1431
|
+
if (stagingIdentity && outcome && mayCleanCurrentStaging(outcome)) {
|
|
1280
1432
|
try {
|
|
1281
1433
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
1282
1434
|
if (named.dev !== stagingIdentity.dev || named.ino !== stagingIdentity.ino) {
|
|
@@ -1291,9 +1443,14 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1291
1443
|
}
|
|
1292
1444
|
|
|
1293
1445
|
/** Replace one managed regular file while retaining the secured staging fd through publication. */
|
|
1294
|
-
export function replaceManagedFileSync(
|
|
1446
|
+
export function replaceManagedFileSync(
|
|
1447
|
+
destination: string,
|
|
1448
|
+
bytes: Uint8Array,
|
|
1449
|
+
root: ManagedDirectoryRoot,
|
|
1450
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1451
|
+
): void {
|
|
1295
1452
|
const parent = path.dirname(destination);
|
|
1296
|
-
ensureManagedDirectory(parent, root);
|
|
1453
|
+
ensureManagedDirectory(parent, root, policy);
|
|
1297
1454
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
|
|
1298
1455
|
let fd: number | undefined;
|
|
1299
1456
|
let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
@@ -1338,8 +1495,9 @@ export async function replaceManagedFile(
|
|
|
1338
1495
|
destination: string,
|
|
1339
1496
|
bytes: Uint8Array,
|
|
1340
1497
|
root: ManagedDirectoryRoot,
|
|
1498
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1341
1499
|
): Promise<void> {
|
|
1342
|
-
replaceManagedFileSync(destination, bytes, root);
|
|
1500
|
+
replaceManagedFileSync(destination, bytes, root, policy);
|
|
1343
1501
|
}
|
|
1344
1502
|
|
|
1345
1503
|
/** Copy the exact bytes captured from one no-follow source descriptor. */
|
|
@@ -1348,11 +1506,12 @@ export async function copyManagedFileNoReplace(
|
|
|
1348
1506
|
destination: string,
|
|
1349
1507
|
snapshot = captureManagedFileNoFollow(source),
|
|
1350
1508
|
root?: ManagedDirectoryRoot,
|
|
1509
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1351
1510
|
): Promise<void> {
|
|
1352
1511
|
const named = captureManagedFileNoFollow(source);
|
|
1353
1512
|
if (!sameIdentity(snapshot.identity, named.identity) || !snapshot.bytes.equals(named.bytes))
|
|
1354
1513
|
throw new Error("source_changed");
|
|
1355
|
-
await publishManagedFileNoReplace(destination, snapshot.bytes, undefined, root);
|
|
1514
|
+
await publishManagedFileNoReplace(destination, snapshot.bytes, undefined, root, policy);
|
|
1356
1515
|
const destinationSnapshot = captureManagedFileNoFollow(destination);
|
|
1357
1516
|
if (!destinationSnapshot.bytes.equals(snapshot.bytes)) throw new Error("durability_failed");
|
|
1358
1517
|
}
|
|
@@ -1362,8 +1521,9 @@ export async function acquireManagedLock(
|
|
|
1362
1521
|
locksDirectory: string,
|
|
1363
1522
|
name: string,
|
|
1364
1523
|
root?: ManagedDirectoryRoot,
|
|
1524
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1365
1525
|
): Promise<ManagedStorageLock> {
|
|
1366
|
-
ensureManagedDirectory(locksDirectory, root);
|
|
1526
|
+
ensureManagedDirectory(locksDirectory, root, policy);
|
|
1367
1527
|
const lockPath = path.join(locksDirectory, `${name}.lock`);
|
|
1368
1528
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1369
1529
|
let staleObservedAt: number | undefined;
|
|
@@ -1513,7 +1673,10 @@ export function fsyncManagedArtifactTree(root: string): NativeDirectoryTreeSnaps
|
|
|
1513
1673
|
const stat = fs.lstatSync(pathname);
|
|
1514
1674
|
if (stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
|
|
1515
1675
|
if (stat.isFile()) {
|
|
1516
|
-
const fd = fs.openSync(
|
|
1676
|
+
const fd = fs.openSync(
|
|
1677
|
+
pathname,
|
|
1678
|
+
(process.platform === "win32" ? fs.constants.O_WRONLY : fs.constants.O_RDONLY) | fs.constants.O_NOFOLLOW,
|
|
1679
|
+
);
|
|
1517
1680
|
try {
|
|
1518
1681
|
fs.fsyncSync(fd);
|
|
1519
1682
|
} finally {
|