@gajae-code/coding-agent 0.12.0 → 0.12.2
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 +111 -0
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +56 -2
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +40 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +7 -1
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +9 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +290 -0
- package/src/async/job-manager.ts +99 -39
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +53 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +491 -27
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
- package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
- package/src/gjc-runtime/workflow-manifest.ts +21 -70
- package/src/internal-urls/docs-index.generated.ts +16 -16
- package/src/modes/DESIGN.md +104 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -15
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +42 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +193 -34
- package/src/modes/controllers/selector-controller.ts +160 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/runtime-init.ts +3 -0
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +7 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +89 -9
- package/src/session/agent-session.ts +844 -173
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +200 -111
- package/src/session/internal/managed-session-storage.ts +492 -62
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +38 -45
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +280 -45
- package/src/task/output-manager.ts +24 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +34 -8
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +6 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
applyOwnerOnlyFdSecurity,
|
|
7
7
|
applyOwnerOnlyPathSecurity,
|
|
8
8
|
exactRemoveDirectoryTree,
|
|
9
|
+
exactReplacePath,
|
|
9
10
|
exactUnlink,
|
|
10
11
|
type NativeDirectoryTreeSnapshot,
|
|
12
|
+
type NativeExactUnlinkResult,
|
|
11
13
|
type NativeOwnerOnlySecurityResult,
|
|
12
14
|
openRecoveryFsRoot,
|
|
13
15
|
type RecoveryFsRoot,
|
|
@@ -34,7 +36,6 @@ const LOCK_LEASE_MS = 60_000;
|
|
|
34
36
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
35
37
|
const LOCK_WAIT_MS = 5_000;
|
|
36
38
|
|
|
37
|
-
const LOCK_STALE_RECHECK_MS = 100;
|
|
38
39
|
export class ManagedPublishError extends Error {
|
|
39
40
|
readonly classification:
|
|
40
41
|
| "destination_conflict"
|
|
@@ -44,12 +45,14 @@ export class ManagedPublishError extends Error {
|
|
|
44
45
|
| "identity_mismatch"
|
|
45
46
|
| "io_error"
|
|
46
47
|
| "durability_failed";
|
|
48
|
+
readonly code: ManagedPublishError["classification"];
|
|
47
49
|
readonly diagnostic: string;
|
|
48
50
|
|
|
49
51
|
constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
|
|
50
52
|
super(classification);
|
|
51
53
|
this.name = "ManagedPublishError";
|
|
52
54
|
this.classification = classification;
|
|
55
|
+
this.code = classification;
|
|
53
56
|
this.diagnostic = formatNativePublishDiagnostic(outcome);
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -134,15 +137,24 @@ export interface ManagedStorageLock {
|
|
|
134
137
|
assertOwned(): void;
|
|
135
138
|
release(): Promise<void>;
|
|
136
139
|
}
|
|
140
|
+
export interface ManagedStorageLockSync {
|
|
141
|
+
path: string;
|
|
142
|
+
attemptId: string;
|
|
143
|
+
assertOwned(): void;
|
|
144
|
+
release(): void;
|
|
145
|
+
}
|
|
137
146
|
|
|
138
147
|
export interface ManagedFileSnapshot {
|
|
139
148
|
bytes: Buffer;
|
|
140
149
|
identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint; ctimeNs: bigint; sha256: string };
|
|
141
150
|
}
|
|
142
151
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
export const MANAGED_SESSION_SECURITY_CODES = [
|
|
153
|
+
"acl_denied",
|
|
154
|
+
"acl_io_error",
|
|
155
|
+
"acl_present",
|
|
156
|
+
"acl_malformed",
|
|
157
|
+
"acl_unknown",
|
|
146
158
|
"acl_unavailable",
|
|
147
159
|
"acl_apply_failed",
|
|
148
160
|
"acl_verify_failed",
|
|
@@ -155,8 +167,53 @@ const GENERAL_FAILURE_CODES = new Set([
|
|
|
155
167
|
"owner_mismatch",
|
|
156
168
|
"mode_mismatch",
|
|
157
169
|
"io_error",
|
|
170
|
+
] as const;
|
|
171
|
+
export type ManagedSessionSecurityCode = (typeof MANAGED_SESSION_SECURITY_CODES)[number];
|
|
172
|
+
const ACL_FAILURE_CODES = new Set<ManagedSessionSecurityCode>([
|
|
173
|
+
"acl_denied",
|
|
174
|
+
"acl_io_error",
|
|
175
|
+
"acl_present",
|
|
176
|
+
"acl_malformed",
|
|
177
|
+
"acl_unknown",
|
|
178
|
+
]);
|
|
179
|
+
const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
|
|
180
|
+
const GENERAL_FAILURE_CODES = new Set<ManagedSessionSecurityCode>(MANAGED_SESSION_SECURITY_CODES);
|
|
181
|
+
const LOCK_OWNERSHIP_LOSS_CODES = new Set<ManagedSessionSecurityCode>([
|
|
182
|
+
"not_found",
|
|
183
|
+
"not_directory",
|
|
184
|
+
"reparse_point",
|
|
185
|
+
"identity_mismatch",
|
|
158
186
|
]);
|
|
159
187
|
|
|
188
|
+
function lockDescriptorOwnershipLost(error: unknown): boolean {
|
|
189
|
+
const code = managedSecurityFailureClassification(error);
|
|
190
|
+
return code !== undefined && LOCK_OWNERSHIP_LOSS_CODES.has(code as ManagedSessionSecurityCode);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export class ManagedSessionSecurityError extends Error {
|
|
194
|
+
readonly code: ManagedSessionSecurityCode;
|
|
195
|
+
|
|
196
|
+
constructor(code: ManagedSessionSecurityCode) {
|
|
197
|
+
super(code);
|
|
198
|
+
this.name = "ManagedSessionSecurityError";
|
|
199
|
+
this.code = code;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function managedSessionSecurityCode(error: unknown): ManagedSessionSecurityCode | undefined {
|
|
204
|
+
if (!(error instanceof Error) || typeof (error as unknown as { code?: unknown }).code !== "string") return undefined;
|
|
205
|
+
const code = (error as unknown as { code: string }).code;
|
|
206
|
+
return GENERAL_FAILURE_CODES.has(code as ManagedSessionSecurityCode)
|
|
207
|
+
? (code as ManagedSessionSecurityCode)
|
|
208
|
+
: undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function managedSessionSecurityErrorForCode(code: string | undefined): ManagedSessionSecurityError {
|
|
212
|
+
return new ManagedSessionSecurityError(
|
|
213
|
+
GENERAL_FAILURE_CODES.has(code as ManagedSessionSecurityCode) ? (code as ManagedSessionSecurityCode) : "io_error",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
160
217
|
function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
161
218
|
return Object.keys(value).every(key => allowed.includes(key));
|
|
162
219
|
}
|
|
@@ -173,10 +230,11 @@ export function validateNativeSecurityResult(
|
|
|
173
230
|
const result = value as Record<string, unknown>;
|
|
174
231
|
if (result.ok === false) {
|
|
175
232
|
if (typeof result.code !== "string") throw new Error("Malformed owner-only security failure");
|
|
176
|
-
|
|
233
|
+
const code = result.code as ManagedSessionSecurityCode;
|
|
234
|
+
if (!ACL_FAILURE_CODES.has(code) && !GENERAL_FAILURE_CODES.has(code)) {
|
|
177
235
|
throw new Error("Unknown owner-only security failure code");
|
|
178
236
|
}
|
|
179
|
-
if (ACL_FAILURE_CODES.has(
|
|
237
|
+
if (ACL_FAILURE_CODES.has(code)) {
|
|
180
238
|
if (
|
|
181
239
|
(result.operation !== "clear" && result.operation !== "query") ||
|
|
182
240
|
(result.attribute !== "access" && result.attribute !== "default")
|
|
@@ -377,12 +435,31 @@ function managedRelativePath(root: ManagedDirectoryRoot, pathname: string): read
|
|
|
377
435
|
|
|
378
436
|
const PROCESS_START_ID = randomUUID();
|
|
379
437
|
|
|
438
|
+
class ManagedSecurityError extends Error {
|
|
439
|
+
readonly #classification: string;
|
|
440
|
+
|
|
441
|
+
constructor(pathname: string, result: NativeSecurity) {
|
|
442
|
+
const classification = result.ok ? "unexpected_security_state" : result.code;
|
|
443
|
+
super(
|
|
444
|
+
result.ok
|
|
445
|
+
? `Unexpected security state for ${pathname}`
|
|
446
|
+
: `Owner-only security rejected ${pathname}: ${classification}`,
|
|
447
|
+
);
|
|
448
|
+
this.name = "ManagedSecurityError";
|
|
449
|
+
this.#classification = classification;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
getClassification(): string {
|
|
453
|
+
return this.#classification;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function managedSecurityFailureClassification(error: unknown): string | undefined {
|
|
458
|
+
return error instanceof ManagedSecurityError ? error.getClassification() : managedSessionSecurityCode(error);
|
|
459
|
+
}
|
|
460
|
+
|
|
380
461
|
function securityError(pathname: string, result: NativeSecurity): Error {
|
|
381
|
-
return new
|
|
382
|
-
result.ok
|
|
383
|
-
? `Unexpected security state for ${pathname}`
|
|
384
|
-
: `Owner-only security rejected ${pathname}: ${result.code}`,
|
|
385
|
-
);
|
|
462
|
+
return new ManagedSecurityError(pathname, result);
|
|
386
463
|
}
|
|
387
464
|
|
|
388
465
|
function secure(pathname: string, kind: "directory" | "file"): void {
|
|
@@ -398,10 +475,11 @@ function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boole
|
|
|
398
475
|
|
|
399
476
|
function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
|
|
400
477
|
const current = fs.lstatSync(pathname, { bigint: true });
|
|
401
|
-
if (current.isSymbolicLink()) throw new
|
|
478
|
+
if (current.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
|
|
402
479
|
const expectedKind = kind === "directory" ? current.isDirectory() : current.isFile();
|
|
403
|
-
if (!expectedKind) throw new
|
|
404
|
-
if (current.dev !== expected.dev || current.ino !== expected.ino)
|
|
480
|
+
if (!expectedKind) throw new ManagedSessionSecurityError("not_directory");
|
|
481
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino)
|
|
482
|
+
throw new ManagedSessionSecurityError("identity_mismatch");
|
|
405
483
|
}
|
|
406
484
|
|
|
407
485
|
function verifyExistingManagedPathSecurity(
|
|
@@ -421,7 +499,7 @@ function verifyExistingManagedPathSecurity(
|
|
|
421
499
|
function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
|
|
422
500
|
const named = fs.lstatSync(pathname, { bigint: true });
|
|
423
501
|
const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
|
|
424
|
-
if (!safeKind || named.isSymbolicLink()) throw new
|
|
502
|
+
if (!safeKind || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
|
|
425
503
|
const verified = validateNativeSecurityResult(
|
|
426
504
|
verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
|
|
427
505
|
"verify",
|
|
@@ -453,7 +531,11 @@ export class ManagedSessionDescendantStore {
|
|
|
453
531
|
readonly #baseDir: string;
|
|
454
532
|
readonly #policy: ManagedSessionSecurityPolicy;
|
|
455
533
|
readonly #authority: RecoveryFsRoot | undefined;
|
|
534
|
+
#ownsAuthority = false;
|
|
535
|
+
#closed = false;
|
|
456
536
|
readonly #authorityBaseDir: string;
|
|
537
|
+
/** Logical profile root inherited by nested managed session destinations. */
|
|
538
|
+
readonly #profileAgentDir: string;
|
|
457
539
|
readonly #subtreeRoot: ManagedDirectoryRoot;
|
|
458
540
|
|
|
459
541
|
constructor(
|
|
@@ -461,12 +543,14 @@ export class ManagedSessionDescendantStore {
|
|
|
461
543
|
baseDir: string,
|
|
462
544
|
retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
|
|
463
545
|
policy?: ManagedSessionSecurityPolicy,
|
|
546
|
+
profileAgentDir?: string,
|
|
464
547
|
) {
|
|
465
548
|
managedRelativePath(root, baseDir);
|
|
466
549
|
|
|
467
550
|
this.#root = root;
|
|
468
551
|
this.#baseDir = path.resolve(baseDir);
|
|
469
552
|
this.#policy = policy ?? "default";
|
|
553
|
+
this.#profileAgentDir = profileAgentDir ?? root.canonicalPath;
|
|
470
554
|
this.#authorityBaseDir = retained?.authorityBaseDir ?? this.#baseDir;
|
|
471
555
|
if (retained) {
|
|
472
556
|
const relative = path.relative(retained.authorityBaseDir, this.#baseDir).split(path.sep).join("/");
|
|
@@ -501,6 +585,7 @@ export class ManagedSessionDescendantStore {
|
|
|
501
585
|
throw new Error("Managed descendant root identity changed");
|
|
502
586
|
}
|
|
503
587
|
this.#authority = authority;
|
|
588
|
+
this.#ownsAuthority = true;
|
|
504
589
|
}
|
|
505
590
|
}
|
|
506
591
|
|
|
@@ -520,10 +605,15 @@ export class ManagedSessionDescendantStore {
|
|
|
520
605
|
return this.#policy;
|
|
521
606
|
}
|
|
522
607
|
|
|
608
|
+
get profileAgentDir(): string {
|
|
609
|
+
return this.#profileAgentDir;
|
|
610
|
+
}
|
|
611
|
+
|
|
523
612
|
deriveSubtree(relativePath: string): ManagedSessionDescendantStore {
|
|
524
613
|
const child = this.ensureDirectory(relativePath);
|
|
525
614
|
const resolved = this.#resolve(relativePath);
|
|
526
|
-
if (!this.#authority)
|
|
615
|
+
if (!this.#authority)
|
|
616
|
+
return new ManagedSessionDescendantStore(this.#root, resolved, undefined, this.#policy, this.#profileAgentDir);
|
|
527
617
|
const retainedChild = this.#authority.retainManagedDirectory(
|
|
528
618
|
this.#relative(resolved),
|
|
529
619
|
child.dev.toString(),
|
|
@@ -537,6 +627,7 @@ export class ManagedSessionDescendantStore {
|
|
|
537
627
|
authorityBaseDir: resolved,
|
|
538
628
|
},
|
|
539
629
|
this.#policy,
|
|
630
|
+
this.#profileAgentDir,
|
|
540
631
|
);
|
|
541
632
|
}
|
|
542
633
|
|
|
@@ -549,6 +640,16 @@ export class ManagedSessionDescendantStore {
|
|
|
549
640
|
);
|
|
550
641
|
}
|
|
551
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Release an authority this store opened itself. Retained authorities are owned by
|
|
645
|
+
* the security context that supplied them and are never closed here. Idempotent.
|
|
646
|
+
*/
|
|
647
|
+
close(): void {
|
|
648
|
+
if (this.#closed || !this.#ownsAuthority || !this.#authority) return;
|
|
649
|
+
this.#closed = true;
|
|
650
|
+
this.#authority.close();
|
|
651
|
+
}
|
|
652
|
+
|
|
552
653
|
assertBound(): void {
|
|
553
654
|
this.#assertBound();
|
|
554
655
|
}
|
|
@@ -558,15 +659,14 @@ export class ManagedSessionDescendantStore {
|
|
|
558
659
|
if (this.#authority) {
|
|
559
660
|
if (this.#authorityBaseDir === this.#baseDir) {
|
|
560
661
|
const verified = this.#authority.verifyOwnerOnlyDirectory();
|
|
561
|
-
if (!verified.ok) throw
|
|
662
|
+
if (!verified.ok) throw managedSessionSecurityErrorForCode(verified.code);
|
|
562
663
|
} else {
|
|
563
664
|
this.#assertBound();
|
|
564
665
|
return;
|
|
565
666
|
}
|
|
566
667
|
} else {
|
|
567
668
|
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
568
|
-
if (!named.isDirectory() || named.isSymbolicLink())
|
|
569
|
-
throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
|
|
669
|
+
if (!named.isDirectory() || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
|
|
570
670
|
if (windowsExistingVerifyFirst(this.#policy))
|
|
571
671
|
verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
|
|
572
672
|
else {
|
|
@@ -653,7 +753,13 @@ export class ManagedSessionDescendantStore {
|
|
|
653
753
|
this.#assertBound();
|
|
654
754
|
const resolved = this.#resolve(relativePath);
|
|
655
755
|
if (!this.#authority) {
|
|
656
|
-
|
|
756
|
+
if (process.platform === "win32") {
|
|
757
|
+
const existing = this.readExpected(relativePath);
|
|
758
|
+
if (existing) replaceManagedFileExactSync(resolved, bytes, existing, this.#subtreeRoot);
|
|
759
|
+
else await publishManagedFileNoReplace(resolved, bytes, undefined, this.#subtreeRoot, this.#policy);
|
|
760
|
+
} else {
|
|
761
|
+
await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
762
|
+
}
|
|
657
763
|
this.#assertBound();
|
|
658
764
|
return;
|
|
659
765
|
}
|
|
@@ -666,7 +772,13 @@ export class ManagedSessionDescendantStore {
|
|
|
666
772
|
this.#assertBound();
|
|
667
773
|
const resolved = this.#resolve(relativePath);
|
|
668
774
|
if (!this.#authority) {
|
|
669
|
-
|
|
775
|
+
if (process.platform === "win32") {
|
|
776
|
+
const existing = this.readExpected(relativePath);
|
|
777
|
+
if (existing) replaceManagedFileExactSync(resolved, bytes, existing, this.#subtreeRoot);
|
|
778
|
+
else publishManagedFileNoReplaceSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
779
|
+
} else {
|
|
780
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
781
|
+
}
|
|
670
782
|
this.#assertBound();
|
|
671
783
|
return;
|
|
672
784
|
}
|
|
@@ -1113,7 +1225,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1113
1225
|
}
|
|
1114
1226
|
|
|
1115
1227
|
function secureFileDescriptor(pathname: string, fd: number, operation: "apply" | "verify"): void {
|
|
1116
|
-
if (process.platform !== "linux") {
|
|
1228
|
+
if (process.platform !== "linux" && process.platform !== "win32") {
|
|
1117
1229
|
if (operation === "apply") secure(pathname, "file");
|
|
1118
1230
|
else {
|
|
1119
1231
|
const verified = validateNativeSecurityResult(verifyOwnerOnlyPathSecurity(pathname, "file"), "verify", "file");
|
|
@@ -1197,11 +1309,9 @@ function isCtimeOnlyIdentityMismatch(
|
|
|
1197
1309
|
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1198
1310
|
}
|
|
1199
1311
|
|
|
1200
|
-
function
|
|
1312
|
+
function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
|
|
1201
1313
|
try {
|
|
1202
|
-
const
|
|
1203
|
-
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
1204
|
-
const value: unknown = JSON.parse(fs.readFileSync(pathname, "utf8"));
|
|
1314
|
+
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
1205
1315
|
if (!value || typeof value !== "object") return undefined;
|
|
1206
1316
|
const record = value as Partial<LockRecord>;
|
|
1207
1317
|
return typeof record.attemptId === "string" &&
|
|
@@ -1217,6 +1327,16 @@ function parseLock(pathname: string): LockRecord | undefined {
|
|
|
1217
1327
|
}
|
|
1218
1328
|
}
|
|
1219
1329
|
|
|
1330
|
+
function parseLock(pathname: string): LockRecord | undefined {
|
|
1331
|
+
try {
|
|
1332
|
+
const stat = fs.lstatSync(pathname);
|
|
1333
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
1334
|
+
return parseLockBytes(fs.readFileSync(pathname));
|
|
1335
|
+
} catch {
|
|
1336
|
+
return undefined;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1220
1340
|
function ownerDefinitelyGone(record: LockRecord): boolean {
|
|
1221
1341
|
if (record.bootId && bootId() && record.bootId !== bootId()) return true;
|
|
1222
1342
|
try {
|
|
@@ -1237,6 +1357,82 @@ function writeLockDescriptor(fd: number, record: LockRecord): void {
|
|
|
1237
1357
|
function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
|
|
1238
1358
|
return left.dev === right.dev && left.ino === right.ino;
|
|
1239
1359
|
}
|
|
1360
|
+
function exactLockRetirementAccepted(lockPath: string, removed: NativeExactUnlinkResult): boolean {
|
|
1361
|
+
if (removed.ok) return true;
|
|
1362
|
+
if (removed.code !== "cleanup_pending") return false;
|
|
1363
|
+
const lockDirectory = path.dirname(path.resolve(lockPath));
|
|
1364
|
+
const authority = [
|
|
1365
|
+
removed.detachedPath,
|
|
1366
|
+
removed.retainedSuccessorPath,
|
|
1367
|
+
removed.retainedPlaceholderPath,
|
|
1368
|
+
removed.retainedUnknownPath,
|
|
1369
|
+
].find(pathname => {
|
|
1370
|
+
if (typeof pathname !== "string" || pathname.length === 0 || !path.isAbsolute(pathname)) return false;
|
|
1371
|
+
const resolved = path.resolve(pathname);
|
|
1372
|
+
if (resolved === path.resolve(lockPath) || path.dirname(resolved) !== lockDirectory) return false;
|
|
1373
|
+
if (!path.basename(resolved).startsWith(".gjc-")) return false;
|
|
1374
|
+
try {
|
|
1375
|
+
return !fs.lstatSync(resolved).isSymbolicLink();
|
|
1376
|
+
} catch {
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
try {
|
|
1381
|
+
fs.lstatSync(lockPath);
|
|
1382
|
+
return false;
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
return !!authority && (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function unlinkManagedLockSnapshotExact(lockPath: string, snapshot: ManagedFileSnapshot): void {
|
|
1389
|
+
const removed = exactUnlink(lockPath, {
|
|
1390
|
+
dev: snapshot.identity.dev,
|
|
1391
|
+
ino: snapshot.identity.ino,
|
|
1392
|
+
size: BigInt(snapshot.identity.size),
|
|
1393
|
+
mtimeNs: snapshot.identity.mtimeNs,
|
|
1394
|
+
sha256: snapshot.identity.sha256,
|
|
1395
|
+
quarantineName: `.gjc-lock-retire-${process.pid}-${randomUUID()}`,
|
|
1396
|
+
});
|
|
1397
|
+
if (!exactLockRetirementAccepted(lockPath, removed)) throw new Error("migration_busy");
|
|
1398
|
+
fsyncDirectory(path.dirname(lockPath));
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function retireCreatedManagedLockExact(lockPath: string, fd: number): void {
|
|
1402
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
1403
|
+
const snapshot = captureManagedFileNoFollow(lockPath);
|
|
1404
|
+
if (snapshot.identity.dev !== opened.dev || snapshot.identity.ino !== opened.ino) throw new Error("migration_busy");
|
|
1405
|
+
unlinkManagedLockSnapshotExact(lockPath, snapshot);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Retire a lock only after exact identity and attempt fencing. A typed
|
|
1410
|
+
* `cleanup_pending` result is terminal lock-authority retirement, not a claim
|
|
1411
|
+
* that retained bytes were deleted: accept it only when native exact-unlink
|
|
1412
|
+
* supplies concrete internal authority in this lock directory and the canonical
|
|
1413
|
+
* lock name is absent, so a successor may acquire while quarantine evidence stays
|
|
1414
|
+
* retained for recovery.
|
|
1415
|
+
*/
|
|
1416
|
+
function retireManagedLockExact(
|
|
1417
|
+
lockPath: string,
|
|
1418
|
+
expected: { lockIdentity?: fs.BigIntStats; attemptId: string },
|
|
1419
|
+
): void {
|
|
1420
|
+
let snapshot: ManagedFileSnapshot;
|
|
1421
|
+
try {
|
|
1422
|
+
snapshot = captureManagedFileNoFollow(lockPath);
|
|
1423
|
+
} catch {
|
|
1424
|
+
throw new Error("migration_busy");
|
|
1425
|
+
}
|
|
1426
|
+
const record = parseLockBytes(snapshot.bytes);
|
|
1427
|
+
if (
|
|
1428
|
+
!record ||
|
|
1429
|
+
record.attemptId !== expected.attemptId ||
|
|
1430
|
+
(expected.lockIdentity &&
|
|
1431
|
+
(snapshot.identity.dev !== expected.lockIdentity.dev || snapshot.identity.ino !== expected.lockIdentity.ino))
|
|
1432
|
+
)
|
|
1433
|
+
throw new Error("migration_busy");
|
|
1434
|
+
unlinkManagedLockSnapshotExact(lockPath, snapshot);
|
|
1435
|
+
}
|
|
1240
1436
|
|
|
1241
1437
|
/** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
|
|
1242
1438
|
export function ensureManagedDirectory(
|
|
@@ -1444,8 +1640,126 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1444
1640
|
if (failure !== undefined) throw failure;
|
|
1445
1641
|
}
|
|
1446
1642
|
|
|
1643
|
+
/**
|
|
1644
|
+
* Atomically replace a managed file only when the destination is still the
|
|
1645
|
+
* exact captured object. Unlike `replaceManagedFileSync`, this never performs
|
|
1646
|
+
* an unconditional pathname rename.
|
|
1647
|
+
*/
|
|
1648
|
+
export function replaceManagedFileExactSync(
|
|
1649
|
+
destination: string,
|
|
1650
|
+
bytes: Uint8Array,
|
|
1651
|
+
expected: Pick<ManagedFileSnapshot, "identity">,
|
|
1652
|
+
root: ManagedDirectoryRoot,
|
|
1653
|
+
): void {
|
|
1654
|
+
assertManagedDirectoryRoot(root);
|
|
1655
|
+
managedRelativePath(root, destination);
|
|
1656
|
+
if (process.platform === "win32") {
|
|
1657
|
+
const parent = path.dirname(destination);
|
|
1658
|
+
const lock = acquireManagedLockSync(
|
|
1659
|
+
parent,
|
|
1660
|
+
`.${path.basename(destination)}.replace`,
|
|
1661
|
+
root,
|
|
1662
|
+
"windows-existing-verify-first",
|
|
1663
|
+
);
|
|
1664
|
+
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
|
|
1665
|
+
let fd: number | undefined;
|
|
1666
|
+
let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1667
|
+
let publicationUncertain = false;
|
|
1668
|
+
try {
|
|
1669
|
+
lock.assertOwned();
|
|
1670
|
+
const current = captureManagedFileNoFollow(destination);
|
|
1671
|
+
if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
|
|
1672
|
+
throw new Error("managed_replace_identity_mismatch");
|
|
1673
|
+
fd = fs.openSync(
|
|
1674
|
+
staging,
|
|
1675
|
+
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
|
|
1676
|
+
0o600,
|
|
1677
|
+
);
|
|
1678
|
+
secureFileDescriptor(staging, fd, "apply");
|
|
1679
|
+
let offset = 0;
|
|
1680
|
+
while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
1681
|
+
fs.fsyncSync(fd);
|
|
1682
|
+
secureFileDescriptor(staging, fd, "verify");
|
|
1683
|
+
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1684
|
+
stagedIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1685
|
+
fs.closeSync(fd);
|
|
1686
|
+
fd = undefined;
|
|
1687
|
+
lock.assertOwned();
|
|
1688
|
+
const outcome = exactReplacePath(staging, destination, {
|
|
1689
|
+
dev: expected.identity.dev,
|
|
1690
|
+
ino: expected.identity.ino,
|
|
1691
|
+
size: BigInt(expected.identity.size),
|
|
1692
|
+
mtimeNs: expected.identity.mtimeNs,
|
|
1693
|
+
sha256: expected.identity.sha256,
|
|
1694
|
+
});
|
|
1695
|
+
if (!outcome.ok) {
|
|
1696
|
+
publicationUncertain = true;
|
|
1697
|
+
throw new Error(
|
|
1698
|
+
outcome.detachedPath === staging
|
|
1699
|
+
? "managed_replace_publication_pending"
|
|
1700
|
+
: (outcome.code ?? "managed_replace_identity_mismatch"),
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
lock.assertOwned();
|
|
1704
|
+
const published = fs.lstatSync(destination, { bigint: true });
|
|
1705
|
+
if (
|
|
1706
|
+
!published.isFile() ||
|
|
1707
|
+
published.isSymbolicLink() ||
|
|
1708
|
+
published.dev !== staged.dev ||
|
|
1709
|
+
published.ino !== staged.ino
|
|
1710
|
+
) {
|
|
1711
|
+
publicationUncertain = true;
|
|
1712
|
+
throw new Error("destination_identity_changed");
|
|
1713
|
+
}
|
|
1714
|
+
secureExistingManagedDirectory(destination, "file");
|
|
1715
|
+
} finally {
|
|
1716
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
1717
|
+
if (stagedIdentity && !publicationUncertain) {
|
|
1718
|
+
try {
|
|
1719
|
+
const named = fs.lstatSync(staging, { bigint: true });
|
|
1720
|
+
if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino) fs.unlinkSync(staging);
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") publicationUncertain = true;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
lock.release();
|
|
1726
|
+
}
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const parent = path.dirname(destination);
|
|
1730
|
+
const parentStat = fs.lstatSync(parent, { bigint: true });
|
|
1731
|
+
if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) throw new Error("managed_replace_parent_unavailable");
|
|
1732
|
+
const authority = openRecoveryFsRoot(parent);
|
|
1733
|
+
try {
|
|
1734
|
+
const retained = authority.identity();
|
|
1735
|
+
if (
|
|
1736
|
+
!retained.ok ||
|
|
1737
|
+
!retained.identity ||
|
|
1738
|
+
retained.identity.dev !== parentStat.dev.toString() ||
|
|
1739
|
+
retained.identity.ino !== parentStat.ino.toString()
|
|
1740
|
+
)
|
|
1741
|
+
throw new Error("managed_replace_parent_identity_changed");
|
|
1742
|
+
const verified = authority.verifyOwnerOnlyDirectory();
|
|
1743
|
+
if (!verified.ok) throw new Error(verified.code ?? "managed_replace_parent_unavailable");
|
|
1744
|
+
const replaced = authority.replaceManaged(
|
|
1745
|
+
path.basename(destination),
|
|
1746
|
+
bytes,
|
|
1747
|
+
expected.identity.dev.toString(),
|
|
1748
|
+
expected.identity.ino.toString(),
|
|
1749
|
+
expected.identity.size.toString(),
|
|
1750
|
+
expected.identity.mtimeNs.toString(),
|
|
1751
|
+
expected.identity.ctimeNs.toString(),
|
|
1752
|
+
expected.identity.sha256,
|
|
1753
|
+
);
|
|
1754
|
+
if (!replaced.ok) throw new Error(replaced.code ?? "managed_replace_identity_mismatch");
|
|
1755
|
+
const synced = authority.fsync();
|
|
1756
|
+
if (!synced.ok) throw new Error(synced.code ?? "managed_replace_fsync_failed");
|
|
1757
|
+
} finally {
|
|
1758
|
+
authority.close();
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1447
1761
|
/** Replace one managed regular file while retaining the secured staging fd through publication. */
|
|
1448
|
-
|
|
1762
|
+
function replaceManagedFileSync(
|
|
1449
1763
|
destination: string,
|
|
1450
1764
|
bytes: Uint8Array,
|
|
1451
1765
|
root: ManagedDirectoryRoot,
|
|
@@ -1493,7 +1807,7 @@ export function replaceManagedFileSync(
|
|
|
1493
1807
|
}
|
|
1494
1808
|
}
|
|
1495
1809
|
|
|
1496
|
-
|
|
1810
|
+
async function replaceManagedFile(
|
|
1497
1811
|
destination: string,
|
|
1498
1812
|
bytes: Uint8Array,
|
|
1499
1813
|
root: ManagedDirectoryRoot,
|
|
@@ -1528,7 +1842,6 @@ export async function acquireManagedLock(
|
|
|
1528
1842
|
ensureManagedDirectory(locksDirectory, root, policy);
|
|
1529
1843
|
const lockPath = path.join(locksDirectory, `${name}.lock`);
|
|
1530
1844
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1531
|
-
let staleObservedAt: number | undefined;
|
|
1532
1845
|
while (true) {
|
|
1533
1846
|
const attemptId = randomUUID();
|
|
1534
1847
|
const now = Date.now();
|
|
@@ -1553,7 +1866,11 @@ export async function acquireManagedLock(
|
|
|
1553
1866
|
fs.fsyncSync(fd);
|
|
1554
1867
|
fsyncDirectory(locksDirectory);
|
|
1555
1868
|
} catch (error) {
|
|
1556
|
-
|
|
1869
|
+
try {
|
|
1870
|
+
retireCreatedManagedLockExact(lockPath, fd);
|
|
1871
|
+
} finally {
|
|
1872
|
+
fs.closeSync(fd);
|
|
1873
|
+
}
|
|
1557
1874
|
throw error;
|
|
1558
1875
|
}
|
|
1559
1876
|
const lockIdentity = fs.fstatSync(fd, { bigint: true });
|
|
@@ -1561,12 +1878,8 @@ export async function acquireManagedLock(
|
|
|
1561
1878
|
let descriptorClosed = false;
|
|
1562
1879
|
const closeDescriptor = (): void => {
|
|
1563
1880
|
if (descriptorClosed) return;
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
} finally {
|
|
1567
|
-
fs.closeSync(fd);
|
|
1568
|
-
descriptorClosed = true;
|
|
1569
|
-
}
|
|
1881
|
+
fs.closeSync(fd);
|
|
1882
|
+
descriptorClosed = true;
|
|
1570
1883
|
};
|
|
1571
1884
|
const assertOwned = (): void => {
|
|
1572
1885
|
const current = parseLock(lockPath);
|
|
@@ -1581,8 +1894,7 @@ export async function acquireManagedLock(
|
|
|
1581
1894
|
descriptorClosed ||
|
|
1582
1895
|
!current ||
|
|
1583
1896
|
!sameFileIdentity(lockIdentity, named) ||
|
|
1584
|
-
current.attemptId !== attemptId
|
|
1585
|
-
current.leaseExpiresAt < Date.now()
|
|
1897
|
+
current.attemptId !== attemptId
|
|
1586
1898
|
)
|
|
1587
1899
|
throw new Error("migration_busy");
|
|
1588
1900
|
};
|
|
@@ -1600,42 +1912,160 @@ export async function acquireManagedLock(
|
|
|
1600
1912
|
attemptId,
|
|
1601
1913
|
assertOwned,
|
|
1602
1914
|
async release(): Promise<void> {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
assertOwned();
|
|
1606
|
-
const now = Date.now();
|
|
1607
|
-
// Do not unlink by pathname: a stale owner could otherwise remove a successor.
|
|
1608
|
-
// The lease is retired through the verified inode-bound descriptor instead.
|
|
1609
|
-
writeLockDescriptor(fd, { ...record, heartbeatAt: now, leaseExpiresAt: now });
|
|
1610
|
-
fsyncDirectory(locksDirectory);
|
|
1611
|
-
} finally {
|
|
1915
|
+
const finalize = (): void => {
|
|
1916
|
+
clearInterval(heartbeat);
|
|
1612
1917
|
released = true;
|
|
1613
1918
|
closeDescriptor();
|
|
1919
|
+
};
|
|
1920
|
+
try {
|
|
1921
|
+
assertOwned();
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
finalize();
|
|
1924
|
+
throw error;
|
|
1925
|
+
}
|
|
1926
|
+
try {
|
|
1927
|
+
secureFileDescriptor(lockPath, fd, "verify");
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
if (lockDescriptorOwnershipLost(error)) finalize();
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1932
|
+
try {
|
|
1933
|
+
retireManagedLockExact(lockPath, { lockIdentity, attemptId });
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
try {
|
|
1936
|
+
assertOwned();
|
|
1937
|
+
} catch {
|
|
1938
|
+
finalize();
|
|
1939
|
+
}
|
|
1940
|
+
throw error;
|
|
1614
1941
|
}
|
|
1942
|
+
finalize();
|
|
1615
1943
|
},
|
|
1616
1944
|
};
|
|
1617
1945
|
} catch (error) {
|
|
1618
1946
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
1619
1947
|
const owner = parseLock(lockPath);
|
|
1620
|
-
if (owner && owner.leaseExpiresAt < Date.now()) {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1948
|
+
if (owner && owner.leaseExpiresAt < Date.now() && ownerDefinitelyGone(owner)) {
|
|
1949
|
+
try {
|
|
1950
|
+
retireManagedLockExact(lockPath, { attemptId: owner.attemptId });
|
|
1951
|
+
} catch {
|
|
1952
|
+
/* retry owner observation */
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if (Date.now() >= deadline) throw new Error("migration_busy");
|
|
1956
|
+
await new Promise<void>(resolve => setTimeout(resolve, 50));
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
/** Acquire a synchronous lease lock for a short cross-process critical section. */
|
|
1961
|
+
export function acquireManagedLockSync(
|
|
1962
|
+
locksDirectory: string,
|
|
1963
|
+
name: string,
|
|
1964
|
+
root?: ManagedDirectoryRoot,
|
|
1965
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
1966
|
+
): ManagedStorageLockSync {
|
|
1967
|
+
ensureManagedDirectory(locksDirectory, root, policy);
|
|
1968
|
+
const lockPath = path.join(locksDirectory, `${name}.lock`);
|
|
1969
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1970
|
+
while (true) {
|
|
1971
|
+
const attemptId = randomUUID();
|
|
1972
|
+
const now = Date.now();
|
|
1973
|
+
const record: LockRecord = {
|
|
1974
|
+
attemptId,
|
|
1975
|
+
pid: process.pid,
|
|
1976
|
+
bootId: bootId(),
|
|
1977
|
+
processStartId: PROCESS_START_ID,
|
|
1978
|
+
createdAt: now,
|
|
1979
|
+
heartbeatAt: now,
|
|
1980
|
+
leaseExpiresAt: now + LOCK_LEASE_MS,
|
|
1981
|
+
};
|
|
1982
|
+
try {
|
|
1983
|
+
const fd = fs.openSync(
|
|
1984
|
+
lockPath,
|
|
1985
|
+
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
|
|
1986
|
+
0o600,
|
|
1987
|
+
);
|
|
1988
|
+
try {
|
|
1989
|
+
secureFileDescriptor(lockPath, fd, "apply");
|
|
1990
|
+
writeLockDescriptor(fd, record);
|
|
1991
|
+
fsyncDirectory(locksDirectory);
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
try {
|
|
1994
|
+
retireCreatedManagedLockExact(lockPath, fd);
|
|
1995
|
+
} finally {
|
|
1996
|
+
fs.closeSync(fd);
|
|
1997
|
+
}
|
|
1998
|
+
throw error;
|
|
1999
|
+
}
|
|
2000
|
+
const lockIdentity = fs.fstatSync(fd, { bigint: true });
|
|
2001
|
+
let released = false;
|
|
2002
|
+
let descriptorClosed = false;
|
|
2003
|
+
const assertOwned = (): void => {
|
|
2004
|
+
const current = parseLock(lockPath);
|
|
2005
|
+
let named: fs.BigIntStats;
|
|
2006
|
+
try {
|
|
2007
|
+
named = fs.lstatSync(lockPath, { bigint: true });
|
|
2008
|
+
} catch {
|
|
2009
|
+
throw new Error("migration_busy");
|
|
2010
|
+
}
|
|
2011
|
+
if (
|
|
2012
|
+
released ||
|
|
2013
|
+
descriptorClosed ||
|
|
2014
|
+
!current ||
|
|
2015
|
+
!sameFileIdentity(lockIdentity, named) ||
|
|
2016
|
+
current.attemptId !== attemptId
|
|
2017
|
+
)
|
|
2018
|
+
throw new Error("migration_busy");
|
|
2019
|
+
};
|
|
2020
|
+
return {
|
|
2021
|
+
path: lockPath,
|
|
2022
|
+
attemptId,
|
|
2023
|
+
assertOwned,
|
|
2024
|
+
release(): void {
|
|
2025
|
+
const finalize = (): void => {
|
|
2026
|
+
released = true;
|
|
2027
|
+
if (!descriptorClosed) {
|
|
2028
|
+
fs.closeSync(fd);
|
|
2029
|
+
descriptorClosed = true;
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
1625
2032
|
try {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
fsyncDirectory(locksDirectory);
|
|
1631
|
-
} catch {
|
|
1632
|
-
/* retry owner observation */
|
|
2033
|
+
assertOwned();
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
finalize();
|
|
2036
|
+
throw error;
|
|
1633
2037
|
}
|
|
1634
|
-
|
|
2038
|
+
try {
|
|
2039
|
+
secureFileDescriptor(lockPath, fd, "verify");
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
if (lockDescriptorOwnershipLost(error)) finalize();
|
|
2042
|
+
throw error;
|
|
2043
|
+
}
|
|
2044
|
+
try {
|
|
2045
|
+
retireManagedLockExact(lockPath, { lockIdentity, attemptId });
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
try {
|
|
2048
|
+
assertOwned();
|
|
2049
|
+
} catch {
|
|
2050
|
+
finalize();
|
|
2051
|
+
}
|
|
2052
|
+
throw error;
|
|
2053
|
+
}
|
|
2054
|
+
finalize();
|
|
2055
|
+
},
|
|
2056
|
+
};
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
2059
|
+
const owner = parseLock(lockPath);
|
|
2060
|
+
if (owner && owner.leaseExpiresAt < Date.now() && ownerDefinitelyGone(owner)) {
|
|
2061
|
+
try {
|
|
2062
|
+
retireManagedLockExact(lockPath, { attemptId: owner.attemptId });
|
|
2063
|
+
} catch {
|
|
2064
|
+
/* Retry owner observation. */
|
|
1635
2065
|
}
|
|
1636
|
-
}
|
|
2066
|
+
}
|
|
1637
2067
|
if (Date.now() >= deadline) throw new Error("migration_busy");
|
|
1638
|
-
|
|
2068
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(50, deadline - Date.now()));
|
|
1639
2069
|
}
|
|
1640
2070
|
}
|
|
1641
2071
|
}
|