@ricsam/r5d-worker 0.0.77 → 0.0.79
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/README.md +6 -0
- package/dist/cjs/main.cjs +1562 -335
- package/dist/cjs/managed-paths.cjs +101 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +184 -5
- package/dist/cjs/project-worktrees.cjs +641 -56
- package/dist/cjs/registry-auth.cjs +310 -0
- package/dist/cjs/repository-transition-policy.cjs +49 -0
- package/dist/cjs/supervisor.cjs +62 -11
- package/dist/cjs/working-tree-mirror.cjs +191 -34
- package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
- package/dist/cjs/workspace-git-sync.cjs +1028 -54
- package/dist/cjs/workspace-mount-boundary.cjs +71 -0
- package/dist/cjs/workspace-path-move.cjs +195 -0
- package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
- package/dist/cjs/workspace-project-config-policy.cjs +46 -0
- package/dist/cjs/workspace-publication-evidence.cjs +46 -0
- package/dist/mjs/main.mjs +1596 -341
- package/dist/mjs/managed-paths.mjs +100 -1
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +181 -5
- package/dist/mjs/project-worktrees.mjs +632 -55
- package/dist/mjs/registry-auth.mjs +269 -0
- package/dist/mjs/repository-transition-policy.mjs +22 -0
- package/dist/mjs/supervisor.mjs +61 -11
- package/dist/mjs/working-tree-mirror.mjs +190 -34
- package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
- package/dist/mjs/workspace-git-sync.mjs +1023 -54
- package/dist/mjs/workspace-mount-boundary.mjs +37 -0
- package/dist/mjs/workspace-path-move.mjs +160 -0
- package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
- package/dist/mjs/workspace-project-config-policy.mjs +21 -0
- package/dist/mjs/workspace-publication-evidence.mjs +21 -0
- package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
- package/dist/types/main.d.ts +175 -1
- package/dist/types/managed-paths.d.ts +11 -0
- package/dist/types/project-workspace-state.d.ts +45 -1
- package/dist/types/project-worktrees.d.ts +119 -2
- package/dist/types/registry-auth.d.ts +47 -0
- package/dist/types/repository-transition-policy.d.ts +26 -0
- package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
- package/dist/types/supervisor-signal-fixture.d.ts +1 -0
- package/dist/types/supervisor.d.ts +6 -4
- package/dist/types/working-tree-mirror.d.ts +7 -0
- package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
- package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
- package/dist/types/workspace-command-sync-policy.d.ts +9 -0
- package/dist/types/workspace-git-sync.d.ts +33 -3
- package/dist/types/workspace-mount-boundary.d.ts +10 -0
- package/dist/types/workspace-path-move.d.ts +21 -0
- package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
- package/dist/types/workspace-project-config-policy.d.ts +34 -0
- package/dist/types/workspace-publication-evidence.d.ts +17 -0
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import os, { hostname } from "node:os";
|
|
6
6
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
7
8
|
import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
8
9
|
import { gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
9
10
|
import {
|
|
@@ -14,40 +15,79 @@ import {
|
|
|
14
15
|
import { terminateProcessTree } from "./process-tree.mjs";
|
|
15
16
|
import { createPtyOutputCoalescer } from "./pty-output-coalescer.mjs";
|
|
16
17
|
import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
|
|
18
|
+
import {
|
|
19
|
+
configureGitHubRegistryAuthFiles,
|
|
20
|
+
planGitHubRegistryAuthFiles,
|
|
21
|
+
preparePrivateAuthFileGeneration,
|
|
22
|
+
registryAuthHasAppManagedGitHubCredential,
|
|
23
|
+
RegistryAuthConfigurationError
|
|
24
|
+
} from "./registry-auth.mjs";
|
|
17
25
|
import { applyWorkspaceIncidentUpdate } from "./workspace-incident-state.mjs";
|
|
18
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
hasActiveVisibleProjectsWorkspaceTarget,
|
|
28
|
+
PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS,
|
|
29
|
+
pendingCreatedBranchAutomaticSyncDeferral,
|
|
30
|
+
pendingCreatedBranchKey,
|
|
31
|
+
projectBranchHasActiveWorkspaceTarget
|
|
32
|
+
} from "./workspace-automatic-sync-policy.mjs";
|
|
33
|
+
import {
|
|
34
|
+
acquireWorkspaceCommandMutation,
|
|
35
|
+
reserveWorkspaceCommandAfterCurrentSync,
|
|
36
|
+
runWorkspaceCommand
|
|
37
|
+
} from "./workspace-command-sync-policy.mjs";
|
|
19
38
|
import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
|
|
39
|
+
import { checkoutPathMovePublicationComplete, preserveProjectCheckoutPathMove } from "./workspace-path-move.mjs";
|
|
40
|
+
import { busyProjectConfigurationChangeIds, deferredProjectConfigurationPendingBranches } from "./workspace-project-config-policy.mjs";
|
|
41
|
+
import { creatorLocalProjectBranchIsAuthorized, workerProjectBranchDisposition } from "./workspace-preserve-only-policy.mjs";
|
|
42
|
+
import { assertProjectBranchDeletionIncarnation } from "./workspace-branch-incarnation-policy.mjs";
|
|
43
|
+
import { activeProjectBranchPublicationEvidence } from "./workspace-publication-evidence.mjs";
|
|
44
|
+
import {
|
|
45
|
+
assertRepositoryExecutionEnabled,
|
|
46
|
+
assertRepositoryTransitionState,
|
|
47
|
+
enabledRepositoryTransitionRequiresReseed,
|
|
48
|
+
repositoryMirrorWritesAllowed
|
|
49
|
+
} from "./repository-transition-policy.mjs";
|
|
20
50
|
import {
|
|
21
51
|
isRetryableWorkerServerStatus,
|
|
22
52
|
superviseWorkerRuntime,
|
|
53
|
+
WORKER_CREDENTIAL_RESTART_EXIT_CODE,
|
|
23
54
|
WORKER_RECONNECT_DELAY_MS,
|
|
24
55
|
WORKER_RECONNECT_EXIT_CODE,
|
|
25
56
|
WORKER_RELOAD_EXIT_CODE,
|
|
26
57
|
WORKER_RUNTIME_ENV
|
|
27
58
|
} from "./supervisor.mjs";
|
|
28
|
-
import { managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
|
|
59
|
+
import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchName } from "./managed-paths.mjs";
|
|
29
60
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
applyObservedProjectMirrorHeads,
|
|
62
|
+
assertProjectMirrorHeadsPushed,
|
|
63
|
+
recoverStaleProjectWorktreeSnapshots,
|
|
64
|
+
createOrRetryLinkedProjectBranch,
|
|
32
65
|
deleteLinkedProjectBranch,
|
|
33
66
|
deleteProjectMirrorBranch,
|
|
34
67
|
ensureProjectWorktrees,
|
|
35
|
-
|
|
68
|
+
observeProjectMirrorHeads,
|
|
69
|
+
projectBranchMayExistAfterCreateFailure,
|
|
70
|
+
projectOriginBranchName,
|
|
36
71
|
projectWorktreeConfigurationFingerprint,
|
|
37
72
|
projectWorktreeOperationInProgress,
|
|
38
73
|
pushProjectMirrorHeads,
|
|
39
74
|
removeProjectWorktrees
|
|
40
75
|
} from "./project-worktrees.mjs";
|
|
41
76
|
import {
|
|
77
|
+
ProjectWorkspacePendingBranchPathChangeError,
|
|
42
78
|
ProjectWorkspaceStateStore,
|
|
43
79
|
pruneAuthoritativelyDesiredBranchDeletions
|
|
44
80
|
} from "./project-workspace-state.mjs";
|
|
45
81
|
import {
|
|
46
82
|
ensureWorkspaceGitClone,
|
|
47
83
|
hydrateWorkspaceGitMounts,
|
|
84
|
+
recoverWorkspaceGitHydration,
|
|
48
85
|
resetWorkspaceGit,
|
|
49
|
-
synchronizeWorkspaceGit
|
|
86
|
+
synchronizeWorkspaceGit,
|
|
87
|
+
workspaceGitHydrationIsCurrent
|
|
50
88
|
} from "./workspace-git-sync.mjs";
|
|
89
|
+
class ProjectWorkspaceConfigurationDeferredError extends Error {
|
|
90
|
+
}
|
|
51
91
|
class WorkerServerUnavailableError extends Error {
|
|
52
92
|
name = "WorkerServerUnavailableError";
|
|
53
93
|
}
|
|
@@ -60,12 +100,19 @@ const MAX_LINE_LENGTH = 2e3;
|
|
|
60
100
|
const WORKSPACE_GIT_QUIET_MS = 5e3;
|
|
61
101
|
const WORKSPACE_GIT_PERIODIC_MS = 6e4;
|
|
62
102
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
103
|
+
const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
|
|
104
|
+
const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
|
|
105
|
+
const workspaceSyncPriorityProcessTargets = /* @__PURE__ */ new Map();
|
|
106
|
+
const workspaceSyncPriorityOperationTargets = /* @__PURE__ */ new Map();
|
|
63
107
|
const pendingProcessTerminals = /* @__PURE__ */ new Map();
|
|
64
108
|
const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
65
109
|
const activePtys = /* @__PURE__ */ new Map();
|
|
110
|
+
const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
|
|
66
111
|
let currentWorkerSocket = null;
|
|
112
|
+
let workerAdmissionGeneration = 0;
|
|
67
113
|
const workspaceMutationGate = new WorkspaceMutationGate();
|
|
68
114
|
let workspaceSyncQueue = Promise.resolve();
|
|
115
|
+
let startupProjectSnapshotRecoveryCompleted = false;
|
|
69
116
|
const workspaceSyncSingleFlight = {
|
|
70
117
|
runExclusive(operation) {
|
|
71
118
|
const queued = workspaceMutationGate.runSync(operation);
|
|
@@ -86,7 +133,29 @@ const workspaceSyncSingleFlight = {
|
|
|
86
133
|
}
|
|
87
134
|
};
|
|
88
135
|
let githubCredential = null;
|
|
136
|
+
let configuredCredentialGenerationFingerprint = null;
|
|
137
|
+
let pendingCredentialGenerationIntentAtProcessStart = null;
|
|
138
|
+
let bootstrapCredentialGenerationFingerprintAtProcessStart = null;
|
|
139
|
+
let bootSessionIdentifierAtProcessStart = null;
|
|
140
|
+
let machineIdentifierAtProcessStart = null;
|
|
141
|
+
let systemdInvocationIdentifierAtProcessStart = null;
|
|
142
|
+
let initialCredentialBootstrapProvenEmpty = false;
|
|
89
143
|
let visibleGitIdentity = null;
|
|
144
|
+
class StaleWorkerAdmissionError extends Error {
|
|
145
|
+
constructor() {
|
|
146
|
+
super("Worker command admission was invalidated before child creation");
|
|
147
|
+
this.name = "StaleWorkerAdmissionError";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function advanceWorkerAdmissionGeneration() {
|
|
151
|
+
workerAdmissionGeneration += 1;
|
|
152
|
+
return workerAdmissionGeneration;
|
|
153
|
+
}
|
|
154
|
+
function assertWorkerChildAdmission(input) {
|
|
155
|
+
if (input.capturedGeneration !== input.currentGeneration || !input.sourceSocketIsCurrent || !input.sourceSocketOpen || !input.workspaceConfigured || !input.runtimeHealthy) {
|
|
156
|
+
throw new StaleWorkerAdmissionError();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
90
159
|
function defaultConfigPath() {
|
|
91
160
|
return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
92
161
|
}
|
|
@@ -788,6 +857,183 @@ function readOnlyCredentialStoreHelper(storePath) {
|
|
|
788
857
|
function credentialStoreDirectory() {
|
|
789
858
|
return path.join(os.homedir(), ".r5d", "git-credentials");
|
|
790
859
|
}
|
|
860
|
+
function credentialGenerationReceiptPath(directoryPath = credentialStoreDirectory()) {
|
|
861
|
+
return path.join(directoryPath, ".auth-generation");
|
|
862
|
+
}
|
|
863
|
+
function pendingCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
|
|
864
|
+
return path.join(directoryPath, ".auth-generation-pending");
|
|
865
|
+
}
|
|
866
|
+
function bootstrapCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
|
|
867
|
+
return path.join(directoryPath, ".auth-generation-bootstrap");
|
|
868
|
+
}
|
|
869
|
+
const BOOT_SESSION_IDENTIFIER_PATTERN = /^(?:linux:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|darwin:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
870
|
+
const MACHINE_IDENTIFIER_PATTERN = /^(?:linux:[0-9a-f]{32}|darwin:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|win32:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
871
|
+
const SYSTEMD_INVOCATION_IDENTIFIER_PATTERN = /^[0-9a-f]{32}$/;
|
|
872
|
+
function exactObjectKeys(value, expected) {
|
|
873
|
+
const actual = Object.keys(value).sort();
|
|
874
|
+
const expectedKeys = [...expected].sort();
|
|
875
|
+
return actual.length === expectedKeys.length && actual.every((key, index) => key === expectedKeys[index]);
|
|
876
|
+
}
|
|
877
|
+
function pendingCredentialGenerationIntentContent(intent) {
|
|
878
|
+
return `${JSON.stringify({ version: 1, fingerprint: intent.fingerprint, boundary: intent.boundary })}
|
|
879
|
+
`;
|
|
880
|
+
}
|
|
881
|
+
function readPendingCredentialGenerationIntent(intentPath = pendingCredentialGenerationPath()) {
|
|
882
|
+
const stat = lstatIfExists(intentPath);
|
|
883
|
+
if (!stat) return null;
|
|
884
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
885
|
+
throw new RegistryAuthConfigurationError(new Error(`Pending credential generation intent must be a regular file: ${intentPath}`));
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
const parsed = JSON.parse(fs.readFileSync(intentPath, "utf8"));
|
|
889
|
+
const boundary = parsed.boundary;
|
|
890
|
+
if (!parsed || Array.isArray(parsed) || !exactObjectKeys(parsed, ["boundary", "fingerprint", "version"]) || parsed.version !== 1 || typeof parsed.fingerprint !== "string" || !/^[0-9a-f]{64}$/.test(parsed.fingerprint) || !boundary || typeof boundary !== "object" || Array.isArray(boundary)) {
|
|
891
|
+
throw new Error("invalid intent fields");
|
|
892
|
+
}
|
|
893
|
+
const boundaryRecord = boundary;
|
|
894
|
+
const machineId = boundaryRecord.machineId;
|
|
895
|
+
if (!(machineId === null || typeof machineId === "string" && MACHINE_IDENTIFIER_PATTERN.test(machineId))) {
|
|
896
|
+
throw new Error("invalid machine identifier");
|
|
897
|
+
}
|
|
898
|
+
let parsedBoundary;
|
|
899
|
+
if (boundaryRecord.kind === "systemd" && exactObjectKeys(boundaryRecord, ["invocationId", "kind", "machineId"]) && typeof boundaryRecord.invocationId === "string" && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(boundaryRecord.invocationId)) {
|
|
900
|
+
parsedBoundary = {
|
|
901
|
+
kind: "systemd",
|
|
902
|
+
machineId,
|
|
903
|
+
invocationId: boundaryRecord.invocationId
|
|
904
|
+
};
|
|
905
|
+
} else if (boundaryRecord.kind === "boot" && exactObjectKeys(boundaryRecord, ["bootSessionId", "kind", "machineId"]) && (boundaryRecord.bootSessionId === null || typeof boundaryRecord.bootSessionId === "string" && BOOT_SESSION_IDENTIFIER_PATTERN.test(boundaryRecord.bootSessionId))) {
|
|
906
|
+
parsedBoundary = {
|
|
907
|
+
kind: "boot",
|
|
908
|
+
machineId,
|
|
909
|
+
bootSessionId: boundaryRecord.bootSessionId
|
|
910
|
+
};
|
|
911
|
+
} else {
|
|
912
|
+
throw new Error("invalid credential boundary");
|
|
913
|
+
}
|
|
914
|
+
return { fingerprint: parsed.fingerprint, boundary: parsedBoundary };
|
|
915
|
+
} catch (error) {
|
|
916
|
+
throw new RegistryAuthConfigurationError(
|
|
917
|
+
new Error(`Pending credential generation intent is malformed: ${intentPath}`, { cause: error })
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function parseBootSessionIdentifier(probe) {
|
|
922
|
+
const value = probe.kernelValue.trim();
|
|
923
|
+
if (probe.platform === "linux") {
|
|
924
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? `linux:${value.toLowerCase()}` : null;
|
|
925
|
+
}
|
|
926
|
+
if (probe.platform === "darwin") {
|
|
927
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? `darwin:${value.toLowerCase()}` : null;
|
|
928
|
+
}
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
function readBootSessionIdentifier(probe) {
|
|
932
|
+
if (probe) return parseBootSessionIdentifier(probe);
|
|
933
|
+
try {
|
|
934
|
+
if (process.platform === "linux") {
|
|
935
|
+
return parseBootSessionIdentifier({
|
|
936
|
+
platform: process.platform,
|
|
937
|
+
kernelValue: fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8")
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
const command = process.platform === "darwin" ? ["/usr/sbin/sysctl", "-n", "kern.bootsessionuuid"] : null;
|
|
941
|
+
if (!command) return null;
|
|
942
|
+
const result = Bun.spawnSync(command, {
|
|
943
|
+
stdout: "pipe",
|
|
944
|
+
stderr: "pipe",
|
|
945
|
+
env: workerGitProcessEnvironment(),
|
|
946
|
+
timeout: 2e3
|
|
947
|
+
});
|
|
948
|
+
if (result.exitCode !== 0) return null;
|
|
949
|
+
return parseBootSessionIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
|
|
950
|
+
} catch {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function parseMachineIdentifier(probe) {
|
|
955
|
+
const value = probe.kernelValue.trim();
|
|
956
|
+
if (probe.platform === "linux") {
|
|
957
|
+
return /^[0-9a-f]{32}$/i.test(value) ? `linux:${value.toLowerCase()}` : null;
|
|
958
|
+
}
|
|
959
|
+
const uuid = probe.platform === "darwin" ? /["']?IOPlatformUUID["']?\s*=\s*["']([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})["']/i.exec(value)?.[1] : probe.platform === "win32" ? /^\{?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}?$/i.exec(value)?.[1] : null;
|
|
960
|
+
return uuid ? `${probe.platform}:${uuid.toLowerCase()}` : null;
|
|
961
|
+
}
|
|
962
|
+
function readMachineIdentifier(probe) {
|
|
963
|
+
if (probe) return parseMachineIdentifier(probe);
|
|
964
|
+
try {
|
|
965
|
+
if (process.platform === "linux") {
|
|
966
|
+
return parseMachineIdentifier({ platform: process.platform, kernelValue: fs.readFileSync("/etc/machine-id", "utf8") });
|
|
967
|
+
}
|
|
968
|
+
const command = process.platform === "darwin" ? ["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"] : null;
|
|
969
|
+
if (!command) return null;
|
|
970
|
+
const result = Bun.spawnSync(command, {
|
|
971
|
+
stdout: "pipe",
|
|
972
|
+
stderr: "pipe",
|
|
973
|
+
env: workerGitProcessEnvironment(),
|
|
974
|
+
timeout: 2e3
|
|
975
|
+
});
|
|
976
|
+
if (result.exitCode !== 0) return null;
|
|
977
|
+
return parseMachineIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
|
|
978
|
+
} catch {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function credentialAuthorityLockPath(rootPath = path.join(os.homedir(), ".r5d")) {
|
|
983
|
+
return path.join(rootPath, ".credential-authority.sqlite");
|
|
984
|
+
}
|
|
985
|
+
function acquireCredentialAuthorityLock(input) {
|
|
986
|
+
const rootPath = path.resolve(input.rootPath ?? path.join(os.homedir(), ".r5d"));
|
|
987
|
+
const rootStat = lstatIfExists(rootPath);
|
|
988
|
+
if (rootStat) {
|
|
989
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
990
|
+
throw new Error(`Worker credential authority root must be a directory: ${rootPath}`);
|
|
991
|
+
}
|
|
992
|
+
} else {
|
|
993
|
+
fs.mkdirSync(rootPath, { recursive: true, mode: 448 });
|
|
994
|
+
}
|
|
995
|
+
fs.chmodSync(rootPath, 448);
|
|
996
|
+
const lockPath = credentialAuthorityLockPath(rootPath);
|
|
997
|
+
const existing = lstatIfExists(lockPath);
|
|
998
|
+
if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
|
|
999
|
+
throw new Error(`Worker credential authority database must be a regular file: ${lockPath}`);
|
|
1000
|
+
}
|
|
1001
|
+
const database = new Database(lockPath, { create: true, strict: true });
|
|
1002
|
+
try {
|
|
1003
|
+
database.exec("PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE");
|
|
1004
|
+
database.exec("CREATE TABLE IF NOT EXISTS authority_metadata (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), machine_id TEXT)");
|
|
1005
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1006
|
+
try {
|
|
1007
|
+
const row = database.query("SELECT machine_id AS machineId FROM authority_metadata WHERE singleton = 1").get();
|
|
1008
|
+
if (row) {
|
|
1009
|
+
if (row.machineId === null || input.machineId === null) {
|
|
1010
|
+
if (row.machineId !== input.machineId) {
|
|
1011
|
+
throw new Error("Cannot prove that the worker credential authority database belongs to this machine");
|
|
1012
|
+
}
|
|
1013
|
+
} else if (row.machineId !== input.machineId) {
|
|
1014
|
+
throw new Error("Worker credential authority home belongs to a different machine; refusing to move or share credential state");
|
|
1015
|
+
}
|
|
1016
|
+
} else {
|
|
1017
|
+
database.query("INSERT INTO authority_metadata (singleton, machine_id) VALUES (1, ?)").run(input.machineId);
|
|
1018
|
+
}
|
|
1019
|
+
database.exec("COMMIT");
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
database.exec("ROLLBACK");
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
database.exec("BEGIN EXCLUSIVE");
|
|
1025
|
+
return { lockPath, database };
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
database.close(false);
|
|
1028
|
+
if (error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message)) {
|
|
1029
|
+
throw new Error("Another r5d-worker credential authority is already running under this OS account", { cause: error });
|
|
1030
|
+
}
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function releaseCredentialAuthorityLockHandle(handle) {
|
|
1035
|
+
handle.database.close(false);
|
|
1036
|
+
}
|
|
791
1037
|
function legacySharedCredentialStorePath() {
|
|
792
1038
|
return path.join(os.homedir(), ".r5d", "github-credentials");
|
|
793
1039
|
}
|
|
@@ -818,9 +1064,24 @@ function ensurePrivateCredentialStoreDirectory(directoryPath) {
|
|
|
818
1064
|
fs.chmodSync(directoryPath, 448);
|
|
819
1065
|
}
|
|
820
1066
|
function removeLegacySharedCredentialStore(storePath = legacySharedCredentialStorePath()) {
|
|
821
|
-
|
|
1067
|
+
const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
|
|
1068
|
+
prepared.commit();
|
|
822
1069
|
}
|
|
823
1070
|
let credentialStoreDirectoryInitialized = false;
|
|
1071
|
+
let credentialAuthorityLockHandle = null;
|
|
1072
|
+
function releaseCredentialAuthorityLock() {
|
|
1073
|
+
const handle = credentialAuthorityLockHandle;
|
|
1074
|
+
credentialAuthorityLockHandle = null;
|
|
1075
|
+
if (!handle) return;
|
|
1076
|
+
try {
|
|
1077
|
+
releaseCredentialAuthorityLockHandle(handle);
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
process.stderr.write(
|
|
1080
|
+
`[r5d-worker] could not release credential authority lock: ${error instanceof Error ? error.message : String(error)}
|
|
1081
|
+
`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
824
1085
|
function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory()) {
|
|
825
1086
|
const stat = lstatIfExists(directoryPath);
|
|
826
1087
|
if (stat) {
|
|
@@ -831,11 +1092,73 @@ function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory(
|
|
|
831
1092
|
}
|
|
832
1093
|
function initializeCredentialStoreDirectoryOnce() {
|
|
833
1094
|
if (credentialStoreDirectoryInitialized) return;
|
|
834
|
-
|
|
835
|
-
|
|
1095
|
+
systemdInvocationIdentifierAtProcessStart = SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? process.env.INVOCATION_ID.toLowerCase() : null;
|
|
1096
|
+
bootSessionIdentifierAtProcessStart = readBootSessionIdentifier();
|
|
1097
|
+
machineIdentifierAtProcessStart = readMachineIdentifier();
|
|
1098
|
+
credentialAuthorityLockHandle = acquireCredentialAuthorityLock({
|
|
1099
|
+
machineId: machineIdentifierAtProcessStart
|
|
1100
|
+
});
|
|
1101
|
+
process.once("exit", releaseCredentialAuthorityLock);
|
|
1102
|
+
initialCredentialBootstrapProvenEmpty = noPriorCredentialArtifactsAtProcessStart();
|
|
1103
|
+
ensurePrivateCredentialStoreDirectory(credentialStoreDirectory());
|
|
1104
|
+
configuredCredentialGenerationFingerprint = readCredentialGenerationReceipt();
|
|
1105
|
+
pendingCredentialGenerationIntentAtProcessStart = readPendingCredentialGenerationIntent();
|
|
1106
|
+
bootstrapCredentialGenerationFingerprintAtProcessStart = readCredentialGenerationReceipt(bootstrapCredentialGenerationPath());
|
|
836
1107
|
credentialStoreDirectoryInitialized = true;
|
|
837
1108
|
}
|
|
1109
|
+
function noPriorCredentialArtifactsAtProcessStart() {
|
|
1110
|
+
if (lstatIfExists(credentialStoreDirectory()) || lstatIfExists(legacySharedCredentialStorePath())) return false;
|
|
1111
|
+
try {
|
|
1112
|
+
return ![dockerConfigPath(), containersAuthPath()].some(registryAuthHasAppManagedGitHubCredential);
|
|
1113
|
+
} catch {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function readCredentialGenerationReceipt(receiptPath = credentialGenerationReceiptPath()) {
|
|
1118
|
+
const stat = lstatIfExists(receiptPath);
|
|
1119
|
+
if (!stat) return null;
|
|
1120
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
1121
|
+
throw new RegistryAuthConfigurationError(new Error(`Credential generation receipt must be a regular file: ${receiptPath}`));
|
|
1122
|
+
}
|
|
1123
|
+
const content = fs.readFileSync(receiptPath, "utf8");
|
|
1124
|
+
const match = /^([0-9a-f]{64})\n$/.exec(content);
|
|
1125
|
+
if (!match) {
|
|
1126
|
+
throw new RegistryAuthConfigurationError(new Error(`Credential generation receipt is malformed: ${receiptPath}`));
|
|
1127
|
+
}
|
|
1128
|
+
return match[1];
|
|
1129
|
+
}
|
|
838
1130
|
const APP_CREDENTIAL_STORE_FILE = /^(?:project|workspace)-[0-9a-f]{64}\.store$/;
|
|
1131
|
+
function planCredentialStoreGeneration(desiredFiles, directoryPath = credentialStoreDirectory()) {
|
|
1132
|
+
ensurePrivateCredentialStoreDirectory(directoryPath);
|
|
1133
|
+
const resolvedDirectory = path.resolve(directoryPath);
|
|
1134
|
+
const updates = /* @__PURE__ */ new Map();
|
|
1135
|
+
const symlinkRemovals = /* @__PURE__ */ new Set();
|
|
1136
|
+
for (const [storePath, content] of desiredFiles) {
|
|
1137
|
+
const resolvedStorePath = path.resolve(storePath);
|
|
1138
|
+
if (path.dirname(resolvedStorePath) !== resolvedDirectory || !APP_CREDENTIAL_STORE_FILE.test(path.basename(resolvedStorePath))) {
|
|
1139
|
+
throw new Error("Invalid app-owned Git credential-store path");
|
|
1140
|
+
}
|
|
1141
|
+
updates.set(resolvedStorePath, content);
|
|
1142
|
+
}
|
|
1143
|
+
for (const name of fs.readdirSync(directoryPath)) {
|
|
1144
|
+
if (!APP_CREDENTIAL_STORE_FILE.test(name)) continue;
|
|
1145
|
+
const storePath = path.join(resolvedDirectory, name);
|
|
1146
|
+
const stat = fs.lstatSync(storePath);
|
|
1147
|
+
if (stat.isSymbolicLink()) {
|
|
1148
|
+
if (updates.has(storePath)) throw new Error(`Required Git credential store must not be a symlink: ${storePath}`);
|
|
1149
|
+
updates.set(storePath, null);
|
|
1150
|
+
symlinkRemovals.add(storePath);
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (!stat.isFile()) throw new Error(`Git credential store must be a regular file: ${storePath}`);
|
|
1154
|
+
if (!updates.has(storePath)) updates.set(storePath, null);
|
|
1155
|
+
}
|
|
1156
|
+
return [...updates].map(([filePath, content]) => ({
|
|
1157
|
+
filePath,
|
|
1158
|
+
content,
|
|
1159
|
+
...symlinkRemovals.has(filePath) ? { allowSymlinkRemoval: true } : {}
|
|
1160
|
+
}));
|
|
1161
|
+
}
|
|
839
1162
|
function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentialStoreDirectory()) {
|
|
840
1163
|
ensurePrivateCredentialStoreDirectory(directoryPath);
|
|
841
1164
|
const resolvedDirectory = path.resolve(directoryPath);
|
|
@@ -853,9 +1176,10 @@ function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentia
|
|
|
853
1176
|
const storePath = path.join(directoryPath, name);
|
|
854
1177
|
const stat = fs.lstatSync(storePath);
|
|
855
1178
|
if (!stat.isFile() && !stat.isSymbolicLink()) continue;
|
|
856
|
-
fs.unlinkSync(storePath);
|
|
857
1179
|
removed.push(storePath);
|
|
858
1180
|
}
|
|
1181
|
+
const prepared = preparePrivateAuthFileGeneration(removed.map((filePath) => ({ filePath, content: null, allowSymlinkRemoval: true })));
|
|
1182
|
+
prepared.commit();
|
|
859
1183
|
return removed;
|
|
860
1184
|
}
|
|
861
1185
|
function removeProjectCredentialStore(projectId) {
|
|
@@ -863,7 +1187,8 @@ function removeProjectCredentialStore(projectId) {
|
|
|
863
1187
|
const stat = lstatIfExists(storePath);
|
|
864
1188
|
if (!stat) return;
|
|
865
1189
|
if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("Git credential store must be a regular file");
|
|
866
|
-
|
|
1190
|
+
const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
|
|
1191
|
+
prepared.commit();
|
|
867
1192
|
}
|
|
868
1193
|
function decodeGitAuthHeader(authHeader) {
|
|
869
1194
|
const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
|
|
@@ -883,16 +1208,10 @@ function credentialUsernameForAuthHeader(authHeader) {
|
|
|
883
1208
|
return credentials.username;
|
|
884
1209
|
}
|
|
885
1210
|
function replaceCredentialStoreFile(storePath, content) {
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
fs.writeFileSync(temporaryPath, content, { flag: "wx", mode: 384 });
|
|
889
|
-
fs.chmodSync(temporaryPath, 384);
|
|
890
|
-
fs.renameSync(temporaryPath, storePath);
|
|
891
|
-
} finally {
|
|
892
|
-
fs.rmSync(temporaryPath, { force: true });
|
|
893
|
-
}
|
|
1211
|
+
const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content }]);
|
|
1212
|
+
prepared.commit();
|
|
894
1213
|
}
|
|
895
|
-
function
|
|
1214
|
+
function credentialStoreContent(remotes) {
|
|
896
1215
|
const authoritativeEntries = /* @__PURE__ */ new Map();
|
|
897
1216
|
for (const input of remotes) {
|
|
898
1217
|
let remote;
|
|
@@ -924,23 +1243,179 @@ function writeCredentialStoreEntries(remotes, storePath) {
|
|
|
924
1243
|
}
|
|
925
1244
|
authoritativeEntries.set(hostKey, replacement);
|
|
926
1245
|
}
|
|
927
|
-
const storeStat = lstatIfExists(storePath);
|
|
928
|
-
if (storeStat) {
|
|
929
|
-
if (storeStat.isSymbolicLink() || !storeStat.isFile()) throw new Error("Git credential store must be a regular file");
|
|
930
|
-
}
|
|
931
1246
|
const replacements = [...authoritativeEntries.values()].filter((entry) => entry !== null);
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1247
|
+
return replacements.length > 0 ? `${replacements.join("\n")}
|
|
1248
|
+
` : null;
|
|
1249
|
+
}
|
|
1250
|
+
function writeCredentialStoreEntries(remotes, storePath) {
|
|
1251
|
+
const content = credentialStoreContent(remotes);
|
|
1252
|
+
const prepared = preparePrivateAuthFileGeneration([{ filePath: storePath, content }]);
|
|
1253
|
+
prepared.commit();
|
|
1254
|
+
return content === null ? null : { hasCredentials: true, storePath };
|
|
1255
|
+
}
|
|
1256
|
+
function credentialHelperForRemotes(remotes, storePath, desiredFiles) {
|
|
1257
|
+
if (!desiredFiles) {
|
|
1258
|
+
const credentialStore = writeCredentialStoreEntries(remotes, storePath);
|
|
1259
|
+
return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
|
|
1260
|
+
}
|
|
1261
|
+
const resolvedStorePath = path.resolve(storePath);
|
|
1262
|
+
const content = credentialStoreContent(remotes);
|
|
1263
|
+
const previous = desiredFiles.get(resolvedStorePath);
|
|
1264
|
+
if (desiredFiles.has(resolvedStorePath) && previous !== content) {
|
|
1265
|
+
throw new Error(`Conflicting desired Git credentials for ${resolvedStorePath}`);
|
|
1266
|
+
}
|
|
1267
|
+
desiredFiles.set(resolvedStorePath, content);
|
|
1268
|
+
return content === null ? null : readOnlyCredentialStoreHelper(resolvedStorePath);
|
|
1269
|
+
}
|
|
1270
|
+
function authenticatedRemoteCredentialSecret(remoteUrl, authHeader) {
|
|
1271
|
+
if (authHeader == null) return null;
|
|
1272
|
+
let remote;
|
|
1273
|
+
try {
|
|
1274
|
+
remote = new URL(remoteUrl);
|
|
1275
|
+
} catch {
|
|
1276
|
+
throw new Error("Invalid authenticated Git remote URL");
|
|
935
1277
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1278
|
+
if (remote.protocol !== "https:" && remote.protocol !== "http:") {
|
|
1279
|
+
throw new Error("Unsupported authenticated Git remote protocol");
|
|
1280
|
+
}
|
|
1281
|
+
if (remote.username || remote.password) throw new Error("Git credentials must not be embedded in a remote URL");
|
|
1282
|
+
const credentials = decodeGitAuthHeader(authHeader);
|
|
1283
|
+
if (!credentials) throw new Error("Unsupported Git authorization header");
|
|
1284
|
+
return credentials.password;
|
|
1285
|
+
}
|
|
1286
|
+
function credentialMaterialCommitment(kind, values) {
|
|
1287
|
+
return createHash("sha256").update(JSON.stringify([`r5d-credential-${kind}-v1`, ...values])).digest("hex");
|
|
1288
|
+
}
|
|
1289
|
+
function credentialGenerationFingerprint(config) {
|
|
1290
|
+
const workerApiUrl = new URL(config.workerBaseUrl);
|
|
1291
|
+
if (workerApiUrl.protocol !== "https:" && workerApiUrl.protocol !== "http:") {
|
|
1292
|
+
throw new Error("Unsupported worker API protocol");
|
|
1293
|
+
}
|
|
1294
|
+
const workerCredentials = decodeGitAuthHeader(config.workerAuthHeader);
|
|
1295
|
+
if (!workerCredentials) throw new Error("Unsupported worker authorization header");
|
|
1296
|
+
const materialCommitments = /* @__PURE__ */ new Set();
|
|
1297
|
+
const addSecret = (secret) => {
|
|
1298
|
+
materialCommitments.add(`secret:${credentialMaterialCommitment("secret", [secret])}`);
|
|
1299
|
+
};
|
|
1300
|
+
addSecret(workerCredentials.password);
|
|
1301
|
+
const workspaceSecret = authenticatedRemoteCredentialSecret(config.workspaceRemoteUrl, config.workerAuthHeader);
|
|
1302
|
+
if (workspaceSecret) addSecret(workspaceSecret);
|
|
1303
|
+
for (const project of config.projects) {
|
|
1304
|
+
if (!project.repoHttpUrl) continue;
|
|
1305
|
+
const secret = authenticatedRemoteCredentialSecret(project.repoHttpUrl, project.repoAuthHeader);
|
|
1306
|
+
if (secret) addSecret(secret);
|
|
1307
|
+
}
|
|
1308
|
+
if (config.githubCredential) {
|
|
1309
|
+
addSecret(config.githubCredential.token);
|
|
1310
|
+
materialCommitments.add(
|
|
1311
|
+
`github_authority:${credentialMaterialCommitment("github_authority", [
|
|
1312
|
+
config.githubCredential.token,
|
|
1313
|
+
...[...config.githubCredential.missingScopes].sort()
|
|
1314
|
+
])}`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
return createHash("sha256").update(JSON.stringify([...materialCommitments].sort())).digest("hex");
|
|
1318
|
+
}
|
|
1319
|
+
function credentialGenerationTransitionPhase(input) {
|
|
1320
|
+
if (input.committedFingerprint === input.incomingFingerprint) return "current";
|
|
1321
|
+
if (input.pendingFingerprintAtProcessStart !== null) {
|
|
1322
|
+
return input.pendingFingerprintAtProcessStart === input.incomingFingerprint ? "publish_from_clean_restart" : "stage_pending_restart";
|
|
1323
|
+
}
|
|
1324
|
+
return "stage_pending_restart";
|
|
1325
|
+
}
|
|
1326
|
+
function initialCredentialBootstrapIsSafe(input) {
|
|
1327
|
+
return input.transitionPhase === "stage_pending_restart" && input.committedFingerprint === null && input.pendingFingerprintAtProcessStart === null && (input.bootstrapFingerprintAtProcessStart === null && input.credentialArtifactsProvenAbsentAtProcessStart || input.bootstrapFingerprintAtProcessStart === input.incomingFingerprint) && input.trackedChildTerminationProven && input.activeProcessCount === 0 && input.credentialProcessGroupCount === 0 && input.activePtyCount === 0;
|
|
940
1328
|
}
|
|
941
|
-
function
|
|
942
|
-
|
|
943
|
-
|
|
1329
|
+
async function fenceCredentialGenerationAtConfigureEntry(input) {
|
|
1330
|
+
if (input.currentFingerprint === input.incomingFingerprint) return false;
|
|
1331
|
+
input.revokeInMemory();
|
|
1332
|
+
await input.terminatePreviousGeneration();
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
|
|
1336
|
+
if (!required) return;
|
|
1337
|
+
fatalExit(exitCode);
|
|
1338
|
+
throw new Error("Credential generation restart callback returned unexpectedly");
|
|
1339
|
+
}
|
|
1340
|
+
function pendingCredentialGenerationBoundary(input) {
|
|
1341
|
+
if (input.systemdContract && input.systemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.systemdInvocationId)) {
|
|
1342
|
+
return { kind: "systemd", machineId: input.machineId, invocationId: input.systemdInvocationId };
|
|
1343
|
+
}
|
|
1344
|
+
return { kind: "boot", machineId: input.machineId, bootSessionId: input.bootSessionId };
|
|
1345
|
+
}
|
|
1346
|
+
function credentialGenerationBoundaryIsComplete(boundary) {
|
|
1347
|
+
return boundary.machineId !== null && (boundary.kind === "systemd" || boundary.bootSessionId !== null);
|
|
1348
|
+
}
|
|
1349
|
+
function credentialGenerationRestartIsContained(input) {
|
|
1350
|
+
const pending = input.pendingBoundary;
|
|
1351
|
+
if (pending === null || pending.machineId === null || input.currentMachineId === null || pending.machineId !== input.currentMachineId) {
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
if (pending.kind === "systemd") {
|
|
1355
|
+
return input.systemdContract && input.currentSystemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.currentSystemdInvocationId) && pending.invocationId !== input.currentSystemdInvocationId;
|
|
1356
|
+
}
|
|
1357
|
+
return pending.bootSessionId !== null && input.currentBootSessionId !== null && pending.bootSessionId !== input.currentBootSessionId;
|
|
1358
|
+
}
|
|
1359
|
+
function credentialReapContractStatus(probe) {
|
|
1360
|
+
let resolved = probe;
|
|
1361
|
+
if (!resolved) {
|
|
1362
|
+
if (process.platform !== "linux") return "direct";
|
|
1363
|
+
try {
|
|
1364
|
+
const cgroupText = fs.readFileSync("/proc/self/cgroup", "utf8");
|
|
1365
|
+
const unit = cgroupText.match(/(?:^|\/)r5d-worker\.service(?:$|\n)/m)?.[0]?.replace(/^\//, "").trim();
|
|
1366
|
+
if (unit !== "r5d-worker.service") {
|
|
1367
|
+
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
|
|
1368
|
+
}
|
|
1369
|
+
const result = Bun.spawnSync(
|
|
1370
|
+
[
|
|
1371
|
+
"/usr/bin/systemctl",
|
|
1372
|
+
"show",
|
|
1373
|
+
unit,
|
|
1374
|
+
"--no-pager",
|
|
1375
|
+
"--property=KillMode",
|
|
1376
|
+
"--property=Restart",
|
|
1377
|
+
"--property=SendSIGKILL",
|
|
1378
|
+
"--property=InvocationID"
|
|
1379
|
+
],
|
|
1380
|
+
{
|
|
1381
|
+
stdout: "pipe",
|
|
1382
|
+
stderr: "pipe",
|
|
1383
|
+
env: workerGitProcessEnvironment(),
|
|
1384
|
+
timeout: 2e3
|
|
1385
|
+
}
|
|
1386
|
+
);
|
|
1387
|
+
if (result.exitCode !== 0) return "systemd_unverified";
|
|
1388
|
+
resolved = {
|
|
1389
|
+
platform: process.platform,
|
|
1390
|
+
invocationId: process.env.INVOCATION_ID,
|
|
1391
|
+
cgroupText,
|
|
1392
|
+
unitProperties: result.stdout.toString()
|
|
1393
|
+
};
|
|
1394
|
+
} catch {
|
|
1395
|
+
return "systemd_unverified";
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (resolved.platform !== "linux") return "direct";
|
|
1399
|
+
if (!/(?:^|\/)r5d-worker\.service(?:$|\n)/m.test(resolved.cgroupText)) {
|
|
1400
|
+
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
|
|
1401
|
+
}
|
|
1402
|
+
if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
|
|
1403
|
+
const properties = new Map(
|
|
1404
|
+
resolved.unitProperties.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
1405
|
+
const separator = line.indexOf("=");
|
|
1406
|
+
return separator < 1 ? [line, ""] : [line.slice(0, separator), line.slice(separator + 1)];
|
|
1407
|
+
})
|
|
1408
|
+
);
|
|
1409
|
+
return properties.get("KillMode") === "control-group" && properties.get("Restart") === "always" && properties.get("SendSIGKILL") === "yes" && properties.get("InvocationID")?.toLowerCase() === resolved.invocationId?.toLowerCase() ? "verified_systemd" : "systemd_unverified";
|
|
1410
|
+
}
|
|
1411
|
+
function verifiedCredentialReapContract(probe) {
|
|
1412
|
+
return credentialReapContractStatus(probe) === "verified_systemd";
|
|
1413
|
+
}
|
|
1414
|
+
function fenceUnsafeWorkspaceSyncFailure(input) {
|
|
1415
|
+
if (input.hydrationCurrent) return false;
|
|
1416
|
+
input.invalidateExecution();
|
|
1417
|
+
requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
|
|
1418
|
+
return true;
|
|
944
1419
|
}
|
|
945
1420
|
const workerGitSecurityTestHarness = {
|
|
946
1421
|
commandArgs: workerGitCommand.commandArgs,
|
|
@@ -952,7 +1427,45 @@ const workerGitSecurityTestHarness = {
|
|
|
952
1427
|
pruneCredentialStoreFiles,
|
|
953
1428
|
readOnlyCredentialStoreHelper,
|
|
954
1429
|
resetCredentialStoreDirectory,
|
|
955
|
-
replaceCredentialStoreFile
|
|
1430
|
+
replaceCredentialStoreFile,
|
|
1431
|
+
configureGitHubAuth(credential, filePaths, configureFiles = configureGitHubRegistryAuthFiles) {
|
|
1432
|
+
configureGitHubAuth(credential, filePaths, configureFiles);
|
|
1433
|
+
},
|
|
1434
|
+
githubCredential: () => githubCredential,
|
|
1435
|
+
credentialGenerationFingerprint,
|
|
1436
|
+
credentialGenerationTransitionPhase,
|
|
1437
|
+
initialCredentialBootstrapIsSafe,
|
|
1438
|
+
fenceCredentialGenerationAtConfigureEntry,
|
|
1439
|
+
requireCredentialGenerationRestart,
|
|
1440
|
+
pendingCredentialGenerationBoundary,
|
|
1441
|
+
credentialGenerationBoundaryIsComplete,
|
|
1442
|
+
credentialGenerationRestartIsContained,
|
|
1443
|
+
credentialReapContractStatus,
|
|
1444
|
+
verifiedCredentialReapContract,
|
|
1445
|
+
fenceUnsafeWorkspaceSyncFailure,
|
|
1446
|
+
assertWorkerChildAdmission,
|
|
1447
|
+
terminateCredentialBearingChildren,
|
|
1448
|
+
terminateCredentialBearingChildrenWithRetention,
|
|
1449
|
+
async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
|
|
1450
|
+
return await commitCredentialGeneration(
|
|
1451
|
+
prepared,
|
|
1452
|
+
credential,
|
|
1453
|
+
async () => await terminateCredentialBearingChildren(children),
|
|
1454
|
+
beforeMutation,
|
|
1455
|
+
previousGenerationFenced
|
|
1456
|
+
);
|
|
1457
|
+
},
|
|
1458
|
+
credentialGenerationReceiptPath,
|
|
1459
|
+
credentialAuthorityLockPath,
|
|
1460
|
+
acquireCredentialAuthorityLock,
|
|
1461
|
+
releaseCredentialAuthorityLockHandle,
|
|
1462
|
+
pendingCredentialGenerationPath,
|
|
1463
|
+
pendingCredentialGenerationIntentContent,
|
|
1464
|
+
readPendingCredentialGenerationIntent,
|
|
1465
|
+
readBootSessionIdentifier,
|
|
1466
|
+
readMachineIdentifier,
|
|
1467
|
+
bootstrapCredentialGenerationPath,
|
|
1468
|
+
readCredentialGenerationReceipt
|
|
956
1469
|
};
|
|
957
1470
|
function dockerConfigPath() {
|
|
958
1471
|
return path.join(os.homedir(), ".docker", "config.json");
|
|
@@ -960,39 +1473,14 @@ function dockerConfigPath() {
|
|
|
960
1473
|
function containersAuthPath() {
|
|
961
1474
|
return path.join(os.homedir(), ".config", "containers", "auth.json");
|
|
962
1475
|
}
|
|
963
|
-
function
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
try {
|
|
968
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
969
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
970
|
-
} catch {
|
|
971
|
-
return {};
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
function writeRegistryAuthFile(filePath, credential) {
|
|
975
|
-
const config = readJsonFile(filePath);
|
|
976
|
-
const existingAuths = config.auths && typeof config.auths === "object" && !Array.isArray(config.auths) ? config.auths : {};
|
|
977
|
-
const auths = existingAuths;
|
|
978
|
-
auths[credential.registry] = {
|
|
979
|
-
username: credential.username,
|
|
980
|
-
password: credential.token,
|
|
981
|
-
auth: Buffer.from(`${credential.username}:${credential.token}`).toString("base64")
|
|
982
|
-
};
|
|
983
|
-
config.auths = auths;
|
|
984
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
985
|
-
fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
|
|
986
|
-
`, { mode: 384 });
|
|
987
|
-
fs.chmodSync(filePath, 384);
|
|
1476
|
+
function configureGitHubAuth(credential, filePaths = [dockerConfigPath(), containersAuthPath()], configureFiles = configureGitHubRegistryAuthFiles) {
|
|
1477
|
+
githubCredential = null;
|
|
1478
|
+
configureFiles(filePaths, credential);
|
|
1479
|
+
installGitHubCredentialGeneration(credential);
|
|
988
1480
|
}
|
|
989
|
-
function
|
|
1481
|
+
function installGitHubCredentialGeneration(credential) {
|
|
990
1482
|
githubCredential = credential;
|
|
991
|
-
if (!credential)
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
writeRegistryAuthFile(dockerConfigPath(), credential);
|
|
995
|
-
writeRegistryAuthFile(containersAuthPath(), credential);
|
|
1483
|
+
if (!credential) return;
|
|
996
1484
|
if (credential.missingScopes.length > 0) {
|
|
997
1485
|
process.stderr.write(
|
|
998
1486
|
`[r5d-worker] GitHub token is missing scope(s): ${credential.missingScopes.join(", ")}. Sign in with GitHub again if gh or GHCR access fails.
|
|
@@ -1003,6 +1491,21 @@ function configureGitHubAuth(credential) {
|
|
|
1003
1491
|
`);
|
|
1004
1492
|
}
|
|
1005
1493
|
}
|
|
1494
|
+
async function commitCredentialGeneration(prepared, credential, terminatePreviousGeneration, beforeMutation, previousGenerationFenced = false) {
|
|
1495
|
+
const changed = prepared.changed;
|
|
1496
|
+
try {
|
|
1497
|
+
if (changed && !previousGenerationFenced) {
|
|
1498
|
+
githubCredential = null;
|
|
1499
|
+
await terminatePreviousGeneration();
|
|
1500
|
+
}
|
|
1501
|
+
prepared.commit(beforeMutation);
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
prepared.discard();
|
|
1504
|
+
throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
|
|
1505
|
+
}
|
|
1506
|
+
installGitHubCredentialGeneration(credential);
|
|
1507
|
+
return changed;
|
|
1508
|
+
}
|
|
1006
1509
|
function containerRegistryEnv() {
|
|
1007
1510
|
if (!githubCredential) {
|
|
1008
1511
|
return {};
|
|
@@ -1032,6 +1535,44 @@ function githubProcessEnv() {
|
|
|
1032
1535
|
...githubCliEnv(githubCredential?.token)
|
|
1033
1536
|
};
|
|
1034
1537
|
}
|
|
1538
|
+
const INHERITED_WORKER_CREDENTIAL_ENV_KEYS = [
|
|
1539
|
+
WORKER_RUNTIME_ENV,
|
|
1540
|
+
"INVOCATION_ID",
|
|
1541
|
+
"R5D_WORKER_TOKEN",
|
|
1542
|
+
"R5D_TOKEN",
|
|
1543
|
+
"R5DCTL_TOKEN",
|
|
1544
|
+
"R5D_API_KEY",
|
|
1545
|
+
"R5DCTL_API_KEY",
|
|
1546
|
+
"GH_TOKEN",
|
|
1547
|
+
"GITHUB_TOKEN",
|
|
1548
|
+
"GH_ENTERPRISE_TOKEN",
|
|
1549
|
+
"DOCKER_AUTH_CONFIG",
|
|
1550
|
+
"REGISTRY_AUTH_FILE",
|
|
1551
|
+
"R5D_GHCR_AUTH_FILE",
|
|
1552
|
+
"NODE_AUTH_TOKEN",
|
|
1553
|
+
"NPM_TOKEN",
|
|
1554
|
+
"BUN_AUTH_TOKEN"
|
|
1555
|
+
];
|
|
1556
|
+
function workerChildProcessEnvironment(layers, inherited = process.env, platform = process.platform) {
|
|
1557
|
+
const environment = {};
|
|
1558
|
+
const reservedExact = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS);
|
|
1559
|
+
const reservedFolded = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS.map((key) => key.toLowerCase()));
|
|
1560
|
+
const assignLayer = (source, scrubInheritedCredentials) => {
|
|
1561
|
+
for (const [key, value] of Object.entries(source)) {
|
|
1562
|
+
const folded = key.toLowerCase();
|
|
1563
|
+
if (scrubInheritedCredentials && (platform === "win32" ? reservedFolded.has(folded) : reservedExact.has(key))) continue;
|
|
1564
|
+
if (platform === "win32") {
|
|
1565
|
+
for (const existing of Object.keys(environment)) {
|
|
1566
|
+
if (existing.toLowerCase() === folded) delete environment[existing];
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
environment[key] = value;
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
assignLayer(inherited, true);
|
|
1573
|
+
for (const layer of layers) assignLayer(layer, false);
|
|
1574
|
+
return environment;
|
|
1575
|
+
}
|
|
1035
1576
|
function primaryProjectBranch(config) {
|
|
1036
1577
|
if (config.branches.length === 0) throw new Error(`Project ${config.projectId} has no configured branches`);
|
|
1037
1578
|
if (config.branches.some(({ branchName }) => branchName === "main")) return "main";
|
|
@@ -1059,6 +1600,8 @@ function describeWorkerSessionTarget(target) {
|
|
|
1059
1600
|
function resolveWorkerSessionTarget(input) {
|
|
1060
1601
|
if (input.target.type === "workspace") {
|
|
1061
1602
|
if (input.target.rootProfile === "visible_projects") {
|
|
1603
|
+
const disabled = [...input.projectConfigById.values()].find(({ executionDisabled }) => executionDisabled);
|
|
1604
|
+
if (disabled) assertRepositoryExecutionEnabled(disabled);
|
|
1062
1605
|
return { target: input.target, rootPath: input.projectsRoot };
|
|
1063
1606
|
}
|
|
1064
1607
|
if (!fs.existsSync(path.join(input.workspaceShadowRoot, ".git"))) {
|
|
@@ -1069,6 +1612,7 @@ function resolveWorkerSessionTarget(input) {
|
|
|
1069
1612
|
const target = input.target;
|
|
1070
1613
|
const config = input.projectConfigById.get(target.projectId);
|
|
1071
1614
|
if (!config) throw new Error(`Project ${target.projectId} is missing from the worker workspace configuration`);
|
|
1615
|
+
assertRepositoryExecutionEnabled(config);
|
|
1072
1616
|
if (!config.branches.some(({ branchName }) => branchName === target.branchName)) {
|
|
1073
1617
|
throw new Error(`Project branch ${target.projectId}/${target.branchName} is missing from the worker workspace configuration`);
|
|
1074
1618
|
}
|
|
@@ -1768,6 +2312,7 @@ async function prepareShellEnvForTarget(input) {
|
|
|
1768
2312
|
async function executeCommand(input) {
|
|
1769
2313
|
const startedAt = Date.now();
|
|
1770
2314
|
let timeout;
|
|
2315
|
+
let spawnedProcess;
|
|
1771
2316
|
try {
|
|
1772
2317
|
const targetProcessEnv = await prepareShellEnvForTarget({
|
|
1773
2318
|
target: input.resolvedTarget.target,
|
|
@@ -1779,18 +2324,17 @@ async function executeCommand(input) {
|
|
|
1779
2324
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1780
2325
|
});
|
|
1781
2326
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
2327
|
+
input.assertAdmission();
|
|
1782
2328
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1783
2329
|
cwd,
|
|
1784
2330
|
stdout: "pipe",
|
|
1785
2331
|
stderr: "pipe",
|
|
1786
2332
|
detached: true,
|
|
1787
|
-
env: {
|
|
1788
|
-
...process.env,
|
|
1789
|
-
...githubProcessEnv(),
|
|
1790
|
-
...input.message.env ?? {},
|
|
1791
|
-
...targetProcessEnv
|
|
1792
|
-
}
|
|
2333
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
|
|
1793
2334
|
});
|
|
2335
|
+
spawnedProcess = subprocess;
|
|
2336
|
+
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2337
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
1794
2338
|
activeProcesses.set(input.message.runId, {
|
|
1795
2339
|
process: subprocess,
|
|
1796
2340
|
target: input.resolvedTarget.target,
|
|
@@ -1827,6 +2371,7 @@ async function executeCommand(input) {
|
|
|
1827
2371
|
durationMs: Date.now() - startedAt
|
|
1828
2372
|
};
|
|
1829
2373
|
} catch (error) {
|
|
2374
|
+
if (error instanceof StaleWorkerAdmissionError) throw error;
|
|
1830
2375
|
return {
|
|
1831
2376
|
type: "exec_result",
|
|
1832
2377
|
requestId: input.message.requestId,
|
|
@@ -1840,6 +2385,9 @@ async function executeCommand(input) {
|
|
|
1840
2385
|
if (timeout) {
|
|
1841
2386
|
clearTimeout(timeout);
|
|
1842
2387
|
}
|
|
2388
|
+
if (spawnedProcess) {
|
|
2389
|
+
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2390
|
+
}
|
|
1843
2391
|
activeProcesses.delete(input.message.runId);
|
|
1844
2392
|
}
|
|
1845
2393
|
}
|
|
@@ -1848,6 +2396,7 @@ async function executeStreamingCommand(input) {
|
|
|
1848
2396
|
let started = false;
|
|
1849
2397
|
let timedOut = false;
|
|
1850
2398
|
let timeout;
|
|
2399
|
+
let spawnedProcess;
|
|
1851
2400
|
try {
|
|
1852
2401
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1853
2402
|
sendWorkerMessage(input.ws, {
|
|
@@ -1869,6 +2418,7 @@ async function executeStreamingCommand(input) {
|
|
|
1869
2418
|
});
|
|
1870
2419
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
1871
2420
|
const interactive = input.message.interactive === true;
|
|
2421
|
+
input.assertAdmission();
|
|
1872
2422
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1873
2423
|
cwd,
|
|
1874
2424
|
// Without an explicit stdin the process reads /dev/null and interactive
|
|
@@ -1877,13 +2427,11 @@ async function executeStreamingCommand(input) {
|
|
|
1877
2427
|
stdout: "pipe",
|
|
1878
2428
|
stderr: "pipe",
|
|
1879
2429
|
detached: true,
|
|
1880
|
-
env: {
|
|
1881
|
-
...process.env,
|
|
1882
|
-
...githubProcessEnv(),
|
|
1883
|
-
...input.message.env ?? {},
|
|
1884
|
-
...targetProcessEnv
|
|
1885
|
-
}
|
|
2430
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
|
|
1886
2431
|
});
|
|
2432
|
+
spawnedProcess = subprocess;
|
|
2433
|
+
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2434
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
1887
2435
|
activeProcesses.set(input.message.runId, {
|
|
1888
2436
|
process: subprocess,
|
|
1889
2437
|
target: input.resolvedTarget.target,
|
|
@@ -1948,6 +2496,7 @@ async function executeStreamingCommand(input) {
|
|
|
1948
2496
|
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
1949
2497
|
sendWorkerMessage(input.ws, terminal);
|
|
1950
2498
|
} catch (error) {
|
|
2499
|
+
if (!started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
1951
2500
|
const message = error instanceof Error ? error.message : String(error);
|
|
1952
2501
|
if (started) {
|
|
1953
2502
|
const terminal = {
|
|
@@ -1971,10 +2520,27 @@ async function executeStreamingCommand(input) {
|
|
|
1971
2520
|
clearTimeout(timeout);
|
|
1972
2521
|
}
|
|
1973
2522
|
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2523
|
+
if (spawnedProcess) {
|
|
2524
|
+
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2525
|
+
}
|
|
1974
2526
|
activeProcesses.delete(input.message.runId);
|
|
1975
2527
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1976
2528
|
}
|
|
1977
2529
|
}
|
|
2530
|
+
async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
2531
|
+
try {
|
|
2532
|
+
await terminateProcessTree(subprocess);
|
|
2533
|
+
if (credentialBearingProcessGroups.get(subprocess.pid) === subprocess) {
|
|
2534
|
+
credentialBearingProcessGroups.delete(subprocess.pid);
|
|
2535
|
+
credentialBearingProcessGroupTargets.delete(subprocess.pid);
|
|
2536
|
+
}
|
|
2537
|
+
} catch (error) {
|
|
2538
|
+
process.stderr.write(
|
|
2539
|
+
`[r5d-worker] retained unreaped credential-bearing process group ${subprocess.pid} from ${runId}: ${error instanceof Error ? error.message : String(error)}
|
|
2540
|
+
`
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
1978
2544
|
function closeProcessStdin(active) {
|
|
1979
2545
|
if (!active?.stdin) {
|
|
1980
2546
|
return;
|
|
@@ -1994,6 +2560,15 @@ function sendWorkerMessage(ws, message) {
|
|
|
1994
2560
|
`);
|
|
1995
2561
|
}
|
|
1996
2562
|
}
|
|
2563
|
+
function sendWorkerMessageFromCurrentSource(ws, message) {
|
|
2564
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
2565
|
+
try {
|
|
2566
|
+
ws.send(JSON.stringify(message));
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
|
|
2569
|
+
`);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
1997
2572
|
function buildActiveProcessReports() {
|
|
1998
2573
|
return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
|
|
1999
2574
|
runId,
|
|
@@ -2073,6 +2648,19 @@ rl.on("line", (line) => {
|
|
|
2073
2648
|
|
|
2074
2649
|
if (message.type === "close") {
|
|
2075
2650
|
ptyProcess.kill();
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
if (message.type === "force_close") {
|
|
2655
|
+
if (process.platform !== "win32" && Number.isSafeInteger(ptyProcess.pid) && ptyProcess.pid > 0) {
|
|
2656
|
+
try {
|
|
2657
|
+
process.kill(-ptyProcess.pid, "SIGKILL");
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
if (!error || error.code !== "ESRCH") throw error;
|
|
2660
|
+
}
|
|
2661
|
+
} else {
|
|
2662
|
+
ptyProcess.kill("SIGKILL");
|
|
2663
|
+
}
|
|
2076
2664
|
}
|
|
2077
2665
|
} catch (error) {
|
|
2078
2666
|
send({ type: "error", error: error instanceof Error ? error.message : String(error) });
|
|
@@ -2104,14 +2692,26 @@ function nodePtyModulePaths() {
|
|
|
2104
2692
|
}
|
|
2105
2693
|
function createNodePtyBridge(options) {
|
|
2106
2694
|
const child = spawnChildProcess("node", ["-e", PTY_BRIDGE_SCRIPT], {
|
|
2107
|
-
env: {
|
|
2108
|
-
...process.env,
|
|
2109
|
-
NODE_PATH: nodePtyModulePaths()
|
|
2110
|
-
}
|
|
2695
|
+
env: workerChildProcessEnvironment([{ NODE_PATH: nodePtyModulePaths() }])
|
|
2111
2696
|
});
|
|
2112
2697
|
let lineBuffer = "";
|
|
2113
2698
|
let emittedTerminalEvent = false;
|
|
2114
2699
|
let stderr = "";
|
|
2700
|
+
let resolveTerminal;
|
|
2701
|
+
const terminal = new Promise((resolve) => {
|
|
2702
|
+
resolveTerminal = resolve;
|
|
2703
|
+
});
|
|
2704
|
+
const markTerminal = () => {
|
|
2705
|
+
resolveTerminal?.();
|
|
2706
|
+
resolveTerminal = void 0;
|
|
2707
|
+
};
|
|
2708
|
+
const terminalWithin = async (timeoutMs) => await Promise.race([
|
|
2709
|
+
terminal.then(() => true),
|
|
2710
|
+
new Promise((resolve) => {
|
|
2711
|
+
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
2712
|
+
timer.unref();
|
|
2713
|
+
})
|
|
2714
|
+
]);
|
|
2115
2715
|
const sendToChild = (message) => {
|
|
2116
2716
|
if (child.stdin.writable) {
|
|
2117
2717
|
child.stdin.write(`${JSON.stringify(message)}
|
|
@@ -2129,11 +2729,13 @@ function createNodePtyBridge(options) {
|
|
|
2129
2729
|
}
|
|
2130
2730
|
if (event.type === "exit") {
|
|
2131
2731
|
emittedTerminalEvent = true;
|
|
2732
|
+
markTerminal();
|
|
2132
2733
|
options.onExit({ exitCode: event.exitCode, signal: event.signal });
|
|
2133
2734
|
return;
|
|
2134
2735
|
}
|
|
2135
2736
|
if (event.type === "error") {
|
|
2136
2737
|
emittedTerminalEvent = true;
|
|
2738
|
+
markTerminal();
|
|
2137
2739
|
options.onError(new Error(event.error));
|
|
2138
2740
|
}
|
|
2139
2741
|
};
|
|
@@ -2162,9 +2764,11 @@ function createNodePtyBridge(options) {
|
|
|
2162
2764
|
});
|
|
2163
2765
|
child.on("error", (error) => {
|
|
2164
2766
|
emittedTerminalEvent = true;
|
|
2767
|
+
markTerminal();
|
|
2165
2768
|
options.onError(error);
|
|
2166
2769
|
});
|
|
2167
2770
|
child.on("exit", (code, signal) => {
|
|
2771
|
+
markTerminal();
|
|
2168
2772
|
if (emittedTerminalEvent) {
|
|
2169
2773
|
return;
|
|
2170
2774
|
}
|
|
@@ -2185,12 +2789,15 @@ function createNodePtyBridge(options) {
|
|
|
2185
2789
|
sendToChild({ type: "resize", cols, rows });
|
|
2186
2790
|
},
|
|
2187
2791
|
kill() {
|
|
2792
|
+
void this.terminate().catch((error) => options.onTerminationError(error instanceof Error ? error : new Error(String(error))));
|
|
2793
|
+
},
|
|
2794
|
+
async terminate() {
|
|
2188
2795
|
sendToChild({ type: "close" });
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2796
|
+
if (await terminalWithin(500)) return;
|
|
2797
|
+
sendToChild({ type: "force_close" });
|
|
2798
|
+
if (await terminalWithin(500)) return;
|
|
2799
|
+
child.kill("SIGKILL");
|
|
2800
|
+
if (!await terminalWithin(500)) throw new Error(`PTY bridge ${child.pid ?? "unknown"} did not terminate after SIGKILL`);
|
|
2194
2801
|
}
|
|
2195
2802
|
};
|
|
2196
2803
|
}
|
|
@@ -2228,6 +2835,7 @@ async function openPty(input) {
|
|
|
2228
2835
|
});
|
|
2229
2836
|
}
|
|
2230
2837
|
});
|
|
2838
|
+
input.assertAdmission();
|
|
2231
2839
|
const ptyProcess = createNodePtyBridge({
|
|
2232
2840
|
file: shell.file,
|
|
2233
2841
|
args: shell.args,
|
|
@@ -2236,14 +2844,7 @@ async function openPty(input) {
|
|
|
2236
2844
|
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
2237
2845
|
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
2238
2846
|
cwd: input.resolvedTarget.rootPath,
|
|
2239
|
-
env: {
|
|
2240
|
-
...process.env,
|
|
2241
|
-
...githubProcessEnv(),
|
|
2242
|
-
...input.message.env ?? {},
|
|
2243
|
-
...planProcessEnv,
|
|
2244
|
-
...targetProcessEnv,
|
|
2245
|
-
...shell.env ?? {}
|
|
2246
|
-
}
|
|
2847
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, planProcessEnv, targetProcessEnv, shell.env ?? {}])
|
|
2247
2848
|
},
|
|
2248
2849
|
onOpened: () => {
|
|
2249
2850
|
sendWorkerMessage(input.ws, {
|
|
@@ -2278,6 +2879,14 @@ async function openPty(input) {
|
|
|
2278
2879
|
ptyId: input.message.ptyId,
|
|
2279
2880
|
error: error.message
|
|
2280
2881
|
});
|
|
2882
|
+
},
|
|
2883
|
+
onTerminationError: (error) => {
|
|
2884
|
+
sendWorkerMessage(input.ws, {
|
|
2885
|
+
type: "pty_error",
|
|
2886
|
+
requestId: input.message.requestId,
|
|
2887
|
+
ptyId: input.message.ptyId,
|
|
2888
|
+
error: error.message
|
|
2889
|
+
});
|
|
2281
2890
|
}
|
|
2282
2891
|
});
|
|
2283
2892
|
activePtys.set(input.message.ptyId, {
|
|
@@ -2310,14 +2919,62 @@ function closePty(message) {
|
|
|
2310
2919
|
if (!ptyProcess) {
|
|
2311
2920
|
return;
|
|
2312
2921
|
}
|
|
2313
|
-
activePtys.delete(message.ptyId);
|
|
2314
2922
|
ptyProcess.kill();
|
|
2315
2923
|
}
|
|
2316
2924
|
function closeAllPtys() {
|
|
2317
2925
|
for (const ptyProcess of activePtys.values()) {
|
|
2318
2926
|
ptyProcess.kill();
|
|
2319
2927
|
}
|
|
2320
|
-
|
|
2928
|
+
}
|
|
2929
|
+
async function terminateCredentialBearingChildren(children) {
|
|
2930
|
+
const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
|
|
2931
|
+
const failures = results.flatMap(
|
|
2932
|
+
(result, index) => result.status === "rejected" ? [new Error(`Failed to terminate ${children[index].label}`, { cause: result.reason })] : []
|
|
2933
|
+
);
|
|
2934
|
+
if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
|
|
2935
|
+
}
|
|
2936
|
+
async function terminateCredentialBearingChildrenWithRetention(children) {
|
|
2937
|
+
const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
|
|
2938
|
+
const failures = [];
|
|
2939
|
+
results.forEach((result, index) => {
|
|
2940
|
+
const child = children[index];
|
|
2941
|
+
if (result.status === "fulfilled") {
|
|
2942
|
+
child.onTerminated();
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
failures.push(new Error(`Failed to terminate ${child.label}`, { cause: result.reason }));
|
|
2946
|
+
});
|
|
2947
|
+
if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
|
|
2948
|
+
}
|
|
2949
|
+
async function terminateActiveCredentialBearingChildren() {
|
|
2950
|
+
const processGroups = [...credentialBearingProcessGroups.entries()];
|
|
2951
|
+
const ptyEntries = [...activePtys.entries()];
|
|
2952
|
+
for (const active of activeProcesses.values()) closeProcessStdin(active);
|
|
2953
|
+
await terminateCredentialBearingChildrenWithRetention([
|
|
2954
|
+
...processGroups.map(([processGroupId, subprocess]) => ({
|
|
2955
|
+
label: `process group ${processGroupId}`,
|
|
2956
|
+
terminate: async () => await terminateProcessTree(subprocess),
|
|
2957
|
+
onTerminated: () => {
|
|
2958
|
+
if (credentialBearingProcessGroups.get(processGroupId) === subprocess) {
|
|
2959
|
+
credentialBearingProcessGroups.delete(processGroupId);
|
|
2960
|
+
credentialBearingProcessGroupTargets.delete(processGroupId);
|
|
2961
|
+
}
|
|
2962
|
+
for (const [runId, active] of activeProcesses) {
|
|
2963
|
+
if (active.process === subprocess) activeProcesses.delete(runId);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
})),
|
|
2967
|
+
...ptyEntries.map(([ptyId, active]) => ({
|
|
2968
|
+
label: `PTY ${ptyId}`,
|
|
2969
|
+
terminate: async () => await active.terminate(),
|
|
2970
|
+
onTerminated: () => {
|
|
2971
|
+
if (activePtys.get(ptyId) === active) activePtys.delete(ptyId);
|
|
2972
|
+
}
|
|
2973
|
+
}))
|
|
2974
|
+
]);
|
|
2975
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || credentialBearingProcessGroupTargets.size > 0 || activePtys.size > 0) {
|
|
2976
|
+
throw new Error("Credential-bearing child ownership remained after termination");
|
|
2977
|
+
}
|
|
2321
2978
|
}
|
|
2322
2979
|
function websocketUrl(baseUrl, label) {
|
|
2323
2980
|
const url = new URL("/worker/ws", baseUrl);
|
|
@@ -2327,6 +2984,7 @@ function websocketUrl(baseUrl, label) {
|
|
|
2327
2984
|
return url.toString();
|
|
2328
2985
|
}
|
|
2329
2986
|
async function startWorker(options) {
|
|
2987
|
+
advanceWorkerAdmissionGeneration();
|
|
2330
2988
|
process.on("uncaughtException", (error) => {
|
|
2331
2989
|
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
2332
2990
|
`);
|
|
@@ -2345,8 +3003,15 @@ async function startWorker(options) {
|
|
|
2345
3003
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
2346
3004
|
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
2347
3005
|
const workspaceShadowRoot = path.join(syncRoot, "workspace");
|
|
3006
|
+
const projectWorktreeSnapshotsRoot = path.join(syncRoot, "project-worktree-snapshots");
|
|
2348
3007
|
const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2349
3008
|
const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
3009
|
+
assertDisjointManagedRoots([
|
|
3010
|
+
{ label: "projects root", rootPath: projectsRoot },
|
|
3011
|
+
{ label: "sync root", rootPath: syncRoot },
|
|
3012
|
+
{ label: "artifacts root", rootPath: artifactRoot },
|
|
3013
|
+
{ label: "plans root", rootPath: planRoot }
|
|
3014
|
+
]);
|
|
2350
3015
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
2351
3016
|
`);
|
|
2352
3017
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
@@ -2359,26 +3024,43 @@ async function startWorker(options) {
|
|
|
2359
3024
|
`);
|
|
2360
3025
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
2361
3026
|
`);
|
|
2362
|
-
const snapshotCleanup = cleanupStaleProjectWorktreeSnapshots();
|
|
2363
|
-
if (snapshotCleanup.removed.length > 0) {
|
|
2364
|
-
process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
|
|
2365
|
-
`);
|
|
2366
|
-
}
|
|
2367
|
-
for (const failure of snapshotCleanup.failed) {
|
|
2368
|
-
process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
|
|
2369
|
-
`);
|
|
2370
|
-
}
|
|
2371
3027
|
runGit(["--version"]);
|
|
2372
3028
|
await verifyR5dctlAuth(baseUrl, token);
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
3029
|
+
const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3030
|
+
if (!startupProjectSnapshotRecoveryCompleted) {
|
|
3031
|
+
const snapshotRecovery = recoverStaleProjectWorktreeSnapshots({
|
|
3032
|
+
projectsRoot,
|
|
3033
|
+
temporaryRoot: projectWorktreeSnapshotsRoot
|
|
3034
|
+
});
|
|
3035
|
+
if (snapshotRecovery.restored.length > 0) {
|
|
3036
|
+
process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
|
|
3037
|
+
`);
|
|
3038
|
+
}
|
|
3039
|
+
if (snapshotRecovery.removed.length > 0) {
|
|
3040
|
+
process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
|
|
3041
|
+
`);
|
|
3042
|
+
}
|
|
3043
|
+
for (const failure of snapshotRecovery.failed) {
|
|
3044
|
+
process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
|
|
3045
|
+
`);
|
|
3046
|
+
}
|
|
3047
|
+
if (snapshotRecovery.failed.length > 0) {
|
|
3048
|
+
throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
|
|
3049
|
+
}
|
|
3050
|
+
startupProjectSnapshotRecoveryCompleted = true;
|
|
3051
|
+
}
|
|
3052
|
+
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
3053
|
+
fs.mkdirSync(syncRoot, { recursive: true });
|
|
3054
|
+
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
3055
|
+
fs.mkdirSync(planRoot, { recursive: true });
|
|
3056
|
+
const store = new ProjectWorkspaceStateStore({
|
|
3057
|
+
stateRoot: path.join(syncRoot, "project-state"),
|
|
3058
|
+
projectsRoot
|
|
3059
|
+
});
|
|
3060
|
+
return { store, state: store.read() };
|
|
2380
3061
|
});
|
|
2381
|
-
|
|
3062
|
+
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3063
|
+
let projectWorkspaceState = initializedWorkspaceState.state;
|
|
2382
3064
|
const projectConfigById = /* @__PURE__ */ new Map();
|
|
2383
3065
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
2384
3066
|
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
@@ -2394,6 +3076,7 @@ async function startWorker(options) {
|
|
|
2394
3076
|
let workspaceAutomaticTimer;
|
|
2395
3077
|
let workspacePeriodicTimer;
|
|
2396
3078
|
let pendingAutomaticTrigger;
|
|
3079
|
+
const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
|
|
2397
3080
|
let automaticSyncInFlight = false;
|
|
2398
3081
|
let terminalReplayTimer;
|
|
2399
3082
|
let workspaceSyncRequestsInFlight = 0;
|
|
@@ -2408,7 +3091,7 @@ async function startWorker(options) {
|
|
|
2408
3091
|
const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
|
|
2409
3092
|
const workspaceProjectRelativePath = (projectId, branchName) => path.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
|
|
2410
3093
|
const workspacePlanRelativePath = (projectId, branchName) => path.posix.join("plans", projectId, encodeURIComponent(branchName));
|
|
2411
|
-
const projectConnection = (project) => {
|
|
3094
|
+
const projectConnection = (project, desiredFiles) => {
|
|
2412
3095
|
const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
|
|
2413
3096
|
const originUrl = project.repoHttpUrl ?? mirrorUrl;
|
|
2414
3097
|
const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
|
|
@@ -2417,7 +3100,8 @@ async function startWorker(options) {
|
|
|
2417
3100
|
{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader },
|
|
2418
3101
|
{ remoteUrl: originUrl, authHeader: originHeader }
|
|
2419
3102
|
],
|
|
2420
|
-
credentialStorePathForProject(project.projectId)
|
|
3103
|
+
credentialStorePathForProject(project.projectId),
|
|
3104
|
+
desiredFiles
|
|
2421
3105
|
);
|
|
2422
3106
|
if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
|
|
2423
3107
|
return {
|
|
@@ -2428,11 +3112,12 @@ async function startWorker(options) {
|
|
|
2428
3112
|
credentialHelper
|
|
2429
3113
|
};
|
|
2430
3114
|
};
|
|
2431
|
-
const projectMirrorConnection = (projectId) => {
|
|
3115
|
+
const projectMirrorConnection = (projectId, desiredFiles) => {
|
|
2432
3116
|
const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, projectId);
|
|
2433
3117
|
const credentialHelper = credentialHelperForRemotes(
|
|
2434
3118
|
[{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader }],
|
|
2435
|
-
credentialStorePathForProject(projectId)
|
|
3119
|
+
credentialStorePathForProject(projectId),
|
|
3120
|
+
desiredFiles
|
|
2436
3121
|
);
|
|
2437
3122
|
if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
|
|
2438
3123
|
return { mirrorUrl, credentialHelper, mirrorCredentialUsername: workerCredentialUsername };
|
|
@@ -2505,6 +3190,29 @@ async function startWorker(options) {
|
|
|
2505
3190
|
readyProjectIds.delete(project.projectId);
|
|
2506
3191
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
2507
3192
|
};
|
|
3193
|
+
const deactivatePreserveOnlyProjectBranch = (project, branchName) => {
|
|
3194
|
+
const branchPath = configuredProjectBranchPath(projectsRoot, project, branchName);
|
|
3195
|
+
pendingCheckouts.delete(projectBranchKey(project.projectId, branchName));
|
|
3196
|
+
if (!fs.existsSync(branchPath)) return;
|
|
3197
|
+
if (!hasProjectWorktree(branchPath)) {
|
|
3198
|
+
throw new Error(`Preserve-only project branch path ${project.projectId}/${branchName} is not a managed Git checkout`);
|
|
3199
|
+
}
|
|
3200
|
+
if (project.branches.length === 0) {
|
|
3201
|
+
throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} without an active anchor branch`);
|
|
3202
|
+
}
|
|
3203
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
3204
|
+
if (branchName === primaryBranchName) {
|
|
3205
|
+
throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
|
|
3206
|
+
}
|
|
3207
|
+
deleteLinkedProjectBranch({
|
|
3208
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3209
|
+
primaryBranchName,
|
|
3210
|
+
branchName
|
|
3211
|
+
});
|
|
3212
|
+
fs.rmSync(path.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
|
|
3213
|
+
readyProjectIds.delete(project.projectId);
|
|
3214
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
3215
|
+
};
|
|
2508
3216
|
const cleanupConfigForDurableDeletion = (deletion) => {
|
|
2509
3217
|
const desiredLayout = projectWorkspaceState.desiredProjects.find(
|
|
2510
3218
|
(project) => project.projectId === deletion.projectId && project.checkoutPathSegments[0] === deletion.checkoutPathSegments[0] && project.checkoutPathSegments[1] === deletion.checkoutPathSegments[1]
|
|
@@ -2519,8 +3227,13 @@ async function startWorker(options) {
|
|
|
2519
3227
|
repoHttpUrl: liveConfig?.repoHttpUrl ?? null,
|
|
2520
3228
|
repoAuthHeader: liveConfig?.repoAuthHeader ?? null,
|
|
2521
3229
|
defaultBranch,
|
|
3230
|
+
repositoryTransitionId: liveConfig?.repositoryTransitionId ?? null,
|
|
3231
|
+
executionDisabled: false,
|
|
3232
|
+
mirrorWritesDisabled: false,
|
|
3233
|
+
preserveOnlyBranches: [],
|
|
2522
3234
|
branches: branchNames.map(
|
|
2523
3235
|
(branchName) => liveConfig?.branches.find((branch) => branch.branchName === branchName) ?? {
|
|
3236
|
+
branchId: "deleted",
|
|
2524
3237
|
branchName,
|
|
2525
3238
|
sourceBranchName: null,
|
|
2526
3239
|
// Deletion never consumes the baseline, but retain a structurally
|
|
@@ -2535,9 +3248,18 @@ async function startWorker(options) {
|
|
|
2535
3248
|
const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
|
|
2536
3249
|
if (deletion.kind === "project") {
|
|
2537
3250
|
if (!deletion.projectDeleted) {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
3251
|
+
const nextProject = projectConfigById.get(deletion.projectId);
|
|
3252
|
+
if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
|
|
3253
|
+
preserveProjectCheckoutPathMove({
|
|
3254
|
+
oldProjectRoot: deletion.managedPath,
|
|
3255
|
+
newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
|
|
3256
|
+
projectsDurabilityRoot: projectsRoot,
|
|
3257
|
+
outerWorkspaceRoot: workspaceShadowRoot,
|
|
3258
|
+
branches: deletion.branchNames.map((branchName) => ({
|
|
3259
|
+
branchName,
|
|
3260
|
+
outerWorkspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, branchName)
|
|
3261
|
+
}))
|
|
3262
|
+
});
|
|
2541
3263
|
readyProjectIds.delete(deletion.projectId);
|
|
2542
3264
|
reconciledProjectConfigFingerprints.delete(deletion.projectId);
|
|
2543
3265
|
continue;
|
|
@@ -2570,32 +3292,15 @@ async function startWorker(options) {
|
|
|
2570
3292
|
});
|
|
2571
3293
|
}
|
|
2572
3294
|
};
|
|
2573
|
-
const pruneStaleOuterWorkspaceEntries = (entries) => {
|
|
2574
|
-
const remove = (candidate) => {
|
|
2575
|
-
if (candidate) fs.rmSync(candidate, { recursive: true, force: true });
|
|
2576
|
-
};
|
|
2577
|
-
for (const entry of entries) {
|
|
2578
|
-
if (entry.kind === "branch") {
|
|
2579
|
-
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
2580
|
-
projectId: entry.projectId,
|
|
2581
|
-
branchName: entry.branchName
|
|
2582
|
-
});
|
|
2583
|
-
remove(entry.projectTreePath);
|
|
2584
|
-
remove(entry.planTreePath);
|
|
2585
|
-
continue;
|
|
2586
|
-
}
|
|
2587
|
-
remove(entry.projectTreePath);
|
|
2588
|
-
remove(entry.planTreePath);
|
|
2589
|
-
for (const branch of entry.branches) {
|
|
2590
|
-
remove(branch.projectTreePath);
|
|
2591
|
-
remove(branch.planTreePath);
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
};
|
|
2595
3295
|
const effectiveWorkerProjects = (projects) => {
|
|
2596
3296
|
const incomingById = new Map(projects.map((project) => [project.projectId, project]));
|
|
2597
|
-
const
|
|
2598
|
-
|
|
3297
|
+
const preserveOnlyByKey = new Map(
|
|
3298
|
+
projects.flatMap(
|
|
3299
|
+
(project) => project.preserveOnlyBranches.map((branch) => [projectBranchKey(project.projectId, branch.branchName), branch])
|
|
3300
|
+
)
|
|
3301
|
+
);
|
|
3302
|
+
const pending = new Map(
|
|
3303
|
+
projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [projectBranchKey(branch.projectId, branch.branchName), branch])
|
|
2599
3304
|
);
|
|
2600
3305
|
return projectWorkspaceState.effectiveDesiredProjects.flatMap((layout) => {
|
|
2601
3306
|
const incoming = incomingById.get(layout.projectId);
|
|
@@ -2603,10 +3308,21 @@ async function startWorker(options) {
|
|
|
2603
3308
|
const existing = projectConfigById.get(layout.projectId);
|
|
2604
3309
|
const branches = layout.branches.flatMap(({ branchName }) => {
|
|
2605
3310
|
const configured = incoming.branches.find((branch) => branch.branchName === branchName);
|
|
2606
|
-
|
|
3311
|
+
const pendingBranch = pending.get(projectBranchKey(layout.projectId, branchName));
|
|
3312
|
+
const preservedBranch = preserveOnlyByKey.get(projectBranchKey(layout.projectId, branchName));
|
|
3313
|
+
const pendingIncarnationIsAuthorized = creatorLocalProjectBranchIsAuthorized({
|
|
3314
|
+
pendingBranchId: pendingBranch?.branchId,
|
|
3315
|
+
preserveOnlyBranchId: preservedBranch?.branchId
|
|
3316
|
+
});
|
|
3317
|
+
const disposition = workerProjectBranchDisposition({
|
|
3318
|
+
activeConfigured: Boolean(configured),
|
|
3319
|
+
locallyPendingCreated: pendingIncarnationIsAuthorized
|
|
3320
|
+
});
|
|
3321
|
+
if (configured && disposition === "active") return [configured];
|
|
3322
|
+
if (disposition === "preserve_only") return [];
|
|
2607
3323
|
const retained = existing?.branches.find((branch) => branch.branchName === branchName);
|
|
2608
3324
|
if (retained) return [retained];
|
|
2609
|
-
if (!
|
|
3325
|
+
if (!pendingBranch?.branchId) return [];
|
|
2610
3326
|
const branchPath = configuredProjectBranchPath(
|
|
2611
3327
|
projectsRoot,
|
|
2612
3328
|
{ ...incoming, checkoutPathSegments: layout.checkoutPathSegments },
|
|
@@ -2615,6 +3331,7 @@ async function startWorker(options) {
|
|
|
2615
3331
|
if (!hasProjectWorktree(branchPath)) return [];
|
|
2616
3332
|
return [
|
|
2617
3333
|
{
|
|
3334
|
+
branchId: pendingBranch.branchId,
|
|
2618
3335
|
branchName,
|
|
2619
3336
|
sourceBranchName: null,
|
|
2620
3337
|
baseCommitHash: runGit(["rev-parse", "HEAD"], { cwd: branchPath })
|
|
@@ -2624,41 +3341,94 @@ async function startWorker(options) {
|
|
|
2624
3341
|
return [{ ...incoming, checkoutPathSegments: [...layout.checkoutPathSegments], branches }];
|
|
2625
3342
|
});
|
|
2626
3343
|
};
|
|
2627
|
-
const
|
|
3344
|
+
const activeWorkspaceMutationTargets = () => [
|
|
3345
|
+
...Array.from(activeProcesses.values(), ({ target }) => target),
|
|
3346
|
+
...credentialBearingProcessGroupTargets.values(),
|
|
3347
|
+
...workspaceSyncPriorityProcessTargets.values(),
|
|
3348
|
+
...workspaceSyncPriorityOperationTargets.values(),
|
|
3349
|
+
...Array.from(activePtys.values(), ({ target }) => target),
|
|
3350
|
+
...workspaceSyncPriorityPtyTargets.values()
|
|
3351
|
+
];
|
|
3352
|
+
const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
|
|
3353
|
+
const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
|
|
3354
|
+
const activeTargets = activeWorkspaceMutationTargets();
|
|
3355
|
+
return projectConfigById.get(projectId)?.executionDisabled === true || hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath) || canonicalWorkspaceMutationIsActive(activeTargets) || projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTargets);
|
|
3356
|
+
};
|
|
3357
|
+
const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
|
|
3358
|
+
const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
|
|
2628
3359
|
const mounts = [];
|
|
2629
|
-
|
|
3360
|
+
const creatorLocalIncarnations = new Map(
|
|
3361
|
+
projectWorkspaceState.locallyPendingCreatedBranches.flatMap(
|
|
3362
|
+
(branch) => branch.branchId ? [[projectBranchKey(branch.projectId, branch.branchName), branch.branchId]] : []
|
|
3363
|
+
)
|
|
3364
|
+
);
|
|
3365
|
+
const checkoutPathMoveBranches = new Set(
|
|
3366
|
+
projectWorkspaceState.pendingTreeDeletions.flatMap(
|
|
3367
|
+
(deletion) => deletion.kind === "project" && !deletion.projectDeleted ? deletion.branchNames.map((branchName) => projectBranchKey(deletion.projectId, branchName)) : []
|
|
3368
|
+
)
|
|
3369
|
+
);
|
|
3370
|
+
for (const project of [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
|
|
2630
3371
|
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2631
3372
|
for (const branch of [...project.branches].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
2632
3373
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
3374
|
+
const branchKey = projectBranchKey(project.projectId, branch.branchName);
|
|
3375
|
+
const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
|
|
3376
|
+
const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
|
|
2633
3377
|
mounts.push({
|
|
2634
3378
|
id: projectMountId(project.projectId, branch.branchName),
|
|
3379
|
+
hydrationIncarnationKey: branch.branchId,
|
|
2635
3380
|
sourcePath: branchPath,
|
|
3381
|
+
durabilityRootPath: projectsRoot,
|
|
2636
3382
|
workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
|
|
2637
3383
|
sourceMode: "git",
|
|
2638
3384
|
hydrateDeletionMode: "git",
|
|
2639
|
-
|
|
3385
|
+
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3386
|
+
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3387
|
+
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3388
|
+
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
2640
3389
|
});
|
|
2641
3390
|
mounts.push({
|
|
2642
3391
|
id: projectPlanMountId(project.projectId, branch.branchName),
|
|
3392
|
+
hydrationIncarnationKey: branch.branchId,
|
|
2643
3393
|
sourcePath: path.join(planRoot, project.projectId, ...branch.branchName.split("/")),
|
|
3394
|
+
durabilityRootPath: planRoot,
|
|
2644
3395
|
workspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
|
|
2645
3396
|
sourceMode: "all",
|
|
2646
|
-
hydrateDeletionMode: "all"
|
|
3397
|
+
hydrateDeletionMode: "all",
|
|
3398
|
+
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3399
|
+
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3400
|
+
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3401
|
+
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
2647
3402
|
});
|
|
2648
3403
|
}
|
|
2649
3404
|
void projectRoot;
|
|
2650
3405
|
}
|
|
2651
3406
|
mounts.push({
|
|
2652
3407
|
id: "workspace-plans",
|
|
3408
|
+
hydrationIncarnationKey: "workspace-plans-v1",
|
|
2653
3409
|
sourcePath: path.join(planRoot, "workspace"),
|
|
3410
|
+
durabilityRootPath: planRoot,
|
|
2654
3411
|
workspaceRelativePath: "workspace-plans",
|
|
2655
3412
|
sourceMode: "all",
|
|
2656
|
-
hydrateDeletionMode: "all"
|
|
3413
|
+
hydrateDeletionMode: "all",
|
|
3414
|
+
// Workspace-level agents receive this directory through R5D_PLANS_DIR.
|
|
3415
|
+
// Keep both projection and hydration away from it while any visible-
|
|
3416
|
+
// projects process, PTY, or built-in write/edit is active or reserved.
|
|
3417
|
+
busy: () => {
|
|
3418
|
+
const activeTargets = activeWorkspaceMutationTargets();
|
|
3419
|
+
return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
|
|
3420
|
+
},
|
|
3421
|
+
busyForRecovery: () => {
|
|
3422
|
+
const activeTargets = activeWorkspaceMutationTargets();
|
|
3423
|
+
return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
|
|
3424
|
+
}
|
|
2657
3425
|
});
|
|
2658
3426
|
for (const deletion of pendingMirrorDeletes.values()) {
|
|
2659
3427
|
mounts.push({
|
|
2660
3428
|
id: deletion.id,
|
|
3429
|
+
hydrationIncarnationKey: deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`,
|
|
2661
3430
|
sourcePath: deletion.sourcePath,
|
|
3431
|
+
durabilityRootPath: projectsRoot,
|
|
2662
3432
|
workspaceRelativePath: deletion.workspaceRelativePath,
|
|
2663
3433
|
sourceMode: "git",
|
|
2664
3434
|
hydrateDeletionMode: "git",
|
|
@@ -2666,7 +3436,9 @@ async function startWorker(options) {
|
|
|
2666
3436
|
});
|
|
2667
3437
|
mounts.push({
|
|
2668
3438
|
id: `${deletion.id}:plan`,
|
|
3439
|
+
hydrationIncarnationKey: `${deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`}:plan`,
|
|
2669
3440
|
sourcePath: deletion.planSourcePath,
|
|
3441
|
+
durabilityRootPath: planRoot,
|
|
2670
3442
|
workspaceRelativePath: deletion.planWorkspaceRelativePath,
|
|
2671
3443
|
sourceMode: "all",
|
|
2672
3444
|
hydrateDeletionMode: "all",
|
|
@@ -2694,10 +3466,34 @@ async function startWorker(options) {
|
|
|
2694
3466
|
const ensureConfiguredProjects = () => {
|
|
2695
3467
|
for (const project of projectConfigById.values()) {
|
|
2696
3468
|
validateProjectId(project.projectId);
|
|
3469
|
+
assertRepositoryTransitionState(project);
|
|
2697
3470
|
for (const branch of project.branches) validateBranchName(branch.branchName);
|
|
3471
|
+
if (project.branches.length === 0) {
|
|
3472
|
+
readyProjectIds.delete(project.projectId);
|
|
3473
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
3474
|
+
continue;
|
|
3475
|
+
}
|
|
3476
|
+
if (project.executionDisabled) {
|
|
3477
|
+
readyProjectIds.delete(project.projectId);
|
|
3478
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
3479
|
+
for (const branch of project.branches) {
|
|
3480
|
+
pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
|
|
3481
|
+
}
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
2698
3484
|
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2699
3485
|
const configFingerprint = projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity });
|
|
2700
|
-
const
|
|
3486
|
+
const durableEnabledTransition = projectWorkspaceState.enabledRepositoryTransitions.find(
|
|
3487
|
+
({ projectId }) => projectId === project.projectId
|
|
3488
|
+
);
|
|
3489
|
+
const transitionReseedRequired = enabledRepositoryTransitionRequiresReseed({
|
|
3490
|
+
project,
|
|
3491
|
+
lastEnabled: {
|
|
3492
|
+
known: Boolean(durableEnabledTransition),
|
|
3493
|
+
transitionId: durableEnabledTransition?.transitionId ?? null
|
|
3494
|
+
}
|
|
3495
|
+
});
|
|
3496
|
+
const alreadyReady = !transitionReseedRequired && readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
|
|
2701
3497
|
if (alreadyReady) {
|
|
2702
3498
|
for (const branch of project.branches) pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
|
|
2703
3499
|
continue;
|
|
@@ -2706,8 +3502,11 @@ async function startWorker(options) {
|
|
|
2706
3502
|
const connection = projectConnection(project);
|
|
2707
3503
|
const states = ensureProjectWorktrees({
|
|
2708
3504
|
projectRoot,
|
|
3505
|
+
snapshotRoot: projectWorktreeSnapshotsRoot,
|
|
2709
3506
|
primaryBranchName: primaryProjectBranch(project),
|
|
2710
3507
|
branches: project.branches,
|
|
3508
|
+
defaultBranch: project.defaultBranch,
|
|
3509
|
+
...transitionReseedRequired && project.repositoryTransitionId !== null ? { exactMirrorBranches: /* @__PURE__ */ new Set([primaryProjectBranch(project)]) } : {},
|
|
2711
3510
|
originUrl: connection.originUrl,
|
|
2712
3511
|
originCredentialUsername: connection.originCredentialUsername,
|
|
2713
3512
|
mirrorUrl: connection.mirrorUrl,
|
|
@@ -2720,7 +3519,7 @@ async function startWorker(options) {
|
|
|
2720
3519
|
for (const state of states) {
|
|
2721
3520
|
pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
|
|
2722
3521
|
const key = projectBranchKey(project.projectId, state.branchName);
|
|
2723
|
-
if (!lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
|
|
3522
|
+
if (transitionReseedRequired || !lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
|
|
2724
3523
|
if ((state.ahead ?? 0) > 0) {
|
|
2725
3524
|
process.stderr.write(
|
|
2726
3525
|
`[r5d-worker] ${project.projectPath}/${state.branchName} is ${state.ahead} commit(s) ahead of origin; switching workers may require publishing or reconciling those commits
|
|
@@ -2728,6 +3527,12 @@ async function startWorker(options) {
|
|
|
2728
3527
|
);
|
|
2729
3528
|
}
|
|
2730
3529
|
}
|
|
3530
|
+
if (transitionReseedRequired) {
|
|
3531
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
3532
|
+
projectId: project.projectId,
|
|
3533
|
+
transitionId: project.repositoryTransitionId
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
2731
3536
|
} catch (error) {
|
|
2732
3537
|
readyProjectIds.delete(project.projectId);
|
|
2733
3538
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
@@ -2747,7 +3552,7 @@ async function startWorker(options) {
|
|
|
2747
3552
|
const collectAheadOfOriginBranches = () => {
|
|
2748
3553
|
const aheadBranches = [];
|
|
2749
3554
|
for (const project of projectConfigById.values()) {
|
|
2750
|
-
if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
3555
|
+
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
2751
3556
|
const connection = projectConnection(project);
|
|
2752
3557
|
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
|
|
2753
3558
|
if (!tryGit(
|
|
@@ -2769,7 +3574,11 @@ async function startWorker(options) {
|
|
|
2769
3574
|
if (!hasProjectWorktree(branchPath)) continue;
|
|
2770
3575
|
try {
|
|
2771
3576
|
let ahead = 0;
|
|
2772
|
-
const originRef = `refs/remotes/origin/${
|
|
3577
|
+
const originRef = `refs/remotes/origin/${projectOriginBranchName({
|
|
3578
|
+
branchName: branch.branchName,
|
|
3579
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
3580
|
+
defaultBranch: project.defaultBranch
|
|
3581
|
+
})}`;
|
|
2773
3582
|
if (tryGit(["show-ref", "--verify", "--quiet", originRef], { cwd: branchPath })) {
|
|
2774
3583
|
const counts = runGit(["rev-list", "--left-right", "--count", `${originRef}...HEAD`], { cwd: branchPath }).split(/\s+/).map(Number);
|
|
2775
3584
|
ahead = Number.isSafeInteger(counts[1]) ? counts[1] : 0;
|
|
@@ -2790,18 +3599,30 @@ async function startWorker(options) {
|
|
|
2790
3599
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
2791
3600
|
);
|
|
2792
3601
|
};
|
|
2793
|
-
const
|
|
2794
|
-
const
|
|
3602
|
+
const captureProjectHeadsForWorkspacePublication = () => {
|
|
3603
|
+
const heads = /* @__PURE__ */ new Map();
|
|
2795
3604
|
for (const project of projectConfigById.values()) {
|
|
2796
3605
|
if (!readyProjectIds.has(project.projectId)) continue;
|
|
2797
|
-
const changed = /* @__PURE__ */ new Set();
|
|
2798
3606
|
for (const branch of project.branches) {
|
|
2799
|
-
if (!active.has(projectMountId(project.projectId, branch.branchName))) continue;
|
|
2800
3607
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
2801
3608
|
if (!hasProjectWorktree(branchPath)) continue;
|
|
2802
|
-
|
|
3609
|
+
heads.set(projectMountId(project.projectId, branch.branchName), runGit(["rev-parse", "HEAD"], { cwd: branchPath }));
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
return heads;
|
|
3613
|
+
};
|
|
3614
|
+
const pushChangedProjectHeads = (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
3615
|
+
const active = new Set(activeMountIds);
|
|
3616
|
+
for (const project of projectConfigById.values()) {
|
|
3617
|
+
if (!readyProjectIds.has(project.projectId) || !repositoryMirrorWritesAllowed(project)) continue;
|
|
3618
|
+
const changed = /* @__PURE__ */ new Map();
|
|
3619
|
+
for (const branch of project.branches) {
|
|
3620
|
+
const mountId = projectMountId(project.projectId, branch.branchName);
|
|
3621
|
+
if (!active.has(mountId) || suppressedMountIds.has(mountId)) continue;
|
|
3622
|
+
const head = publicationHeads.get(mountId);
|
|
3623
|
+
if (!head) throw new Error(`Missing pre-projection head for active project mount ${mountId}`);
|
|
2803
3624
|
if (lastObservedProjectHeads.get(projectBranchKey(project.projectId, branch.branchName)) !== head) {
|
|
2804
|
-
changed.
|
|
3625
|
+
changed.set(branch.branchName, head);
|
|
2805
3626
|
}
|
|
2806
3627
|
}
|
|
2807
3628
|
if (changed.size === 0) continue;
|
|
@@ -2812,10 +3633,12 @@ async function startWorker(options) {
|
|
|
2812
3633
|
mirrorUrl: connection.mirrorUrl,
|
|
2813
3634
|
credentialHelper: connection.credentialHelper,
|
|
2814
3635
|
credentialUsername: connection.mirrorCredentialUsername,
|
|
2815
|
-
onlyBranches: changed
|
|
3636
|
+
onlyBranches: new Set(changed.keys()),
|
|
3637
|
+
publicationHeads: changed
|
|
2816
3638
|
});
|
|
3639
|
+
assertProjectMirrorHeadsPushed(results);
|
|
2817
3640
|
for (const result of results) {
|
|
2818
|
-
|
|
3641
|
+
lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
|
|
2819
3642
|
}
|
|
2820
3643
|
}
|
|
2821
3644
|
const publishedTombstoneIds = [
|
|
@@ -2833,6 +3656,7 @@ async function startWorker(options) {
|
|
|
2833
3656
|
}
|
|
2834
3657
|
for (const [key, deletion] of pendingMirrorDeletes) {
|
|
2835
3658
|
if (!active.has(deletion.id)) continue;
|
|
3659
|
+
if (!repositoryMirrorWritesAllowed(projectConfigById.get(deletion.projectId))) continue;
|
|
2836
3660
|
if (!deletion.projectDeleted) {
|
|
2837
3661
|
deleteProjectMirrorBranch({
|
|
2838
3662
|
gitDirectory: deletion.gitDirectory,
|
|
@@ -2855,73 +3679,105 @@ async function startWorker(options) {
|
|
|
2855
3679
|
lastObservedProjectHeads.delete(key);
|
|
2856
3680
|
}
|
|
2857
3681
|
};
|
|
2858
|
-
const
|
|
3682
|
+
const observeProjectHeadsBeforeOuterWorkspace = (input) => {
|
|
3683
|
+
const bundles = [];
|
|
2859
3684
|
for (const project of projectConfigById.values()) {
|
|
2860
|
-
if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
3685
|
+
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
2861
3686
|
try {
|
|
2862
3687
|
const connection = projectConnection(project);
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3688
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
3689
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
3690
|
+
bundles.push({
|
|
3691
|
+
projectId: project.projectId,
|
|
3692
|
+
projectRoot,
|
|
3693
|
+
primaryBranchName,
|
|
3694
|
+
observations: observeProjectMirrorHeads({
|
|
3695
|
+
projectRoot,
|
|
3696
|
+
primaryBranchName,
|
|
3697
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
3698
|
+
mirrorUrl: connection.mirrorUrl,
|
|
3699
|
+
credentialHelper: connection.credentialHelper,
|
|
3700
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
3701
|
+
allowNonFastForward: input.allowNonFastForward
|
|
3702
|
+
})
|
|
2870
3703
|
});
|
|
3704
|
+
} catch (error) {
|
|
3705
|
+
if (input.failClosed) throw error;
|
|
3706
|
+
process.stderr.write(
|
|
3707
|
+
`[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
3708
|
+
`
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
return bundles;
|
|
3713
|
+
};
|
|
3714
|
+
const applyObservedProjectHeadsAfterInboundWorkspace = (bundles, activeMountIds, requireObservedHeads) => {
|
|
3715
|
+
const active = new Set(activeMountIds);
|
|
3716
|
+
for (const bundle of bundles) {
|
|
3717
|
+
const project = projectConfigById.get(bundle.projectId);
|
|
3718
|
+
if (!project || project.executionDisabled || !readyProjectIds.has(project.projectId)) continue;
|
|
3719
|
+
const observations = bundle.observations.filter(({ branchName }) => active.has(projectMountId(project.projectId, branchName)));
|
|
3720
|
+
if (observations.length === 0) continue;
|
|
3721
|
+
try {
|
|
3722
|
+
const refreshed = applyObservedProjectMirrorHeads({
|
|
3723
|
+
projectRoot: bundle.projectRoot,
|
|
3724
|
+
primaryBranchName: bundle.primaryBranchName,
|
|
3725
|
+
observations,
|
|
3726
|
+
branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
|
|
3727
|
+
});
|
|
3728
|
+
if (requireObservedHeads) {
|
|
3729
|
+
for (const observation of observations) {
|
|
3730
|
+
if (!observation.shouldMove || !observation.mirrorHead) continue;
|
|
3731
|
+
const checkoutPath = configuredProjectBranchPath(projectsRoot, project, observation.branchName);
|
|
3732
|
+
const currentHead = runGit(["rev-parse", "HEAD"], { cwd: checkoutPath });
|
|
3733
|
+
if (currentHead !== observation.mirrorHead) {
|
|
3734
|
+
throw new Error(
|
|
3735
|
+
`Project ${project.projectId}/${observation.branchName} did not apply observed hidden mirror head ${observation.mirrorHead}`
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
2871
3740
|
for (const state of refreshed) {
|
|
2872
3741
|
lastObservedProjectHeads.set(
|
|
2873
3742
|
projectBranchKey(project.projectId, state.branchName),
|
|
2874
|
-
state.
|
|
3743
|
+
state.moved && state.mirrorHead ? state.mirrorHead : state.previousHead
|
|
2875
3744
|
);
|
|
2876
3745
|
}
|
|
2877
3746
|
} catch (error) {
|
|
3747
|
+
if (requireObservedHeads) throw error;
|
|
2878
3748
|
process.stderr.write(
|
|
2879
|
-
`[r5d-worker] failed to
|
|
3749
|
+
`[r5d-worker] failed to apply observed project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
2880
3750
|
`
|
|
2881
3751
|
);
|
|
2882
3752
|
}
|
|
2883
3753
|
}
|
|
2884
3754
|
};
|
|
2885
|
-
const
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3755
|
+
const mapWorkspaceGitResult = (attemptId, trigger, result) => {
|
|
3756
|
+
const activeProjectBranchPublications = activeProjectBranchPublicationEvidence([...projectConfigById.values()], result.activeMountIds);
|
|
3757
|
+
return {
|
|
3758
|
+
type: "workspace_sync",
|
|
3759
|
+
attemptId,
|
|
3760
|
+
workerLabel: label,
|
|
3761
|
+
trigger,
|
|
3762
|
+
outcome: result.outcome === "pushed" ? "published" : result.outcome,
|
|
3763
|
+
startingHead: result.startingHead,
|
|
3764
|
+
...result.localHead ? { localHead: result.localHead } : {},
|
|
3765
|
+
...result.publishedHead ? { publishedHead: result.publishedHead } : {},
|
|
3766
|
+
rebaseCount: result.rebaseCount,
|
|
3767
|
+
diffSizeBytes: result.diffSizeBytes,
|
|
3768
|
+
gitStatus: workspaceGitStatus(),
|
|
3769
|
+
affectedProjects: affectedProjects(result.affectedPaths),
|
|
3770
|
+
affectedPaths: result.affectedPaths,
|
|
3771
|
+
activeMountIds: result.activeMountIds,
|
|
3772
|
+
skippedMountIds: result.skippedMountIds,
|
|
3773
|
+
activeProjectBranchPublications,
|
|
3774
|
+
discardedPaths: [],
|
|
3775
|
+
localChangesDiscarded: false,
|
|
3776
|
+
...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
|
|
3777
|
+
...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
|
|
3778
|
+
...result.error ? { error: result.error } : {}
|
|
3779
|
+
};
|
|
2904
3780
|
};
|
|
2905
|
-
const mapWorkspaceGitResult = (attemptId, trigger, result) => ({
|
|
2906
|
-
type: "workspace_sync",
|
|
2907
|
-
attemptId,
|
|
2908
|
-
workerLabel: label,
|
|
2909
|
-
trigger,
|
|
2910
|
-
outcome: result.outcome === "pushed" ? "published" : result.outcome,
|
|
2911
|
-
startingHead: result.startingHead,
|
|
2912
|
-
...result.localHead ? { localHead: result.localHead } : {},
|
|
2913
|
-
...result.publishedHead ? { publishedHead: result.publishedHead } : {},
|
|
2914
|
-
rebaseCount: result.rebaseCount,
|
|
2915
|
-
diffSizeBytes: result.diffSizeBytes,
|
|
2916
|
-
gitStatus: workspaceGitStatus(),
|
|
2917
|
-
affectedProjects: affectedProjects(result.affectedPaths),
|
|
2918
|
-
affectedPaths: result.affectedPaths,
|
|
2919
|
-
discardedPaths: [],
|
|
2920
|
-
localChangesDiscarded: false,
|
|
2921
|
-
...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
|
|
2922
|
-
...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
|
|
2923
|
-
...result.error ? { error: result.error } : {}
|
|
2924
|
-
});
|
|
2925
3781
|
const failedWorkspaceSyncResult = (attemptId, trigger, error) => ({
|
|
2926
3782
|
type: "workspace_sync",
|
|
2927
3783
|
attemptId,
|
|
@@ -2945,6 +3801,16 @@ async function startWorker(options) {
|
|
|
2945
3801
|
}
|
|
2946
3802
|
const mounts = buildWorkspaceMounts();
|
|
2947
3803
|
if (input.resetToCanonical) {
|
|
3804
|
+
const resetHasBusyLiveMount = mounts.some(
|
|
3805
|
+
(mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
|
|
3806
|
+
);
|
|
3807
|
+
if (resetHasBusyLiveMount) {
|
|
3808
|
+
throw new Error("Workspace reset cannot observe project mirror heads while a live mount is busy; remediation remains active");
|
|
3809
|
+
}
|
|
3810
|
+
const observedProjectHeads2 = observeProjectHeadsBeforeOuterWorkspace({
|
|
3811
|
+
allowNonFastForward: true,
|
|
3812
|
+
failClosed: true
|
|
3813
|
+
});
|
|
2948
3814
|
const reset = resetWorkspaceGit({
|
|
2949
3815
|
workspacePath: workspaceShadowRoot,
|
|
2950
3816
|
remoteUrl: workspaceRemoteUrl,
|
|
@@ -2953,7 +3819,7 @@ async function startWorker(options) {
|
|
|
2953
3819
|
gitIdentity: workspaceGitIdentity,
|
|
2954
3820
|
mounts
|
|
2955
3821
|
});
|
|
2956
|
-
|
|
3822
|
+
applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
|
|
2957
3823
|
return {
|
|
2958
3824
|
type: "workspace_sync",
|
|
2959
3825
|
attemptId: input.attemptId,
|
|
@@ -2968,11 +3834,23 @@ async function startWorker(options) {
|
|
|
2968
3834
|
gitStatus: workspaceGitStatus(),
|
|
2969
3835
|
affectedProjects: affectedProjects(reset.discardedPaths),
|
|
2970
3836
|
affectedPaths: reset.discardedPaths,
|
|
3837
|
+
activeMountIds: reset.activeMountIds,
|
|
3838
|
+
skippedMountIds: reset.skippedMountIds,
|
|
2971
3839
|
discardedPaths: reset.discardedPaths,
|
|
2972
3840
|
localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
|
|
2973
3841
|
};
|
|
2974
3842
|
}
|
|
2975
3843
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
3844
|
+
const ordinaryCycleHasBusyLiveMount = mounts.some(
|
|
3845
|
+
(mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
|
|
3846
|
+
);
|
|
3847
|
+
const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
3848
|
+
const inboundMoveMountIds = new Set(
|
|
3849
|
+
observedProjectHeads.flatMap(
|
|
3850
|
+
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
3851
|
+
)
|
|
3852
|
+
);
|
|
3853
|
+
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : captureProjectHeadsForWorkspacePublication();
|
|
2976
3854
|
const result = await synchronizeWorkspaceGit({
|
|
2977
3855
|
attemptId: input.attemptId,
|
|
2978
3856
|
workerLabel: label,
|
|
@@ -2985,13 +3863,12 @@ async function startWorker(options) {
|
|
|
2985
3863
|
commitDetail: input.confirmationReason ?? input.trigger.detail,
|
|
2986
3864
|
allowLargeDiff: input.confirmedLargeDiff,
|
|
2987
3865
|
skipMountMirror: outerRemediation,
|
|
2988
|
-
skipMountHydration: outerRemediation,
|
|
2989
3866
|
afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
|
|
2990
|
-
pushChangedProjectHeads(activeMountIds, publishedHead);
|
|
3867
|
+
pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
|
|
2991
3868
|
}
|
|
2992
3869
|
});
|
|
2993
3870
|
if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
|
|
2994
|
-
|
|
3871
|
+
applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
|
|
2995
3872
|
}
|
|
2996
3873
|
return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
2997
3874
|
};
|
|
@@ -3012,6 +3889,12 @@ async function startWorker(options) {
|
|
|
3012
3889
|
const runWorkspaceSync = async (input) => {
|
|
3013
3890
|
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
3014
3891
|
const requestedAt = Date.now();
|
|
3892
|
+
if (!input.requestId && input.sendResult !== false) {
|
|
3893
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
3894
|
+
throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
|
|
3895
|
+
}
|
|
3896
|
+
ws.send(JSON.stringify({ type: "workspace_sync_started", attemptId }));
|
|
3897
|
+
}
|
|
3015
3898
|
let queueEnteredAt = requestedAt;
|
|
3016
3899
|
let syncStartedAt = requestedAt;
|
|
3017
3900
|
workspaceSyncRequestsInFlight += 1;
|
|
@@ -3029,6 +3912,28 @@ async function startWorker(options) {
|
|
|
3029
3912
|
resetToCanonical: input.resetToCanonical
|
|
3030
3913
|
});
|
|
3031
3914
|
} catch (error) {
|
|
3915
|
+
let hydrationCurrent = input.resetToCanonical !== true;
|
|
3916
|
+
if (hydrationCurrent) {
|
|
3917
|
+
try {
|
|
3918
|
+
hydrationCurrent = workspaceGitHydrationIsCurrent(workspaceShadowRoot, buildWorkspaceMounts());
|
|
3919
|
+
} catch {
|
|
3920
|
+
hydrationCurrent = false;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
if (!hydrationCurrent) {
|
|
3924
|
+
process.stderr.write(
|
|
3925
|
+
`[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
|
|
3926
|
+
`
|
|
3927
|
+
);
|
|
3928
|
+
}
|
|
3929
|
+
fenceUnsafeWorkspaceSyncFailure({
|
|
3930
|
+
hydrationCurrent,
|
|
3931
|
+
invalidateExecution: () => {
|
|
3932
|
+
workspaceConfigured = false;
|
|
3933
|
+
advanceWorkerAdmissionGeneration();
|
|
3934
|
+
},
|
|
3935
|
+
exitCode: 1
|
|
3936
|
+
});
|
|
3032
3937
|
return failedWorkspaceSyncResult(attemptId, input.trigger, error);
|
|
3033
3938
|
}
|
|
3034
3939
|
});
|
|
@@ -3050,7 +3955,13 @@ async function startWorker(options) {
|
|
|
3050
3955
|
};
|
|
3051
3956
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
3052
3957
|
pendingAutomaticTrigger = trigger;
|
|
3053
|
-
|
|
3958
|
+
const pendingBranchDeferral = () => pendingCreatedBranchAutomaticSyncDeferral(
|
|
3959
|
+
projectWorkspaceState.locallyPendingCreatedBranches,
|
|
3960
|
+
activeWorkspaceMutationTargets(),
|
|
3961
|
+
pendingCreatedBranchPublicationNotBefore
|
|
3962
|
+
);
|
|
3963
|
+
const initialDeferral = pendingBranchDeferral();
|
|
3964
|
+
if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
|
|
3054
3965
|
return;
|
|
3055
3966
|
}
|
|
3056
3967
|
if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
|
|
@@ -3058,13 +3969,25 @@ async function startWorker(options) {
|
|
|
3058
3969
|
() => {
|
|
3059
3970
|
workspaceAutomaticTimer = void 0;
|
|
3060
3971
|
const scheduledTrigger = pendingAutomaticTrigger;
|
|
3061
|
-
pendingAutomaticTrigger = void 0;
|
|
3062
3972
|
if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
|
|
3973
|
+
const currentDeferral = pendingBranchDeferral();
|
|
3974
|
+
if (currentDeferral?.kind === "active_target") return;
|
|
3975
|
+
if (currentDeferral?.kind === "creation_grace") {
|
|
3976
|
+
scheduleAutomaticWorkspaceSync(scheduledTrigger, currentDeferral.retryAfterMs);
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
pendingAutomaticTrigger = void 0;
|
|
3063
3980
|
automaticSyncInFlight = true;
|
|
3064
3981
|
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
3065
3982
|
if (result.outcome === "failed") {
|
|
3066
3983
|
pendingAutomaticTrigger = scheduledTrigger;
|
|
3067
3984
|
}
|
|
3985
|
+
}).catch((error) => {
|
|
3986
|
+
pendingAutomaticTrigger = scheduledTrigger;
|
|
3987
|
+
process.stderr.write(
|
|
3988
|
+
`[r5d-worker] failed to start automatic workspace sync: ${error instanceof Error ? error.message : String(error)}
|
|
3989
|
+
`
|
|
3990
|
+
);
|
|
3068
3991
|
}).finally(() => {
|
|
3069
3992
|
automaticSyncInFlight = false;
|
|
3070
3993
|
heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
@@ -3073,7 +3996,7 @@ async function startWorker(options) {
|
|
|
3073
3996
|
}
|
|
3074
3997
|
});
|
|
3075
3998
|
},
|
|
3076
|
-
Math.max(0, delayMs)
|
|
3999
|
+
Math.max(0, delayMs, initialDeferral?.kind === "creation_grace" ? initialDeferral.retryAfterMs : 0)
|
|
3077
4000
|
);
|
|
3078
4001
|
workspaceAutomaticTimer.unref();
|
|
3079
4002
|
};
|
|
@@ -3081,32 +4004,190 @@ async function startWorker(options) {
|
|
|
3081
4004
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
3082
4005
|
};
|
|
3083
4006
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
3084
|
-
const resolveMessageTarget = (target) =>
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
4007
|
+
const resolveMessageTarget = (target) => {
|
|
4008
|
+
if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
|
|
4009
|
+
if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
|
|
4010
|
+
throw new Error(`Project ${target.projectId} is not ready on this worker`);
|
|
4011
|
+
}
|
|
4012
|
+
if (target.type === "workspace" && target.rootProfile === "visible_projects" && [...projectConfigById.keys()].some((projectId) => !readyProjectIds.has(projectId))) {
|
|
4013
|
+
throw new Error("Visible project execution is unavailable while project configuration is incomplete");
|
|
4014
|
+
}
|
|
4015
|
+
return resolveWorkerSessionTarget({
|
|
4016
|
+
target,
|
|
4017
|
+
projectsRoot,
|
|
4018
|
+
workspaceShadowRoot,
|
|
4019
|
+
projectConfigById
|
|
4020
|
+
});
|
|
4021
|
+
};
|
|
3090
4022
|
const configureWorkerWorkspace = async (message) => {
|
|
4023
|
+
const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
|
|
4024
|
+
...message,
|
|
4025
|
+
workerBaseUrl: baseUrl,
|
|
4026
|
+
workerAuthHeader: bearerAuthHeader
|
|
4027
|
+
});
|
|
4028
|
+
const credentialTransitionPhase = credentialGenerationTransitionPhase({
|
|
4029
|
+
committedFingerprint: configuredCredentialGenerationFingerprint,
|
|
4030
|
+
pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
|
|
4031
|
+
incomingFingerprint: incomingCredentialGenerationFingerprint
|
|
4032
|
+
});
|
|
4033
|
+
const credentialReapStatus = credentialReapContractStatus();
|
|
4034
|
+
const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
|
|
4035
|
+
const credentialRestartIsContained = credentialGenerationRestartIsContained({
|
|
4036
|
+
systemdContract: verifiedCredentialReapContractNow,
|
|
4037
|
+
pendingBoundary: pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null,
|
|
4038
|
+
currentSystemdInvocationId: systemdInvocationIdentifierAtProcessStart,
|
|
4039
|
+
currentBootSessionId: bootSessionIdentifierAtProcessStart,
|
|
4040
|
+
currentMachineId: machineIdentifierAtProcessStart
|
|
4041
|
+
});
|
|
4042
|
+
let trackedChildTerminationProven = true;
|
|
4043
|
+
if (credentialTransitionPhase !== "current") {
|
|
4044
|
+
workspaceConfigured = false;
|
|
4045
|
+
githubCredential = null;
|
|
4046
|
+
advanceWorkerAdmissionGeneration();
|
|
4047
|
+
try {
|
|
4048
|
+
await terminateActiveCredentialBearingChildren();
|
|
4049
|
+
} catch (error) {
|
|
4050
|
+
trackedChildTerminationProven = false;
|
|
4051
|
+
process.stderr.write(
|
|
4052
|
+
`[r5d-worker] tracked credential-bearing child termination was incomplete at the credential transition boundary: ${error instanceof Error ? error.message : String(error)}
|
|
4053
|
+
`
|
|
4054
|
+
);
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
const initialBootstrapAuthorized = initialCredentialBootstrapIsSafe({
|
|
4058
|
+
transitionPhase: credentialTransitionPhase,
|
|
4059
|
+
committedFingerprint: configuredCredentialGenerationFingerprint,
|
|
4060
|
+
pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
|
|
4061
|
+
bootstrapFingerprintAtProcessStart: bootstrapCredentialGenerationFingerprintAtProcessStart,
|
|
4062
|
+
incomingFingerprint: incomingCredentialGenerationFingerprint,
|
|
4063
|
+
credentialArtifactsProvenAbsentAtProcessStart: initialCredentialBootstrapProvenEmpty,
|
|
4064
|
+
trackedChildTerminationProven,
|
|
4065
|
+
activeProcessCount: activeProcesses.size,
|
|
4066
|
+
credentialProcessGroupCount: credentialBearingProcessGroups.size,
|
|
4067
|
+
activePtyCount: activePtys.size
|
|
4068
|
+
});
|
|
4069
|
+
const staleIntentCleanupAuthorized = credentialTransitionPhase === "current" && (pendingCredentialGenerationIntentAtProcessStart !== null || bootstrapCredentialGenerationFingerprintAtProcessStart !== null);
|
|
4070
|
+
const credentialPublicationPreauthorized = credentialTransitionPhase === "publish_from_clean_restart" && credentialRestartIsContained || initialBootstrapAuthorized || staleIntentCleanupAuthorized;
|
|
4071
|
+
if (initialBootstrapAuthorized && bootstrapCredentialGenerationFingerprintAtProcessStart === null) {
|
|
4072
|
+
try {
|
|
4073
|
+
const bootstrap = preparePrivateAuthFileGeneration([
|
|
4074
|
+
{ filePath: bootstrapCredentialGenerationPath(), content: `${incomingCredentialGenerationFingerprint}
|
|
4075
|
+
` }
|
|
4076
|
+
]);
|
|
4077
|
+
bootstrap.commit();
|
|
4078
|
+
bootstrapCredentialGenerationFingerprintAtProcessStart = incomingCredentialGenerationFingerprint;
|
|
4079
|
+
} catch (error) {
|
|
4080
|
+
const wrapped = error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
|
|
4081
|
+
return {
|
|
4082
|
+
result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, wrapped),
|
|
4083
|
+
pending: [],
|
|
4084
|
+
aheadOfOriginBranches: []
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
if (credentialTransitionPhase === "stage_pending_restart" && !initialBootstrapAuthorized) {
|
|
4089
|
+
if (credentialReapStatus === "systemd_unverified") {
|
|
4090
|
+
process.stderr.write(
|
|
4091
|
+
"[r5d-worker] this process is inside r5d-worker.service but its credential-reaping contract could not be verified; no pending credential generation was written and the service may retry safely\n"
|
|
4092
|
+
);
|
|
4093
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4094
|
+
}
|
|
4095
|
+
const boundary = pendingCredentialGenerationBoundary({
|
|
4096
|
+
systemdContract: verifiedCredentialReapContractNow,
|
|
4097
|
+
systemdInvocationId: systemdInvocationIdentifierAtProcessStart,
|
|
4098
|
+
bootSessionId: bootSessionIdentifierAtProcessStart,
|
|
4099
|
+
machineId: machineIdentifierAtProcessStart
|
|
4100
|
+
});
|
|
4101
|
+
if (!credentialGenerationBoundaryIsComplete(boundary)) {
|
|
4102
|
+
process.stderr.write(
|
|
4103
|
+
process.platform === "win32" ? "[r5d-worker] credential rotation is unavailable for a direct Windows worker because no authoritative per-boot identifier is supported; move it behind the verified systemd contract or reprovision a clean worker host\n" : "[r5d-worker] could not attest this worker's stable machine and boot identifiers; no pending credential generation was written\n"
|
|
4104
|
+
);
|
|
4105
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4106
|
+
}
|
|
4107
|
+
try {
|
|
4108
|
+
const pending = preparePrivateAuthFileGeneration([
|
|
4109
|
+
{
|
|
4110
|
+
filePath: pendingCredentialGenerationPath(),
|
|
4111
|
+
content: pendingCredentialGenerationIntentContent({
|
|
4112
|
+
fingerprint: incomingCredentialGenerationFingerprint,
|
|
4113
|
+
boundary
|
|
4114
|
+
})
|
|
4115
|
+
}
|
|
4116
|
+
]);
|
|
4117
|
+
pending.commit();
|
|
4118
|
+
} catch (error) {
|
|
4119
|
+
process.stderr.write(
|
|
4120
|
+
`[r5d-worker] could not stage pending credential generation before fatal restart: ${error instanceof Error ? error.message : String(error)}
|
|
4121
|
+
`
|
|
4122
|
+
);
|
|
4123
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4124
|
+
}
|
|
4125
|
+
process.stderr.write(
|
|
4126
|
+
verifiedCredentialReapContractNow ? "[r5d-worker] credential transition staged without secrets; restarting through the service cgroup boundary\n" : "[r5d-worker] credential transition staged without secrets; reboot this worker host or VM before starting r5d-worker again\n"
|
|
4127
|
+
);
|
|
4128
|
+
requireCredentialGenerationRestart(true, void 0, WORKER_CREDENTIAL_RESTART_EXIT_CODE);
|
|
4129
|
+
}
|
|
4130
|
+
if (credentialTransitionPhase !== "current" && !trackedChildTerminationProven) {
|
|
4131
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4132
|
+
}
|
|
4133
|
+
if (credentialTransitionPhase === "publish_from_clean_restart" && !credentialRestartIsContained) {
|
|
4134
|
+
const pendingBoundary = pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null;
|
|
4135
|
+
const detail = pendingBoundary?.machineId === null || machineIdentifierAtProcessStart === null ? "credential transition cannot prove it is still on the same worker host; keep the credential directory host-local and ensure the OS machine identifier is available" : pendingBoundary?.kind === "systemd" ? "credential transition is waiting for a new verified r5d-worker.service invocation on the same host" : pendingBoundary?.bootSessionId === null || bootSessionIdentifierAtProcessStart === null ? "credential transition cannot prove a portable whole-host restart; ensure its kernel boot identifier is available" : "credential transition is still in the boot session that staged it";
|
|
4136
|
+
process.stderr.write(
|
|
4137
|
+
`[r5d-worker] ${detail}; no new credential bytes were published. ${pendingBoundary?.kind === "boot" ? "Reboot the entire worker host or VM before starting r5d-worker again." : "The verified systemd unit must start a new invocation before retrying."}
|
|
4138
|
+
`
|
|
4139
|
+
);
|
|
4140
|
+
requireCredentialGenerationRestart(true, void 0, WORKER_CREDENTIAL_RESTART_EXIT_CODE);
|
|
4141
|
+
}
|
|
4142
|
+
if (credentialPublicationPreauthorized) workspaceConfigured = false;
|
|
3091
4143
|
workspaceSyncRequestsInFlight += 1;
|
|
3092
4144
|
try {
|
|
3093
4145
|
return await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
4146
|
+
for (const project of message.projects) assertRepositoryTransitionState(project);
|
|
4147
|
+
const busyConfigurationChanges = busyProjectConfigurationChangeIds({
|
|
4148
|
+
currentProjects: [...projectConfigById.values()],
|
|
4149
|
+
incomingProjects: message.projects,
|
|
4150
|
+
activeTargets: activeWorkspaceMutationTargets(),
|
|
4151
|
+
currentFingerprint: (project) => projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity }),
|
|
4152
|
+
incomingFingerprint: (project) => projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: message.gitIdentity }),
|
|
4153
|
+
currentProjectReady: (project) => {
|
|
4154
|
+
const fingerprint = projectWorktreeConfigurationFingerprint({ ...project, gitIdentity: workspaceGitIdentity });
|
|
4155
|
+
return readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === fingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
|
|
4156
|
+
}
|
|
4157
|
+
});
|
|
4158
|
+
if (busyConfigurationChanges.length > 0) {
|
|
4159
|
+
throw new ProjectWorkspaceConfigurationDeferredError(
|
|
4160
|
+
`Project workspace configuration deferred while visible checkout activity is active: ${busyConfigurationChanges.join(", ")}`
|
|
4161
|
+
);
|
|
4162
|
+
}
|
|
3094
4163
|
workspaceConfigured = false;
|
|
4164
|
+
const preserveOnlyBranches = message.projects.flatMap(
|
|
4165
|
+
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
4166
|
+
);
|
|
3095
4167
|
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
3096
|
-
desiredProjects: message.projects
|
|
4168
|
+
desiredProjects: message.projects,
|
|
4169
|
+
preserveOnlyBranches
|
|
3097
4170
|
});
|
|
3098
|
-
const
|
|
4171
|
+
const stillPendingCreatedBranches = new Map(
|
|
4172
|
+
projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
|
|
4173
|
+
pendingCreatedBranchKey(branch.projectId, branch.branchName),
|
|
4174
|
+
branch
|
|
4175
|
+
])
|
|
4176
|
+
);
|
|
4177
|
+
for (const key of pendingCreatedBranchPublicationNotBefore.keys()) {
|
|
4178
|
+
if (!stillPendingCreatedBranches.has(key)) pendingCreatedBranchPublicationNotBefore.delete(key);
|
|
4179
|
+
}
|
|
4180
|
+
const desiredCredentialStoreFiles = /* @__PURE__ */ new Map();
|
|
3099
4181
|
const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
|
|
3100
|
-
requiredCredentialStorePaths.add(workspaceStorePath);
|
|
3101
4182
|
const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
|
|
3102
4183
|
[{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
|
|
3103
|
-
workspaceStorePath
|
|
4184
|
+
workspaceStorePath,
|
|
4185
|
+
desiredCredentialStoreFiles
|
|
3104
4186
|
);
|
|
3105
4187
|
if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
|
|
3106
4188
|
const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
|
|
3107
4189
|
for (const project of message.projects) {
|
|
3108
|
-
|
|
3109
|
-
projectConnection(project);
|
|
4190
|
+
projectConnection(project, desiredCredentialStoreFiles);
|
|
3110
4191
|
}
|
|
3111
4192
|
const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
|
|
3112
4193
|
...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
|
|
@@ -3115,30 +4196,81 @@ async function startWorker(options) {
|
|
|
3115
4196
|
]);
|
|
3116
4197
|
for (const projectId of pendingCredentialProjectIds) {
|
|
3117
4198
|
const desiredProject = desiredProjectConfigById.get(projectId);
|
|
3118
|
-
if (desiredProject) projectConnection(desiredProject);
|
|
3119
|
-
else projectMirrorConnection(projectId);
|
|
3120
|
-
requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
|
|
4199
|
+
if (desiredProject) projectConnection(desiredProject, desiredCredentialStoreFiles);
|
|
4200
|
+
else projectMirrorConnection(projectId, desiredCredentialStoreFiles);
|
|
3121
4201
|
}
|
|
3122
4202
|
for (const deletion of pendingMirrorDeletes.values()) {
|
|
3123
4203
|
const desiredProject = desiredProjectConfigById.get(deletion.projectId);
|
|
3124
|
-
const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
|
|
4204
|
+
const connection = desiredProject ? projectConnection(desiredProject, desiredCredentialStoreFiles) : projectMirrorConnection(deletion.projectId, desiredCredentialStoreFiles);
|
|
3125
4205
|
deletion.mirrorUrl = connection.mirrorUrl;
|
|
3126
4206
|
deletion.credentialHelper = connection.credentialHelper;
|
|
3127
4207
|
deletion.credentialUsername = connection.mirrorCredentialUsername;
|
|
3128
4208
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
4209
|
+
let preparedAuthGeneration;
|
|
4210
|
+
try {
|
|
4211
|
+
preparedAuthGeneration = preparePrivateAuthFileGeneration([
|
|
4212
|
+
...planCredentialStoreGeneration(desiredCredentialStoreFiles),
|
|
4213
|
+
...planGitHubRegistryAuthFiles([dockerConfigPath(), containersAuthPath()], message.githubCredential),
|
|
4214
|
+
{ filePath: legacySharedCredentialStorePath(), content: null, allowSymlinkRemoval: true },
|
|
4215
|
+
{ filePath: credentialGenerationReceiptPath(), content: `${incomingCredentialGenerationFingerprint}
|
|
4216
|
+
` },
|
|
4217
|
+
{ filePath: pendingCredentialGenerationPath(), content: null },
|
|
4218
|
+
{ filePath: bootstrapCredentialGenerationPath(), content: null }
|
|
4219
|
+
]);
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
throw error instanceof RegistryAuthConfigurationError ? error : new RegistryAuthConfigurationError(error);
|
|
4222
|
+
}
|
|
4223
|
+
await commitCredentialGeneration(
|
|
4224
|
+
preparedAuthGeneration,
|
|
4225
|
+
message.githubCredential,
|
|
4226
|
+
terminateActiveCredentialBearingChildren,
|
|
4227
|
+
void 0,
|
|
4228
|
+
credentialPublicationPreauthorized
|
|
4229
|
+
);
|
|
4230
|
+
configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
|
|
4231
|
+
pendingCredentialGenerationIntentAtProcessStart = null;
|
|
4232
|
+
bootstrapCredentialGenerationFingerprintAtProcessStart = null;
|
|
3131
4233
|
visibleGitIdentity = message.gitIdentity;
|
|
3132
4234
|
workspaceGitIdentity = message.gitIdentity;
|
|
3133
4235
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
3134
4236
|
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
3135
4237
|
workspaceCredentialUsername = workerCredentialUsername;
|
|
3136
|
-
pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes,
|
|
4238
|
+
pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, projectWorkspaceState.desiredProjects);
|
|
3137
4239
|
const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
|
|
3138
4240
|
...project,
|
|
3139
4241
|
branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
|
|
3140
4242
|
}));
|
|
3141
4243
|
const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
|
|
4244
|
+
ensureWorkspaceGitClone({
|
|
4245
|
+
workspacePath: workspaceShadowRoot,
|
|
4246
|
+
remoteUrl: message.workspaceRemoteUrl,
|
|
4247
|
+
credentialHelper: nextWorkspaceCredentialHelper,
|
|
4248
|
+
credentialUsername: workerCredentialUsername,
|
|
4249
|
+
gitIdentity: message.gitIdentity
|
|
4250
|
+
});
|
|
4251
|
+
const recoverySourceOverrides = /* @__PURE__ */ new Map();
|
|
4252
|
+
for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
|
|
4253
|
+
if (deletion.kind !== "project" || deletion.projectDeleted) continue;
|
|
4254
|
+
for (const branchName of deletion.branchNames) {
|
|
4255
|
+
const oldBranchPath = path.join(deletion.managedPath, ...branchName.split("/"));
|
|
4256
|
+
if (fs.existsSync(oldBranchPath)) recoverySourceOverrides.set(projectMountId(deletion.projectId, branchName), oldBranchPath);
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
const recoveryMounts = buildWorkspaceMounts(effectiveProjects).map((mount) => ({
|
|
4260
|
+
...mount,
|
|
4261
|
+
sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
|
|
4262
|
+
busy: mount.busyForRecovery ?? mount.busy
|
|
4263
|
+
}));
|
|
4264
|
+
recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts);
|
|
4265
|
+
for (const project of message.projects) {
|
|
4266
|
+
for (const { branchName } of project.preserveOnlyBranches) {
|
|
4267
|
+
if (stillPendingCreatedBranches.has(pendingCreatedBranchKey(project.projectId, branchName))) {
|
|
4268
|
+
continue;
|
|
4269
|
+
}
|
|
4270
|
+
deactivatePreserveOnlyProjectBranch(project, branchName);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
const preserveOnlyKeys = new Set(preserveOnlyBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName)));
|
|
3142
4274
|
for (const existingProject of projectConfigById.values()) {
|
|
3143
4275
|
const nextProject = nextProjectConfigById.get(existingProject.projectId);
|
|
3144
4276
|
if (!nextProject) {
|
|
@@ -3148,6 +4280,7 @@ async function startWorker(options) {
|
|
|
3148
4280
|
const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
|
|
3149
4281
|
for (const existingBranch of existingProject.branches) {
|
|
3150
4282
|
if (!nextBranches.has(existingBranch.branchName)) {
|
|
4283
|
+
if (preserveOnlyKeys.has(projectBranchKey(existingProject.projectId, existingBranch.branchName))) continue;
|
|
3151
4284
|
stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
|
|
3152
4285
|
}
|
|
3153
4286
|
}
|
|
@@ -3163,40 +4296,30 @@ async function startWorker(options) {
|
|
|
3163
4296
|
for (const projectId of [...readyProjectIds]) {
|
|
3164
4297
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
3165
4298
|
}
|
|
3166
|
-
ensureWorkspaceGitClone({
|
|
3167
|
-
workspacePath: workspaceShadowRoot,
|
|
3168
|
-
remoteUrl: message.workspaceRemoteUrl,
|
|
3169
|
-
credentialHelper: nextWorkspaceCredentialHelper,
|
|
3170
|
-
credentialUsername: workerCredentialUsername,
|
|
3171
|
-
gitIdentity: message.gitIdentity
|
|
3172
|
-
});
|
|
3173
|
-
pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
|
|
3174
4299
|
stageDurableWorkspaceDeletions();
|
|
3175
4300
|
const mountsBeforeGit = buildWorkspaceMounts();
|
|
3176
4301
|
hydrateWorkspaceGitMounts(
|
|
3177
4302
|
workspaceShadowRoot,
|
|
3178
|
-
mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath))
|
|
4303
|
+
mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath) && mount.busy?.() !== true),
|
|
4304
|
+
// These mounts are intentionally hydrated before their project Git
|
|
4305
|
+
// layouts can be marked ready. No process can resolve an unready
|
|
4306
|
+
// target, and preserving the fetched outer tree here prevents clean
|
|
4307
|
+
// checkout initialization from erasing synchronized dirt.
|
|
4308
|
+
{ ignoreBusy: true }
|
|
3179
4309
|
);
|
|
3180
4310
|
ensureConfiguredProjects();
|
|
3181
|
-
|
|
4311
|
+
const result = await performWorkspaceSync({
|
|
3182
4312
|
attemptId: crypto.randomUUID(),
|
|
3183
4313
|
trigger: { type: "connect" }
|
|
3184
4314
|
});
|
|
3185
|
-
if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
3186
|
-
const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
|
|
3187
|
-
if (staleAfterInbound.length > 0) {
|
|
3188
|
-
pruneStaleOuterWorkspaceEntries(staleAfterInbound);
|
|
3189
|
-
stageDurableWorkspaceDeletions();
|
|
3190
|
-
result = await performWorkspaceSync({
|
|
3191
|
-
attemptId: crypto.randomUUID(),
|
|
3192
|
-
trigger: { type: "connect" }
|
|
3193
|
-
});
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
4315
|
const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
|
|
3197
4316
|
if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
4317
|
+
const activeMountIds = new Set(result.activeMountIds ?? []);
|
|
3198
4318
|
const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
|
|
3199
|
-
(deletion) => deletion.kind === "project" && !deletion.projectDeleted
|
|
4319
|
+
(deletion) => deletion.kind === "project" && !deletion.projectDeleted && checkoutPathMovePublicationComplete(
|
|
4320
|
+
deletion.branchNames.map((branchName) => projectMountId(deletion.projectId, branchName)),
|
|
4321
|
+
activeMountIds
|
|
4322
|
+
) ? [deletion.tombstoneId] : []
|
|
3200
4323
|
);
|
|
3201
4324
|
if (layoutCleanupIds.length > 0) {
|
|
3202
4325
|
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
@@ -3212,18 +4335,37 @@ async function startWorker(options) {
|
|
|
3212
4335
|
return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
|
|
3213
4336
|
});
|
|
3214
4337
|
} catch (error) {
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
4338
|
+
if (error instanceof RegistryAuthConfigurationError) {
|
|
4339
|
+
workspaceConfigured = false;
|
|
4340
|
+
githubCredential = null;
|
|
4341
|
+
configuredCredentialGenerationFingerprint = null;
|
|
4342
|
+
let childTerminationProven = true;
|
|
4343
|
+
try {
|
|
4344
|
+
await terminateActiveCredentialBearingChildren();
|
|
4345
|
+
} catch (childError) {
|
|
4346
|
+
childTerminationProven = false;
|
|
4347
|
+
process.stderr.write(
|
|
4348
|
+
`[r5d-worker] failed to fully terminate credential-bearing children: ${childError instanceof Error ? childError.message : String(childError)}
|
|
4349
|
+
`
|
|
4350
|
+
);
|
|
3222
4351
|
}
|
|
4352
|
+
process.stderr.write(
|
|
4353
|
+
`[r5d-worker] fatal credential-generation transition failure; exiting for whole-runtime cleanup: ${error instanceof Error ? error.message : String(error)}
|
|
4354
|
+
`
|
|
4355
|
+
);
|
|
4356
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4357
|
+
}
|
|
4358
|
+
workspaceConfigured = false;
|
|
4359
|
+
const deferred = error instanceof ProjectWorkspaceConfigurationDeferredError || error instanceof ProjectWorkspacePendingBranchPathChangeError;
|
|
4360
|
+
if (deferred) pendingCheckouts.clear();
|
|
4361
|
+
for (const pending of deferredProjectConfigurationPendingBranches(message.projects)) {
|
|
4362
|
+
pendingCheckouts.set(projectBranchKey(pending.projectId, pending.branchName), pending);
|
|
3223
4363
|
}
|
|
3224
4364
|
return {
|
|
3225
4365
|
result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
|
|
3226
|
-
pending: [...pendingCheckouts.values()]
|
|
4366
|
+
pending: [...pendingCheckouts.values()].sort(
|
|
4367
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4368
|
+
),
|
|
3227
4369
|
aheadOfOriginBranches: []
|
|
3228
4370
|
};
|
|
3229
4371
|
} finally {
|
|
@@ -3318,12 +4460,22 @@ async function startWorker(options) {
|
|
|
3318
4460
|
ws.addEventListener("message", (event) => {
|
|
3319
4461
|
void (async () => {
|
|
3320
4462
|
const message = JSON.parse(String(event.data));
|
|
4463
|
+
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
4464
|
+
const assertMessageAdmission = () => assertWorkerChildAdmission({
|
|
4465
|
+
capturedGeneration: messageAdmissionGeneration,
|
|
4466
|
+
currentGeneration: workerAdmissionGeneration,
|
|
4467
|
+
sourceSocketIsCurrent: currentWorkerSocket === ws,
|
|
4468
|
+
sourceSocketOpen: ws.readyState === WebSocket.OPEN,
|
|
4469
|
+
workspaceConfigured,
|
|
4470
|
+
runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
|
|
4471
|
+
});
|
|
3321
4472
|
if (message.type === "connected") {
|
|
3322
4473
|
return;
|
|
3323
4474
|
}
|
|
3324
4475
|
if (message.type === "workspace_config") {
|
|
4476
|
+
advanceWorkerAdmissionGeneration();
|
|
3325
4477
|
const configured = await configureWorkerWorkspace(message);
|
|
3326
|
-
|
|
4478
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3327
4479
|
type: "workspace_configured",
|
|
3328
4480
|
requestId: message.requestId,
|
|
3329
4481
|
result: configured.result,
|
|
@@ -3358,59 +4510,85 @@ async function startWorker(options) {
|
|
|
3358
4510
|
}
|
|
3359
4511
|
if (message.type === "create_project_branch") {
|
|
3360
4512
|
try {
|
|
3361
|
-
|
|
4513
|
+
const pendingBranch = {
|
|
4514
|
+
branchId: message.branchId,
|
|
3362
4515
|
projectId: message.projectId,
|
|
3363
4516
|
branchName: message.targetBranch
|
|
3364
|
-
}
|
|
4517
|
+
};
|
|
3365
4518
|
const created = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3366
4519
|
const project = projectConfigById.get(message.projectId);
|
|
3367
4520
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3368
|
-
|
|
3369
|
-
|
|
4521
|
+
assertRepositoryExecutionEnabled(project);
|
|
4522
|
+
if (!repositoryMirrorWritesAllowed(project)) {
|
|
4523
|
+
throw new Error(`Project ${message.projectId} branch creation is disabled while its repository connection is transitioning`);
|
|
3370
4524
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
branchName
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
{
|
|
3387
|
-
|
|
3388
|
-
sourceBranchName: message.sourceBranch,
|
|
3389
|
-
baseCommitHash: createdBranch.baseCommitHash
|
|
4525
|
+
const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
|
|
4526
|
+
({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
|
|
4527
|
+
);
|
|
4528
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
4529
|
+
let gitBranchCreated = false;
|
|
4530
|
+
try {
|
|
4531
|
+
if (!readyProjectIds.has(project.projectId)) {
|
|
4532
|
+
throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
|
|
4533
|
+
}
|
|
4534
|
+
if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
|
|
4535
|
+
throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
|
|
4536
|
+
}
|
|
4537
|
+
const existingBranch = project.branches.find(({ branchName }) => branchName === message.targetBranch);
|
|
4538
|
+
if (existingBranch) {
|
|
4539
|
+
if (!pendingWasRecorded) throw new Error(`Project branch ${message.targetBranch} already exists`);
|
|
4540
|
+
if (existingBranch.branchId !== message.branchId) {
|
|
4541
|
+
throw new Error(`Pending project branch ${message.targetBranch} belongs to another durable branch incarnation`);
|
|
3390
4542
|
}
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
4543
|
+
const existingPath = configuredProjectBranchPath(projectsRoot, project, message.targetBranch);
|
|
4544
|
+
if (!hasProjectWorktree(existingPath)) {
|
|
4545
|
+
throw new Error(`Pending project branch ${message.targetBranch} is not initialized on this worker`);
|
|
4546
|
+
}
|
|
4547
|
+
return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
|
|
4548
|
+
}
|
|
4549
|
+
const createdBranch = createOrRetryLinkedProjectBranch({
|
|
4550
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
4551
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
4552
|
+
sourceBranchName: message.sourceBranch,
|
|
4553
|
+
branchName: message.targetBranch,
|
|
4554
|
+
pendingRetry: pendingWasRecorded
|
|
4555
|
+
});
|
|
4556
|
+
gitBranchCreated = true;
|
|
4557
|
+
projectConfigById.set(project.projectId, {
|
|
4558
|
+
...project,
|
|
4559
|
+
branches: [
|
|
4560
|
+
...project.branches,
|
|
4561
|
+
{
|
|
4562
|
+
branchId: message.branchId,
|
|
4563
|
+
branchName: message.targetBranch,
|
|
4564
|
+
sourceBranchName: message.sourceBranch,
|
|
4565
|
+
baseCommitHash: createdBranch.baseCommitHash
|
|
4566
|
+
}
|
|
4567
|
+
]
|
|
4568
|
+
});
|
|
4569
|
+
lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
|
|
4570
|
+
return createdBranch;
|
|
4571
|
+
} catch (error) {
|
|
4572
|
+
if (!pendingWasRecorded && !gitBranchCreated && !projectBranchMayExistAfterCreateFailure(error)) {
|
|
4573
|
+
projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
4574
|
+
}
|
|
4575
|
+
throw error;
|
|
4576
|
+
}
|
|
3395
4577
|
});
|
|
4578
|
+
pendingCreatedBranchPublicationNotBefore.set(
|
|
4579
|
+
pendingCreatedBranchKey(message.projectId, message.targetBranch),
|
|
4580
|
+
Date.now() + PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS
|
|
4581
|
+
);
|
|
3396
4582
|
sendWorkerMessage(ws, {
|
|
3397
4583
|
type: "operation_result",
|
|
3398
4584
|
requestId: message.requestId,
|
|
3399
4585
|
result: {
|
|
3400
4586
|
type: "create_project_branch",
|
|
4587
|
+
branchId: message.branchId,
|
|
3401
4588
|
branchName: message.targetBranch,
|
|
3402
4589
|
baseCommitHash: created.baseCommitHash
|
|
3403
4590
|
}
|
|
3404
4591
|
});
|
|
3405
|
-
markWorkspaceDirty(
|
|
3406
|
-
{
|
|
3407
|
-
type: "manual",
|
|
3408
|
-
projectId: message.projectId,
|
|
3409
|
-
branchName: message.targetBranch,
|
|
3410
|
-
detail: `created project branch from ${message.sourceBranch}`
|
|
3411
|
-
},
|
|
3412
|
-
true
|
|
3413
|
-
);
|
|
3414
4592
|
} catch (error) {
|
|
3415
4593
|
sendWorkerMessage(ws, {
|
|
3416
4594
|
type: "operation_result",
|
|
@@ -3423,42 +4601,66 @@ async function startWorker(options) {
|
|
|
3423
4601
|
if (message.type === "delete_project_branch") {
|
|
3424
4602
|
try {
|
|
3425
4603
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
3426
|
-
|
|
3427
|
-
projectId: message.projectId,
|
|
3428
|
-
branchName: message.branchName
|
|
3429
|
-
});
|
|
3430
|
-
await workspaceSyncSingleFlight.runMutation(() => {
|
|
4604
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3431
4605
|
const project = projectConfigById.get(message.projectId);
|
|
3432
4606
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3433
|
-
|
|
4607
|
+
assertRepositoryExecutionEnabled(project);
|
|
4608
|
+
if (!repositoryMirrorWritesAllowed(project)) {
|
|
4609
|
+
throw new Error(`Project ${message.projectId} branch deletion is disabled while its repository connection is transitioning`);
|
|
4610
|
+
}
|
|
4611
|
+
const configuredBranch = project.branches.find(({ branchName }) => branchName === message.branchName);
|
|
4612
|
+
const pendingLocalBranch = projectWorkspaceState.locallyPendingCreatedBranches.find(
|
|
4613
|
+
({ projectId, branchName }) => projectId === message.projectId && branchName === message.branchName
|
|
4614
|
+
);
|
|
4615
|
+
assertProjectBranchDeletionIncarnation({
|
|
4616
|
+
requiredBranchId: message.branchId,
|
|
4617
|
+
configuredBranch,
|
|
4618
|
+
pendingLocalBranch
|
|
4619
|
+
});
|
|
4620
|
+
const hasDurableTombstone = projectWorkspaceState.tombstones.some(
|
|
4621
|
+
(tombstone) => tombstone.kind === "branch" && tombstone.projectId === message.projectId && tombstone.branchName === message.branchName
|
|
4622
|
+
);
|
|
4623
|
+
if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
|
|
4624
|
+
return false;
|
|
4625
|
+
}
|
|
4626
|
+
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
4627
|
+
projectId: message.projectId,
|
|
4628
|
+
branchName: message.branchName
|
|
4629
|
+
});
|
|
4630
|
+
pendingCreatedBranchPublicationNotBefore.delete(pendingCreatedBranchKey(message.projectId, message.branchName));
|
|
4631
|
+
if (pendingMirrorDeletes.has(deletionKey)) return true;
|
|
3434
4632
|
stageProjectBranchDeletion(project, message.branchName, true);
|
|
3435
4633
|
projectConfigById.set(project.projectId, {
|
|
3436
4634
|
...project,
|
|
3437
4635
|
branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
|
|
3438
4636
|
});
|
|
4637
|
+
return true;
|
|
3439
4638
|
});
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
4639
|
+
if (deletionNeedsSync) {
|
|
4640
|
+
const result = await runWorkspaceSync({
|
|
4641
|
+
trigger: {
|
|
4642
|
+
type: "manual",
|
|
4643
|
+
projectId: message.projectId,
|
|
4644
|
+
branchName: message.branchName,
|
|
4645
|
+
detail: "deleted project branch"
|
|
4646
|
+
},
|
|
4647
|
+
confirmedLargeDiff: true,
|
|
4648
|
+
confirmationReason: "explicit project branch deletion",
|
|
4649
|
+
sendResult: false
|
|
4650
|
+
});
|
|
4651
|
+
if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
|
|
4652
|
+
throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
|
|
4653
|
+
}
|
|
4654
|
+
if (pendingMirrorDeletes.has(deletionKey)) {
|
|
4655
|
+
throw new Error("Project branch deletion did not remove the hidden mirror ref");
|
|
4656
|
+
}
|
|
3456
4657
|
}
|
|
3457
4658
|
sendWorkerMessage(ws, {
|
|
3458
4659
|
type: "operation_result",
|
|
3459
4660
|
requestId: message.requestId,
|
|
3460
4661
|
result: {
|
|
3461
4662
|
type: "delete_project_branch",
|
|
4663
|
+
branchId: message.branchId,
|
|
3462
4664
|
branchName: message.branchName
|
|
3463
4665
|
}
|
|
3464
4666
|
});
|
|
@@ -3646,9 +4848,13 @@ async function startWorker(options) {
|
|
|
3646
4848
|
});
|
|
3647
4849
|
return;
|
|
3648
4850
|
}
|
|
3649
|
-
|
|
4851
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
4852
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
4853
|
+
});
|
|
4854
|
+
let releaseWorkspaceMutation;
|
|
3650
4855
|
let mutationLeaseTransferred = false;
|
|
3651
4856
|
try {
|
|
4857
|
+
releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
|
|
3652
4858
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3653
4859
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
3654
4860
|
`);
|
|
@@ -3657,6 +4863,7 @@ async function startWorker(options) {
|
|
|
3657
4863
|
message,
|
|
3658
4864
|
resolvedTarget,
|
|
3659
4865
|
planRoot,
|
|
4866
|
+
assertAdmission: assertMessageAdmission,
|
|
3660
4867
|
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
3661
4868
|
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
3662
4869
|
onTerminal: () => {
|
|
@@ -3666,13 +4873,14 @@ async function startWorker(options) {
|
|
|
3666
4873
|
});
|
|
3667
4874
|
mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
|
|
3668
4875
|
} catch (error) {
|
|
3669
|
-
|
|
4876
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3670
4877
|
type: "pty_error",
|
|
3671
4878
|
requestId: message.requestId,
|
|
3672
4879
|
ptyId: message.ptyId,
|
|
3673
4880
|
error: error instanceof Error ? error.message : String(error)
|
|
3674
4881
|
});
|
|
3675
4882
|
} finally {
|
|
4883
|
+
workspaceSyncPriorityPtyTargets.delete(message.ptyId);
|
|
3676
4884
|
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
3677
4885
|
}
|
|
3678
4886
|
return;
|
|
@@ -3709,15 +4917,29 @@ async function startWorker(options) {
|
|
|
3709
4917
|
return;
|
|
3710
4918
|
}
|
|
3711
4919
|
let result;
|
|
4920
|
+
let targetReserved = false;
|
|
3712
4921
|
try {
|
|
4922
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
4923
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
4924
|
+
targetReserved = true;
|
|
4925
|
+
});
|
|
3713
4926
|
const runCommand = async () => {
|
|
3714
4927
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3715
4928
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
3716
4929
|
`);
|
|
3717
|
-
return await executeCommand({
|
|
4930
|
+
return await executeCommand({
|
|
4931
|
+
message,
|
|
4932
|
+
resolvedTarget,
|
|
4933
|
+
baseUrl,
|
|
4934
|
+
token,
|
|
4935
|
+
artifactRoot,
|
|
4936
|
+
planRoot,
|
|
4937
|
+
assertAdmission: assertMessageAdmission
|
|
4938
|
+
});
|
|
3718
4939
|
};
|
|
3719
4940
|
result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
|
|
3720
4941
|
} catch (error) {
|
|
4942
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
3721
4943
|
result = {
|
|
3722
4944
|
type: "exec_result",
|
|
3723
4945
|
requestId: message.requestId,
|
|
@@ -3726,6 +4948,8 @@ async function startWorker(options) {
|
|
|
3726
4948
|
exitCode: 1,
|
|
3727
4949
|
error: error instanceof Error ? error.message : String(error)
|
|
3728
4950
|
};
|
|
4951
|
+
} finally {
|
|
4952
|
+
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
3729
4953
|
}
|
|
3730
4954
|
ws.send(JSON.stringify(result));
|
|
3731
4955
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3763,7 +4987,8 @@ async function startWorker(options) {
|
|
|
3763
4987
|
baseUrl,
|
|
3764
4988
|
token,
|
|
3765
4989
|
artifactRoot,
|
|
3766
|
-
planRoot
|
|
4990
|
+
planRoot,
|
|
4991
|
+
assertAdmission: assertMessageAdmission
|
|
3767
4992
|
});
|
|
3768
4993
|
} finally {
|
|
3769
4994
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3779,9 +5004,21 @@ async function startWorker(options) {
|
|
|
3779
5004
|
}
|
|
3780
5005
|
}
|
|
3781
5006
|
};
|
|
3782
|
-
const execution =
|
|
5007
|
+
const execution = (async () => {
|
|
5008
|
+
let targetReserved = false;
|
|
5009
|
+
try {
|
|
5010
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
5011
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
5012
|
+
targetReserved = true;
|
|
5013
|
+
});
|
|
5014
|
+
await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
|
|
5015
|
+
} finally {
|
|
5016
|
+
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
5017
|
+
}
|
|
5018
|
+
})();
|
|
3783
5019
|
void execution.catch((error) => {
|
|
3784
|
-
|
|
5020
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
5021
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3785
5022
|
type: "exec_start_error",
|
|
3786
5023
|
requestId: message.requestId,
|
|
3787
5024
|
runId: message.runId,
|
|
@@ -3791,6 +5028,14 @@ async function startWorker(options) {
|
|
|
3791
5028
|
return;
|
|
3792
5029
|
}
|
|
3793
5030
|
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
|
|
5031
|
+
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
5032
|
+
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
|
|
5033
|
+
if (reservesVisibleWorkspace) {
|
|
5034
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
5035
|
+
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
let dirtyTrigger;
|
|
3794
5039
|
try {
|
|
3795
5040
|
const result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
|
|
3796
5041
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
@@ -3810,13 +5055,14 @@ async function startWorker(options) {
|
|
|
3810
5055
|
result
|
|
3811
5056
|
})
|
|
3812
5057
|
);
|
|
3813
|
-
if (
|
|
3814
|
-
|
|
5058
|
+
if (mutatesVisibleWorkspace) {
|
|
5059
|
+
dirtyTrigger = {
|
|
3815
5060
|
type: message.type,
|
|
3816
5061
|
sessionId: message.sessionId,
|
|
3817
5062
|
toolCallId: message.requestId,
|
|
3818
5063
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
3819
|
-
}
|
|
5064
|
+
};
|
|
5065
|
+
markWorkspaceDirty(dirtyTrigger);
|
|
3820
5066
|
}
|
|
3821
5067
|
} catch (error) {
|
|
3822
5068
|
ws.send(
|
|
@@ -3826,6 +5072,13 @@ async function startWorker(options) {
|
|
|
3826
5072
|
error: error instanceof Error ? error.message : String(error)
|
|
3827
5073
|
})
|
|
3828
5074
|
);
|
|
5075
|
+
} finally {
|
|
5076
|
+
if (reservesVisibleWorkspace) {
|
|
5077
|
+
workspaceSyncPriorityOperationTargets.delete(message.requestId);
|
|
5078
|
+
if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
|
|
5079
|
+
scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
3829
5082
|
}
|
|
3830
5083
|
return;
|
|
3831
5084
|
}
|
|
@@ -3863,6 +5116,7 @@ async function startWorker(options) {
|
|
|
3863
5116
|
terminalReplayTimer = void 0;
|
|
3864
5117
|
}
|
|
3865
5118
|
if (currentWorkerSocket === ws) {
|
|
5119
|
+
advanceWorkerAdmissionGeneration();
|
|
3866
5120
|
currentWorkerSocket = null;
|
|
3867
5121
|
}
|
|
3868
5122
|
closeAllPtys();
|
|
@@ -3875,16 +5129,16 @@ async function startWorker(options) {
|
|
|
3875
5129
|
if (shutdownAfterClose) {
|
|
3876
5130
|
process.exit(0);
|
|
3877
5131
|
}
|
|
3878
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5132
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3879
5133
|
process.stderr.write(
|
|
3880
|
-
`[r5d-worker] ${activeProcesses.size} process(es)
|
|
5134
|
+
`[r5d-worker] ${activeProcesses.size} process(es), ${credentialBearingProcessGroups.size} owned process group(s), and ${activePtys.size} PTY(s) active with ${pendingProcessTerminals.size} terminal report(s) pending; reconnecting in ${WORKER_RECONNECT_DELAY_MS}ms
|
|
3881
5135
|
`
|
|
3882
5136
|
);
|
|
3883
5137
|
const reconnect = () => {
|
|
3884
5138
|
void startWorker(options).catch((error) => {
|
|
3885
5139
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
3886
5140
|
`);
|
|
3887
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5141
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3888
5142
|
setTimeout(reconnect, WORKER_RECONNECT_DELAY_MS);
|
|
3889
5143
|
return;
|
|
3890
5144
|
}
|
|
@@ -3966,6 +5220,7 @@ export {
|
|
|
3966
5220
|
resolveWorkerFilePath,
|
|
3967
5221
|
resolveWorkerSessionTarget,
|
|
3968
5222
|
syncSessionArtifacts,
|
|
5223
|
+
workerChildProcessEnvironment,
|
|
3969
5224
|
workerGitSecurityTestHarness,
|
|
3970
5225
|
writeWorkerTextFile
|
|
3971
5226
|
};
|