@gajae-code/coding-agent 0.11.7 → 0.11.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -1
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/keybindings.d.ts +33 -3
- 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/model-profiles.d.ts +2 -3
- package/dist/types/config/settings-schema.d.ts +15 -2
- 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/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +4 -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 +5 -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 +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- 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/runtime-mcp/redaction.d.ts +2 -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 +7 -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 +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -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/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- 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/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -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 +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +50 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commands/sdk.ts +2 -2
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +2 -3
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -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/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- 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 +78 -4
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/state-writer.ts +166 -2
- 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 +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/internal-urls/docs-index.generated.ts +8 -6
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- 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 +20 -4
- package/src/modes/components/welcome.ts +32 -20
- 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 +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +70 -36
- 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 +3 -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/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +7 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/json-rpc.ts +7 -4
- 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/redaction.ts +30 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +20 -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 +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- 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 +27 -2
- 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 +15 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +853 -112
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- 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 +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +138 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +998 -133
- package/src/session/internal/managed-session-storage.ts +268 -203
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +406 -360
- package/src/session/session-storage.ts +151 -11
- package/src/skill-state/active-state.ts +10 -0
- package/src/task/executor.ts +13 -5
- 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 -88
- 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/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -18,16 +18,99 @@ import {
|
|
|
18
18
|
verifyOwnerOnlyPathSecurity,
|
|
19
19
|
verifyOwnerOnlyPathSecurityExpected,
|
|
20
20
|
} from "@gajae-code/natives";
|
|
21
|
+
import {
|
|
22
|
+
classifyNativePublishOutcome,
|
|
23
|
+
formatNativePublishDiagnostic,
|
|
24
|
+
mayCleanCurrentStaging,
|
|
25
|
+
type NativePublishOutcome,
|
|
26
|
+
} from "./native-publish-outcome";
|
|
21
27
|
|
|
22
28
|
export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
23
|
-
export const MANAGED_ARTIFACT_MAX_FILES =
|
|
29
|
+
export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
|
|
24
30
|
export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
25
31
|
export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
32
|
+
export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
|
|
26
33
|
const LOCK_LEASE_MS = 60_000;
|
|
27
34
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
28
35
|
const LOCK_WAIT_MS = 5_000;
|
|
29
36
|
|
|
30
37
|
const LOCK_STALE_RECHECK_MS = 100;
|
|
38
|
+
export class ManagedPublishError extends Error {
|
|
39
|
+
readonly classification:
|
|
40
|
+
| "destination_conflict"
|
|
41
|
+
| "atomic_unavailable"
|
|
42
|
+
| "invalid_request"
|
|
43
|
+
| "durability_not_provable"
|
|
44
|
+
| "identity_mismatch"
|
|
45
|
+
| "io_error"
|
|
46
|
+
| "durability_failed";
|
|
47
|
+
readonly diagnostic: string;
|
|
48
|
+
|
|
49
|
+
constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
|
|
50
|
+
super(classification);
|
|
51
|
+
this.name = "ManagedPublishError";
|
|
52
|
+
this.classification = classification;
|
|
53
|
+
this.diagnostic = formatNativePublishDiagnostic(outcome);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
|
|
58
|
+
const classification =
|
|
59
|
+
outcome.reason === "destination_exists"
|
|
60
|
+
? "destination_conflict"
|
|
61
|
+
: outcome.reason === "atomic_unavailable"
|
|
62
|
+
? "atomic_unavailable"
|
|
63
|
+
: outcome.reason === "invalid_request"
|
|
64
|
+
? "invalid_request"
|
|
65
|
+
: outcome.reason === "durability_not_provable"
|
|
66
|
+
? "durability_not_provable"
|
|
67
|
+
: outcome.reason === "identity_violation"
|
|
68
|
+
? "identity_mismatch"
|
|
69
|
+
: outcome.reason === "io_failure"
|
|
70
|
+
? "io_error"
|
|
71
|
+
: "durability_failed";
|
|
72
|
+
return new ManagedPublishError(classification, outcome);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
|
|
76
|
+
function sameDirectoryTreeSnapshotAfterRename(
|
|
77
|
+
left: NativeDirectoryTreeSnapshot,
|
|
78
|
+
right: NativeDirectoryTreeSnapshot,
|
|
79
|
+
): boolean {
|
|
80
|
+
return (
|
|
81
|
+
left.rootDev === right.rootDev &&
|
|
82
|
+
left.rootIno === right.rootIno &&
|
|
83
|
+
left.entries.length === right.entries.length &&
|
|
84
|
+
left.entries.every((entry, index) => {
|
|
85
|
+
const other = right.entries[index];
|
|
86
|
+
return (
|
|
87
|
+
other !== undefined &&
|
|
88
|
+
entry.relativePath === other.relativePath &&
|
|
89
|
+
entry.kind === other.kind &&
|
|
90
|
+
entry.dev === other.dev &&
|
|
91
|
+
entry.ino === other.ino &&
|
|
92
|
+
entry.size === other.size &&
|
|
93
|
+
entry.mtimeNs === other.mtimeNs &&
|
|
94
|
+
(entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
|
|
95
|
+
entry.sha256 === other.sha256
|
|
96
|
+
);
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ManagedTreeMoveOutcomeError extends Error {
|
|
102
|
+
constructor(
|
|
103
|
+
message: string,
|
|
104
|
+
readonly stagingCleanupSafe: boolean,
|
|
105
|
+
) {
|
|
106
|
+
super(message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Cleanup is safe only when the native move reports that it did not commit. */
|
|
111
|
+
export function mayCleanManagedTreeStaging(error: unknown): boolean {
|
|
112
|
+
return !(error instanceof ManagedTreeMoveOutcomeError) || error.stagingCleanupSafe;
|
|
113
|
+
}
|
|
31
114
|
|
|
32
115
|
export type ManagedSessionSecurityPolicy = "default" | "windows-existing-verify-first";
|
|
33
116
|
|
|
@@ -38,7 +121,10 @@ export type ManagedStorageFailure =
|
|
|
38
121
|
| "destination_conflict"
|
|
39
122
|
| "source_changed"
|
|
40
123
|
| "unsafe_artifacts"
|
|
124
|
+
| "artifact_capacity_exceeded"
|
|
41
125
|
| "durability_failed"
|
|
126
|
+
| "atomic_unavailable"
|
|
127
|
+
| "durability_not_provable"
|
|
42
128
|
| "migration_retired"
|
|
43
129
|
| "managed_storage_unsupported";
|
|
44
130
|
|
|
@@ -97,6 +183,9 @@ export function validateNativeSecurityResult(
|
|
|
97
183
|
) {
|
|
98
184
|
throw new Error("Malformed ACL security failure evidence");
|
|
99
185
|
}
|
|
186
|
+
if (operation === "verify" && result.operation !== "query") {
|
|
187
|
+
throw new Error("Verify failure unexpectedly reports ACL mutation");
|
|
188
|
+
}
|
|
100
189
|
} else if (result.operation !== undefined || result.attribute !== undefined) {
|
|
101
190
|
throw new Error("Unexpected ACL fields on owner-only security failure");
|
|
102
191
|
}
|
|
@@ -183,6 +272,40 @@ export interface ManagedDirectoryRoot {
|
|
|
183
272
|
readonly ino: bigint;
|
|
184
273
|
}
|
|
185
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Establish the first managed directory beneath a potentially shared ancestor.
|
|
277
|
+
* The ancestor is used only to create the managed chain; every component from
|
|
278
|
+
* `configuredRoot` downward is independently type, symlink, owner, mode, and
|
|
279
|
+
* ACL checked. This deliberately does not impose owner-only policy on `/tmp`.
|
|
280
|
+
*/
|
|
281
|
+
export function prepareManagedDirectoryRoot(
|
|
282
|
+
configuredRoot: string,
|
|
283
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
284
|
+
): ManagedDirectoryRoot {
|
|
285
|
+
const target = path.resolve(configuredRoot);
|
|
286
|
+
const missing: string[] = [];
|
|
287
|
+
let existing = target;
|
|
288
|
+
for (;;) {
|
|
289
|
+
try {
|
|
290
|
+
assertSafeDirectory(existing);
|
|
291
|
+
break;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
294
|
+
const parent = path.dirname(existing);
|
|
295
|
+
if (parent === existing) throw new Error(`Managed root is unavailable: ${configuredRoot}`);
|
|
296
|
+
missing.unshift(path.basename(existing));
|
|
297
|
+
existing = parent;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
let current = fs.realpathSync.native(existing);
|
|
301
|
+
for (const component of missing) {
|
|
302
|
+
current = path.join(current, component);
|
|
303
|
+
const created = ensureDirectoryComponent(current);
|
|
304
|
+
secureManagedDirectory(current, created, policy);
|
|
305
|
+
}
|
|
306
|
+
if (missing.length === 0) secureManagedDirectory(current, false, policy);
|
|
307
|
+
return managedDirectoryRoot(current);
|
|
308
|
+
}
|
|
186
309
|
export function managedDirectoryRoot(configuredRoot: string): ManagedDirectoryRoot {
|
|
187
310
|
const canonicalPath = fs.realpathSync.native(configuredRoot);
|
|
188
311
|
const stat = fs.lstatSync(canonicalPath, { bigint: true });
|
|
@@ -339,8 +462,8 @@ export class ManagedSessionDescendantStore {
|
|
|
339
462
|
retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
|
|
340
463
|
policy?: ManagedSessionSecurityPolicy,
|
|
341
464
|
) {
|
|
342
|
-
assertManagedDirectoryRoot(root);
|
|
343
465
|
managedRelativePath(root, baseDir);
|
|
466
|
+
|
|
344
467
|
this.#root = root;
|
|
345
468
|
this.#baseDir = path.resolve(baseDir);
|
|
346
469
|
this.#policy = policy ?? "default";
|
|
@@ -350,20 +473,17 @@ export class ManagedSessionDescendantStore {
|
|
|
350
473
|
const captured = retained.authority.snapshotManagedTree(relative);
|
|
351
474
|
if (!captured.ok || !captured.snapshot)
|
|
352
475
|
throw new Error(captured.code ?? "Managed subtree identity unavailable");
|
|
353
|
-
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
354
|
-
if (
|
|
355
|
-
captured.snapshot.rootDev !== subtreeStat.dev.toString() ||
|
|
356
|
-
captured.snapshot.rootIno !== subtreeStat.ino.toString()
|
|
357
|
-
)
|
|
358
|
-
throw new Error("Managed descendant root identity changed");
|
|
359
476
|
this.#subtreeRoot = Object.freeze({
|
|
360
477
|
canonicalPath: this.#baseDir,
|
|
361
|
-
dev:
|
|
362
|
-
ino:
|
|
478
|
+
dev: BigInt(captured.snapshot.rootDev),
|
|
479
|
+
ino: BigInt(captured.snapshot.rootIno),
|
|
363
480
|
});
|
|
364
481
|
this.#authority = retained.authority;
|
|
482
|
+
this.#assertBound();
|
|
483
|
+
|
|
365
484
|
return;
|
|
366
485
|
}
|
|
486
|
+
assertManagedDirectoryRoot(root);
|
|
367
487
|
ensureManagedDirectory(this.#baseDir, root, this.#policy);
|
|
368
488
|
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
369
489
|
this.#subtreeRoot = Object.freeze({ canonicalPath: this.#baseDir, dev: subtreeStat.dev, ino: subtreeStat.ino });
|
|
@@ -557,7 +677,7 @@ export class ManagedSessionDescendantStore {
|
|
|
557
677
|
appendSync(relativePath: string, bytes: Uint8Array): void {
|
|
558
678
|
this.#assertBound();
|
|
559
679
|
const resolved = this.#resolve(relativePath);
|
|
560
|
-
|
|
680
|
+
let existing = this.readExpected(relativePath);
|
|
561
681
|
if (!existing) throw new Error("managed_append_missing");
|
|
562
682
|
if (this.#authority) {
|
|
563
683
|
const appended = this.#authority.appendManaged(
|
|
@@ -574,20 +694,49 @@ export class ManagedSessionDescendantStore {
|
|
|
574
694
|
this.#assertBound();
|
|
575
695
|
return;
|
|
576
696
|
}
|
|
697
|
+
// On Darwin, the first write-append open can change only ctime (e.g. com.apple.provenance)
|
|
698
|
+
// while leaving dev/ino/size/mtime/content intact. Allow one bounded refresh+retry for that
|
|
699
|
+
// transition only; any other identity change or a second ctime transition stays fail-closed.
|
|
700
|
+
// See https://github.com/Yeachan-Heo/gajae-code/issues/2944
|
|
577
701
|
let fd: number | undefined;
|
|
702
|
+
let darwinCtimeRefreshConsumed = false;
|
|
578
703
|
try {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
704
|
+
for (;;) {
|
|
705
|
+
fd = fs.openSync(resolved, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW);
|
|
706
|
+
const before = identity(fs.fstatSync(fd, { bigint: true }));
|
|
707
|
+
if (!sameIdentity(before, existing.identity)) {
|
|
708
|
+
const allowDarwinCtimeRefresh =
|
|
709
|
+
process.platform === "darwin" &&
|
|
710
|
+
!darwinCtimeRefreshConsumed &&
|
|
711
|
+
isCtimeOnlyIdentityMismatch(before, existing.identity);
|
|
712
|
+
if (!allowDarwinCtimeRefresh) throw new Error("identity_mismatch");
|
|
713
|
+
fs.closeSync(fd);
|
|
714
|
+
fd = undefined;
|
|
715
|
+
darwinCtimeRefreshConsumed = true;
|
|
716
|
+
const original = existing;
|
|
717
|
+
const refreshed = this.readExpected(relativePath);
|
|
718
|
+
if (
|
|
719
|
+
!refreshed ||
|
|
720
|
+
!sameStableIdentityIgnoringCtime(refreshed.identity, original.identity) ||
|
|
721
|
+
refreshed.identity.sha256 !== original.identity.sha256 ||
|
|
722
|
+
!refreshed.bytes.equals(original.bytes)
|
|
723
|
+
) {
|
|
724
|
+
throw new Error("identity_mismatch");
|
|
725
|
+
}
|
|
726
|
+
existing = refreshed;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
730
|
+
let offset = 0;
|
|
731
|
+
while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
732
|
+
fs.fsyncSync(fd);
|
|
733
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
734
|
+
const after = identity(fs.fstatSync(fd, { bigint: true }));
|
|
735
|
+
const named = identity(fs.lstatSync(resolved, { bigint: true }));
|
|
736
|
+
if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
|
|
737
|
+
throw new Error("identity_mismatch");
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
591
740
|
} finally {
|
|
592
741
|
if (fd !== undefined) fs.closeSync(fd);
|
|
593
742
|
}
|
|
@@ -608,6 +757,7 @@ export class ManagedSessionDescendantStore {
|
|
|
608
757
|
if (!created.ok) throw new Error(created.code ?? "managed_publish_failed");
|
|
609
758
|
if (!created.identity) throw new Error("managed_publish_identity_unavailable");
|
|
610
759
|
let captured: ReturnType<RecoveryFsRoot["readManaged"]> | undefined;
|
|
760
|
+
let published: unknown;
|
|
611
761
|
try {
|
|
612
762
|
captured = this.#authority.readManaged(temporary);
|
|
613
763
|
if (!captured.ok || !captured.identity || !captured.data)
|
|
@@ -633,7 +783,7 @@ export class ManagedSessionDescendantStore {
|
|
|
633
783
|
digest,
|
|
634
784
|
);
|
|
635
785
|
if (!synced.ok) throw new Error(synced.code ?? "managed_publish_fsync_failed");
|
|
636
|
-
|
|
786
|
+
published = this.#authority.renameManagedFileNoReplace(
|
|
637
787
|
temporary,
|
|
638
788
|
relative,
|
|
639
789
|
captured.identity.dev,
|
|
@@ -643,35 +793,14 @@ export class ManagedSessionDescendantStore {
|
|
|
643
793
|
captured.identity.ctimeNs,
|
|
644
794
|
digest,
|
|
645
795
|
);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
published.code === "already_exists"
|
|
649
|
-
? "destination_conflict"
|
|
650
|
-
: (published.code ?? "managed_publish_failed"),
|
|
651
|
-
);
|
|
796
|
+
const outcome = classifyNativePublishOutcome(published, "retained_file");
|
|
797
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
652
798
|
} catch (error) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
installed.identity &&
|
|
659
|
-
installed.data &&
|
|
660
|
-
installed.identity.dev === captured.identity.dev &&
|
|
661
|
-
installed.identity.ino === captured.identity.ino &&
|
|
662
|
-
(installed.identity.sha256 ?? createHash("sha256").update(installed.data).digest("hex")) === digest
|
|
663
|
-
) {
|
|
664
|
-
const removed = this.#authority.removeManaged(
|
|
665
|
-
relative,
|
|
666
|
-
installed.identity.dev,
|
|
667
|
-
installed.identity.ino,
|
|
668
|
-
installed.identity.size,
|
|
669
|
-
installed.identity.mtimeNs,
|
|
670
|
-
installed.identity.ctimeNs,
|
|
671
|
-
digest,
|
|
672
|
-
);
|
|
673
|
-
if (!removed.ok) throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
674
|
-
}
|
|
799
|
+
// A committed or unknown native outcome is evidence, not authorization to
|
|
800
|
+
// probe or remove the destination. Only a validated pre-mutation result
|
|
801
|
+
// may clean this operation's still-named staging object.
|
|
802
|
+
const outcome = captured ? classifyNativePublishOutcome(published, "retained_file") : undefined;
|
|
803
|
+
if (captured?.ok && captured.identity && captured.data && outcome && mayCleanCurrentStaging(outcome)) {
|
|
675
804
|
const staged = this.#authority.readManaged(temporary);
|
|
676
805
|
if (
|
|
677
806
|
staged.ok &&
|
|
@@ -689,7 +818,8 @@ export class ManagedSessionDescendantStore {
|
|
|
689
818
|
staged.identity.ctimeNs,
|
|
690
819
|
staged.identity.sha256 ?? createHash("sha256").update(staged.data).digest("hex"),
|
|
691
820
|
);
|
|
692
|
-
if (!removed.ok
|
|
821
|
+
if (!removed.ok && removed.code !== "cleanup_pending")
|
|
822
|
+
throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
693
823
|
}
|
|
694
824
|
}
|
|
695
825
|
throw error;
|
|
@@ -762,7 +892,17 @@ export class ManagedSessionDescendantStore {
|
|
|
762
892
|
sha256: expected.identity.sha256,
|
|
763
893
|
quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
|
|
764
894
|
});
|
|
765
|
-
if (
|
|
895
|
+
if (
|
|
896
|
+
!removed.ok &&
|
|
897
|
+
!(
|
|
898
|
+
removed.code === "cleanup_pending" &&
|
|
899
|
+
(removed.detachedPath ??
|
|
900
|
+
removed.retainedSuccessorPath ??
|
|
901
|
+
removed.retainedPlaceholderPath ??
|
|
902
|
+
removed.retainedUnknownPath) !== undefined
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
766
906
|
this.#assertBound();
|
|
767
907
|
return;
|
|
768
908
|
}
|
|
@@ -775,7 +915,8 @@ export class ManagedSessionDescendantStore {
|
|
|
775
915
|
expected.identity.ctimeNs.toString(),
|
|
776
916
|
expected.identity.sha256,
|
|
777
917
|
);
|
|
778
|
-
if (!removed.ok
|
|
918
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
919
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
779
920
|
this.#assertBound();
|
|
780
921
|
}
|
|
781
922
|
/** Read and remove one managed descendant through retained authority. */
|
|
@@ -799,7 +940,8 @@ export class ManagedSessionDescendantStore {
|
|
|
799
940
|
existing.identity.ctimeNs,
|
|
800
941
|
existing.identity.sha256 ?? createHash("sha256").update(existing.data).digest("hex"),
|
|
801
942
|
);
|
|
802
|
-
if (!removed.ok
|
|
943
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
944
|
+
throw new Error(removed.code ?? "managed_consume_failed");
|
|
803
945
|
this.#assertBound();
|
|
804
946
|
return existing.data;
|
|
805
947
|
}
|
|
@@ -885,92 +1027,31 @@ export class ManagedSessionDescendantStore {
|
|
|
885
1027
|
sourceRelativePath: string,
|
|
886
1028
|
destinationRelativePath: string,
|
|
887
1029
|
expected: NativeDirectoryTreeSnapshot,
|
|
888
|
-
):
|
|
1030
|
+
): NativeDirectoryTreeSnapshot {
|
|
889
1031
|
this.#assertBound();
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
try {
|
|
896
|
-
this.ensureDirectory(destinationRelativePath);
|
|
897
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
898
|
-
for (const entry of expected.entries) {
|
|
899
|
-
if (entry.relativePath === "") continue;
|
|
900
|
-
const target = path.posix.join(destinationRelativePath.replaceAll(path.sep, "/"), entry.relativePath);
|
|
901
|
-
if (entry.kind === "directory") {
|
|
902
|
-
this.ensureDirectory(target);
|
|
903
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
const sourceEntry = this.readExpected(
|
|
907
|
-
path.posix.join(sourceRelativePath.replaceAll(path.sep, "/"), entry.relativePath),
|
|
908
|
-
);
|
|
909
|
-
if (
|
|
910
|
-
!sourceEntry ||
|
|
911
|
-
sourceEntry.identity.dev.toString() !== entry.dev ||
|
|
912
|
-
sourceEntry.identity.ino.toString() !== entry.ino ||
|
|
913
|
-
sourceEntry.identity.size.toString() !== entry.size ||
|
|
914
|
-
sourceEntry.identity.mtimeNs.toString() !== entry.mtimeNs ||
|
|
915
|
-
sourceEntry.identity.sha256 !== entry.sha256
|
|
916
|
-
)
|
|
917
|
-
throw new Error("artifact_source_changed");
|
|
918
|
-
this.publishNoReplaceSync(target, sourceEntry.bytes);
|
|
919
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
920
|
-
}
|
|
921
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
922
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
923
|
-
tree.entries.map(entry => ({
|
|
924
|
-
relativePath: entry.relativePath,
|
|
925
|
-
kind: entry.kind,
|
|
926
|
-
size: entry.size,
|
|
927
|
-
sha256: entry.sha256,
|
|
928
|
-
}));
|
|
929
|
-
if (JSON.stringify(comparable(destinationSnapshot)) !== JSON.stringify(comparable(expected)))
|
|
930
|
-
throw new Error("artifact_destination_mismatch");
|
|
931
|
-
const terminalSource = snapshotDirectoryTree(source);
|
|
932
|
-
if (
|
|
933
|
-
!terminalSource.ok ||
|
|
934
|
-
!terminalSource.snapshot ||
|
|
935
|
-
JSON.stringify(terminalSource.snapshot) !== JSON.stringify(expected)
|
|
1032
|
+
const moved = this.#authority
|
|
1033
|
+
? this.#authority.renameManagedTreeNoReplace(
|
|
1034
|
+
this.#relative(this.#resolve(sourceRelativePath)),
|
|
1035
|
+
this.#relative(this.#resolve(destinationRelativePath)),
|
|
1036
|
+
expected,
|
|
936
1037
|
)
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
this.#relative(this.#resolve(destinationRelativePath)),
|
|
953
|
-
expected,
|
|
954
|
-
);
|
|
955
|
-
if (!moved.ok) {
|
|
956
|
-
try {
|
|
957
|
-
const installed = this.captureTree(destinationRelativePath);
|
|
958
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
959
|
-
tree.entries.map(entry => ({
|
|
960
|
-
relativePath: entry.relativePath,
|
|
961
|
-
kind: entry.kind,
|
|
962
|
-
size: entry.size,
|
|
963
|
-
sha256: entry.sha256,
|
|
964
|
-
}));
|
|
965
|
-
if (JSON.stringify(comparable(installed)) !== JSON.stringify(comparable(expected)))
|
|
966
|
-
throw new Error("managed_publish_reconcile_failed");
|
|
967
|
-
this.removeTreeExpected(destinationRelativePath, installed);
|
|
968
|
-
} catch (error) {
|
|
969
|
-
if (!(error instanceof Error) || error.message !== "not_found") throw error;
|
|
970
|
-
}
|
|
971
|
-
throw new Error(moved.code ?? "managed_move_failed");
|
|
972
|
-
}
|
|
1038
|
+
: renameNoReplacePath(this.#resolve(sourceRelativePath), this.#resolve(destinationRelativePath));
|
|
1039
|
+
const outcome = classifyNativePublishOutcome(moved, this.#authority ? "retained_tree" : "direct_rename");
|
|
1040
|
+
if (!outcome.ok)
|
|
1041
|
+
throw new ManagedTreeMoveOutcomeError(publishFailure(outcome).message, mayCleanCurrentStaging(outcome));
|
|
1042
|
+
const movedSnapshot = this.#authority
|
|
1043
|
+
? this.#authority.snapshotManagedTree(this.#relative(this.#resolve(destinationRelativePath)))
|
|
1044
|
+
: snapshotDirectoryTree(this.#resolve(destinationRelativePath));
|
|
1045
|
+
if (
|
|
1046
|
+
!movedSnapshot.ok ||
|
|
1047
|
+
!movedSnapshot.snapshot ||
|
|
1048
|
+
!sameDirectoryTreeSnapshotAfterRename(movedSnapshot.snapshot, expected)
|
|
1049
|
+
)
|
|
1050
|
+
throw new ManagedTreeMoveOutcomeError("artifact_destination_mismatch", false);
|
|
1051
|
+
if (!this.#authority) fsyncDirectory(this.#baseDir);
|
|
1052
|
+
|
|
973
1053
|
this.#assertBound();
|
|
1054
|
+
return movedSnapshot.snapshot;
|
|
974
1055
|
}
|
|
975
1056
|
|
|
976
1057
|
removeTreeExpected(relativePath: string, expected: NativeDirectoryTreeSnapshot): void {
|
|
@@ -1098,6 +1179,24 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
|
|
|
1098
1179
|
);
|
|
1099
1180
|
}
|
|
1100
1181
|
|
|
1182
|
+
/** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
|
|
1183
|
+
function sameStableIdentityIgnoringCtime(
|
|
1184
|
+
left: ManagedFileSnapshot["identity"],
|
|
1185
|
+
right: ManagedFileSnapshot["identity"],
|
|
1186
|
+
): boolean {
|
|
1187
|
+
return (
|
|
1188
|
+
left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** True when observed identity matches expected on every field except ctimeNs. */
|
|
1193
|
+
function isCtimeOnlyIdentityMismatch(
|
|
1194
|
+
observed: ManagedFileSnapshot["identity"],
|
|
1195
|
+
expected: ManagedFileSnapshot["identity"],
|
|
1196
|
+
): boolean {
|
|
1197
|
+
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1101
1200
|
function parseLock(pathname: string): LockRecord | undefined {
|
|
1102
1201
|
try {
|
|
1103
1202
|
const stat = fs.lstatSync(pathname);
|
|
@@ -1228,9 +1327,10 @@ export async function publishManagedFileNoReplace(
|
|
|
1228
1327
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1229
1328
|
let fd: number | undefined;
|
|
1230
1329
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1231
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1232
|
-
let committed = false;
|
|
1233
1330
|
let failure: unknown;
|
|
1331
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1332
|
+
let renameAttempted = false;
|
|
1333
|
+
|
|
1234
1334
|
try {
|
|
1235
1335
|
assertOwned?.();
|
|
1236
1336
|
fd = fs.openSync(
|
|
@@ -1245,17 +1345,13 @@ export async function publishManagedFileNoReplace(
|
|
|
1245
1345
|
secureFileDescriptor(staging, fd, "verify");
|
|
1246
1346
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1247
1347
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1248
|
-
stagingSnapshot = {
|
|
1249
|
-
bytes: Buffer.from(bytes),
|
|
1250
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1251
|
-
};
|
|
1252
1348
|
assertOwned?.();
|
|
1253
1349
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1350
|
+
renameAttempted = true;
|
|
1351
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1352
|
+
|
|
1353
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1354
|
+
|
|
1259
1355
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1260
1356
|
if (
|
|
1261
1357
|
!named.isFile() ||
|
|
@@ -1270,25 +1366,12 @@ export async function publishManagedFileNoReplace(
|
|
|
1270
1366
|
fd = undefined;
|
|
1271
1367
|
|
|
1272
1368
|
fsyncDirectory(parent);
|
|
1273
|
-
committed = true;
|
|
1274
1369
|
} catch (error) {
|
|
1275
1370
|
failure = error;
|
|
1276
1371
|
} finally {
|
|
1277
1372
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1281
|
-
if (
|
|
1282
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1283
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1284
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1285
|
-
)
|
|
1286
|
-
await unlinkManagedFileVerified(destination, installed.identity);
|
|
1287
|
-
} catch (error) {
|
|
1288
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
if (stagingIdentity) {
|
|
1373
|
+
|
|
1374
|
+
if (stagingIdentity && (!renameAttempted || (outcome && mayCleanCurrentStaging(outcome)))) {
|
|
1292
1375
|
await fsp
|
|
1293
1376
|
.lstat(staging, { bigint: true })
|
|
1294
1377
|
.then(stat => {
|
|
@@ -1314,9 +1397,9 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1314
1397
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1315
1398
|
let fd: number | undefined;
|
|
1316
1399
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1317
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1318
|
-
let committed = false;
|
|
1319
1400
|
let failure: unknown;
|
|
1401
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1402
|
+
|
|
1320
1403
|
try {
|
|
1321
1404
|
fd = fs.openSync(
|
|
1322
1405
|
staging,
|
|
@@ -1330,16 +1413,10 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1330
1413
|
secureFileDescriptor(staging, fd, "verify");
|
|
1331
1414
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1332
1415
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1333
|
-
stagingSnapshot = {
|
|
1334
|
-
bytes: Buffer.from(bytes),
|
|
1335
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1336
|
-
};
|
|
1337
1416
|
|
|
1338
|
-
|
|
1339
|
-
if (!
|
|
1340
|
-
|
|
1341
|
-
throw new Error("durability_failed");
|
|
1342
|
-
}
|
|
1417
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1418
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1419
|
+
|
|
1343
1420
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1344
1421
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
1345
1422
|
throw new Error("destination_identity_changed");
|
|
@@ -1348,35 +1425,12 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1348
1425
|
fs.closeSync(fd);
|
|
1349
1426
|
fd = undefined;
|
|
1350
1427
|
fsyncDirectory(parent);
|
|
1351
|
-
committed = true;
|
|
1352
1428
|
} catch (error) {
|
|
1353
1429
|
failure = error;
|
|
1354
1430
|
} finally {
|
|
1355
1431
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1356
1432
|
}
|
|
1357
|
-
if (stagingIdentity) {
|
|
1358
|
-
if (!committed && stagingSnapshot) {
|
|
1359
|
-
try {
|
|
1360
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1361
|
-
if (
|
|
1362
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1363
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1364
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1365
|
-
) {
|
|
1366
|
-
const removed = exactUnlink(destination, {
|
|
1367
|
-
dev: installed.identity.dev,
|
|
1368
|
-
ino: installed.identity.ino,
|
|
1369
|
-
size: BigInt(installed.identity.size),
|
|
1370
|
-
mtimeNs: installed.identity.mtimeNs,
|
|
1371
|
-
sha256: installed.identity.sha256,
|
|
1372
|
-
quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
|
|
1373
|
-
});
|
|
1374
|
-
if (!removed.ok) failure ??= new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
1375
|
-
}
|
|
1376
|
-
} catch (error) {
|
|
1377
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1433
|
+
if (stagingIdentity && outcome && mayCleanCurrentStaging(outcome)) {
|
|
1380
1434
|
try {
|
|
1381
1435
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
1382
1436
|
if (named.dev !== stagingIdentity.dev || named.ino !== stagingIdentity.ino) {
|
|
@@ -1586,8 +1640,20 @@ export async function acquireManagedLock(
|
|
|
1586
1640
|
}
|
|
1587
1641
|
}
|
|
1588
1642
|
|
|
1643
|
+
export interface ManagedArtifactTreeLimits {
|
|
1644
|
+
maxFiles?: number;
|
|
1645
|
+
maxTotalBytes?: number;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1589
1648
|
/** Bounds a no-follow artifact tree before it can be copied or deleted. */
|
|
1590
|
-
export function validateManagedArtifactTree(root: string): void {
|
|
1649
|
+
export function validateManagedArtifactTree(root: string, limits: ManagedArtifactTreeLimits = {}): void {
|
|
1650
|
+
const clampLimit = (limit: number | undefined, maximum: number): number => {
|
|
1651
|
+
if (limit === undefined) return maximum;
|
|
1652
|
+
if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit <= 0) throw new Error("unsafe_artifacts");
|
|
1653
|
+
return Math.min(limit, maximum);
|
|
1654
|
+
};
|
|
1655
|
+
const maxFiles = clampLimit(limits.maxFiles, MANAGED_ARTIFACT_MAX_FILES);
|
|
1656
|
+
const maxTotalBytes = clampLimit(limits.maxTotalBytes, MANAGED_ARTIFACT_MAX_TOTAL_BYTES);
|
|
1591
1657
|
let files = 0;
|
|
1592
1658
|
let bytes = 0;
|
|
1593
1659
|
const visit = (directory: string, depth: number): void => {
|
|
@@ -1604,8 +1670,7 @@ export function validateManagedArtifactTree(root: string): void {
|
|
|
1604
1670
|
if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
|
|
1605
1671
|
files++;
|
|
1606
1672
|
bytes += stat.size;
|
|
1607
|
-
if (files >
|
|
1608
|
-
throw new Error("unsafe_artifacts");
|
|
1673
|
+
if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
|
|
1609
1674
|
}
|
|
1610
1675
|
};
|
|
1611
1676
|
const rootStat = fs.lstatSync(root);
|