@ricsam/r5d-worker 0.0.78 → 0.0.80
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 +1550 -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 +676 -58
- 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 +196 -36
- 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 +1037 -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 +1584 -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 +667 -57
- 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 +195 -36
- 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 +1032 -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 +14 -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
|
+
})
|
|
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)
|
|
2870
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
|
};
|
|
@@ -3035,6 +3912,28 @@ async function startWorker(options) {
|
|
|
3035
3912
|
resetToCanonical: input.resetToCanonical
|
|
3036
3913
|
});
|
|
3037
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
|
+
});
|
|
3038
3937
|
return failedWorkspaceSyncResult(attemptId, input.trigger, error);
|
|
3039
3938
|
}
|
|
3040
3939
|
});
|
|
@@ -3056,7 +3955,13 @@ async function startWorker(options) {
|
|
|
3056
3955
|
};
|
|
3057
3956
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
3058
3957
|
pendingAutomaticTrigger = trigger;
|
|
3059
|
-
|
|
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") {
|
|
3060
3965
|
return;
|
|
3061
3966
|
}
|
|
3062
3967
|
if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
|
|
@@ -3064,8 +3969,14 @@ async function startWorker(options) {
|
|
|
3064
3969
|
() => {
|
|
3065
3970
|
workspaceAutomaticTimer = void 0;
|
|
3066
3971
|
const scheduledTrigger = pendingAutomaticTrigger;
|
|
3067
|
-
pendingAutomaticTrigger = void 0;
|
|
3068
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;
|
|
3069
3980
|
automaticSyncInFlight = true;
|
|
3070
3981
|
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
3071
3982
|
if (result.outcome === "failed") {
|
|
@@ -3085,7 +3996,7 @@ async function startWorker(options) {
|
|
|
3085
3996
|
}
|
|
3086
3997
|
});
|
|
3087
3998
|
},
|
|
3088
|
-
Math.max(0, delayMs)
|
|
3999
|
+
Math.max(0, delayMs, initialDeferral?.kind === "creation_grace" ? initialDeferral.retryAfterMs : 0)
|
|
3089
4000
|
);
|
|
3090
4001
|
workspaceAutomaticTimer.unref();
|
|
3091
4002
|
};
|
|
@@ -3093,32 +4004,190 @@ async function startWorker(options) {
|
|
|
3093
4004
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
3094
4005
|
};
|
|
3095
4006
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
3096
|
-
const resolveMessageTarget = (target) =>
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
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
|
+
};
|
|
3102
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;
|
|
3103
4143
|
workspaceSyncRequestsInFlight += 1;
|
|
3104
4144
|
try {
|
|
3105
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
|
+
}
|
|
3106
4163
|
workspaceConfigured = false;
|
|
4164
|
+
const preserveOnlyBranches = message.projects.flatMap(
|
|
4165
|
+
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
4166
|
+
);
|
|
3107
4167
|
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
3108
|
-
desiredProjects: message.projects
|
|
4168
|
+
desiredProjects: message.projects,
|
|
4169
|
+
preserveOnlyBranches
|
|
3109
4170
|
});
|
|
3110
|
-
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();
|
|
3111
4181
|
const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
|
|
3112
|
-
requiredCredentialStorePaths.add(workspaceStorePath);
|
|
3113
4182
|
const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
|
|
3114
4183
|
[{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
|
|
3115
|
-
workspaceStorePath
|
|
4184
|
+
workspaceStorePath,
|
|
4185
|
+
desiredCredentialStoreFiles
|
|
3116
4186
|
);
|
|
3117
4187
|
if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
|
|
3118
4188
|
const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
|
|
3119
4189
|
for (const project of message.projects) {
|
|
3120
|
-
|
|
3121
|
-
projectConnection(project);
|
|
4190
|
+
projectConnection(project, desiredCredentialStoreFiles);
|
|
3122
4191
|
}
|
|
3123
4192
|
const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
|
|
3124
4193
|
...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
|
|
@@ -3127,30 +4196,81 @@ async function startWorker(options) {
|
|
|
3127
4196
|
]);
|
|
3128
4197
|
for (const projectId of pendingCredentialProjectIds) {
|
|
3129
4198
|
const desiredProject = desiredProjectConfigById.get(projectId);
|
|
3130
|
-
if (desiredProject) projectConnection(desiredProject);
|
|
3131
|
-
else projectMirrorConnection(projectId);
|
|
3132
|
-
requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
|
|
4199
|
+
if (desiredProject) projectConnection(desiredProject, desiredCredentialStoreFiles);
|
|
4200
|
+
else projectMirrorConnection(projectId, desiredCredentialStoreFiles);
|
|
3133
4201
|
}
|
|
3134
4202
|
for (const deletion of pendingMirrorDeletes.values()) {
|
|
3135
4203
|
const desiredProject = desiredProjectConfigById.get(deletion.projectId);
|
|
3136
|
-
const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
|
|
4204
|
+
const connection = desiredProject ? projectConnection(desiredProject, desiredCredentialStoreFiles) : projectMirrorConnection(deletion.projectId, desiredCredentialStoreFiles);
|
|
3137
4205
|
deletion.mirrorUrl = connection.mirrorUrl;
|
|
3138
4206
|
deletion.credentialHelper = connection.credentialHelper;
|
|
3139
4207
|
deletion.credentialUsername = connection.mirrorCredentialUsername;
|
|
3140
4208
|
}
|
|
3141
|
-
|
|
3142
|
-
|
|
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;
|
|
3143
4233
|
visibleGitIdentity = message.gitIdentity;
|
|
3144
4234
|
workspaceGitIdentity = message.gitIdentity;
|
|
3145
4235
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
3146
4236
|
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
3147
4237
|
workspaceCredentialUsername = workerCredentialUsername;
|
|
3148
|
-
pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes,
|
|
4238
|
+
pruneAuthoritativelyDesiredBranchDeletions(pendingMirrorDeletes, projectWorkspaceState.desiredProjects);
|
|
3149
4239
|
const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
|
|
3150
4240
|
...project,
|
|
3151
4241
|
branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
|
|
3152
4242
|
}));
|
|
3153
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)));
|
|
3154
4274
|
for (const existingProject of projectConfigById.values()) {
|
|
3155
4275
|
const nextProject = nextProjectConfigById.get(existingProject.projectId);
|
|
3156
4276
|
if (!nextProject) {
|
|
@@ -3160,6 +4280,7 @@ async function startWorker(options) {
|
|
|
3160
4280
|
const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
|
|
3161
4281
|
for (const existingBranch of existingProject.branches) {
|
|
3162
4282
|
if (!nextBranches.has(existingBranch.branchName)) {
|
|
4283
|
+
if (preserveOnlyKeys.has(projectBranchKey(existingProject.projectId, existingBranch.branchName))) continue;
|
|
3163
4284
|
stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
|
|
3164
4285
|
}
|
|
3165
4286
|
}
|
|
@@ -3175,40 +4296,30 @@ async function startWorker(options) {
|
|
|
3175
4296
|
for (const projectId of [...readyProjectIds]) {
|
|
3176
4297
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
3177
4298
|
}
|
|
3178
|
-
ensureWorkspaceGitClone({
|
|
3179
|
-
workspacePath: workspaceShadowRoot,
|
|
3180
|
-
remoteUrl: message.workspaceRemoteUrl,
|
|
3181
|
-
credentialHelper: nextWorkspaceCredentialHelper,
|
|
3182
|
-
credentialUsername: workerCredentialUsername,
|
|
3183
|
-
gitIdentity: message.gitIdentity
|
|
3184
|
-
});
|
|
3185
|
-
pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
|
|
3186
4299
|
stageDurableWorkspaceDeletions();
|
|
3187
4300
|
const mountsBeforeGit = buildWorkspaceMounts();
|
|
3188
4301
|
hydrateWorkspaceGitMounts(
|
|
3189
4302
|
workspaceShadowRoot,
|
|
3190
|
-
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 }
|
|
3191
4309
|
);
|
|
3192
4310
|
ensureConfiguredProjects();
|
|
3193
|
-
|
|
4311
|
+
const result = await performWorkspaceSync({
|
|
3194
4312
|
attemptId: crypto.randomUUID(),
|
|
3195
4313
|
trigger: { type: "connect" }
|
|
3196
4314
|
});
|
|
3197
|
-
if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
3198
|
-
const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
|
|
3199
|
-
if (staleAfterInbound.length > 0) {
|
|
3200
|
-
pruneStaleOuterWorkspaceEntries(staleAfterInbound);
|
|
3201
|
-
stageDurableWorkspaceDeletions();
|
|
3202
|
-
result = await performWorkspaceSync({
|
|
3203
|
-
attemptId: crypto.randomUUID(),
|
|
3204
|
-
trigger: { type: "connect" }
|
|
3205
|
-
});
|
|
3206
|
-
}
|
|
3207
|
-
}
|
|
3208
4315
|
const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
|
|
3209
4316
|
if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
4317
|
+
const activeMountIds = new Set(result.activeMountIds ?? []);
|
|
3210
4318
|
const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
|
|
3211
|
-
(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] : []
|
|
3212
4323
|
);
|
|
3213
4324
|
if (layoutCleanupIds.length > 0) {
|
|
3214
4325
|
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
@@ -3224,18 +4335,37 @@ async function startWorker(options) {
|
|
|
3224
4335
|
return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
|
|
3225
4336
|
});
|
|
3226
4337
|
} catch (error) {
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
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
|
+
);
|
|
3234
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);
|
|
3235
4363
|
}
|
|
3236
4364
|
return {
|
|
3237
4365
|
result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
|
|
3238
|
-
pending: [...pendingCheckouts.values()]
|
|
4366
|
+
pending: [...pendingCheckouts.values()].sort(
|
|
4367
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4368
|
+
),
|
|
3239
4369
|
aheadOfOriginBranches: []
|
|
3240
4370
|
};
|
|
3241
4371
|
} finally {
|
|
@@ -3330,12 +4460,22 @@ async function startWorker(options) {
|
|
|
3330
4460
|
ws.addEventListener("message", (event) => {
|
|
3331
4461
|
void (async () => {
|
|
3332
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
|
+
});
|
|
3333
4472
|
if (message.type === "connected") {
|
|
3334
4473
|
return;
|
|
3335
4474
|
}
|
|
3336
4475
|
if (message.type === "workspace_config") {
|
|
4476
|
+
advanceWorkerAdmissionGeneration();
|
|
3337
4477
|
const configured = await configureWorkerWorkspace(message);
|
|
3338
|
-
|
|
4478
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3339
4479
|
type: "workspace_configured",
|
|
3340
4480
|
requestId: message.requestId,
|
|
3341
4481
|
result: configured.result,
|
|
@@ -3370,59 +4510,85 @@ async function startWorker(options) {
|
|
|
3370
4510
|
}
|
|
3371
4511
|
if (message.type === "create_project_branch") {
|
|
3372
4512
|
try {
|
|
3373
|
-
|
|
4513
|
+
const pendingBranch = {
|
|
4514
|
+
branchId: message.branchId,
|
|
3374
4515
|
projectId: message.projectId,
|
|
3375
4516
|
branchName: message.targetBranch
|
|
3376
|
-
}
|
|
4517
|
+
};
|
|
3377
4518
|
const created = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3378
4519
|
const project = projectConfigById.get(message.projectId);
|
|
3379
4520
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3380
|
-
|
|
3381
|
-
|
|
4521
|
+
assertRepositoryExecutionEnabled(project);
|
|
4522
|
+
if (!repositoryMirrorWritesAllowed(project)) {
|
|
4523
|
+
throw new Error(`Project ${message.projectId} branch creation is disabled while its repository connection is transitioning`);
|
|
3382
4524
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
branchName
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
{
|
|
3399
|
-
|
|
3400
|
-
sourceBranchName: message.sourceBranch,
|
|
3401
|
-
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`);
|
|
3402
4542
|
}
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
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
|
+
}
|
|
3407
4577
|
});
|
|
4578
|
+
pendingCreatedBranchPublicationNotBefore.set(
|
|
4579
|
+
pendingCreatedBranchKey(message.projectId, message.targetBranch),
|
|
4580
|
+
Date.now() + PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS
|
|
4581
|
+
);
|
|
3408
4582
|
sendWorkerMessage(ws, {
|
|
3409
4583
|
type: "operation_result",
|
|
3410
4584
|
requestId: message.requestId,
|
|
3411
4585
|
result: {
|
|
3412
4586
|
type: "create_project_branch",
|
|
4587
|
+
branchId: message.branchId,
|
|
3413
4588
|
branchName: message.targetBranch,
|
|
3414
4589
|
baseCommitHash: created.baseCommitHash
|
|
3415
4590
|
}
|
|
3416
4591
|
});
|
|
3417
|
-
markWorkspaceDirty(
|
|
3418
|
-
{
|
|
3419
|
-
type: "manual",
|
|
3420
|
-
projectId: message.projectId,
|
|
3421
|
-
branchName: message.targetBranch,
|
|
3422
|
-
detail: `created project branch from ${message.sourceBranch}`
|
|
3423
|
-
},
|
|
3424
|
-
true
|
|
3425
|
-
);
|
|
3426
4592
|
} catch (error) {
|
|
3427
4593
|
sendWorkerMessage(ws, {
|
|
3428
4594
|
type: "operation_result",
|
|
@@ -3435,42 +4601,66 @@ async function startWorker(options) {
|
|
|
3435
4601
|
if (message.type === "delete_project_branch") {
|
|
3436
4602
|
try {
|
|
3437
4603
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
3438
|
-
|
|
3439
|
-
projectId: message.projectId,
|
|
3440
|
-
branchName: message.branchName
|
|
3441
|
-
});
|
|
3442
|
-
await workspaceSyncSingleFlight.runMutation(() => {
|
|
4604
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3443
4605
|
const project = projectConfigById.get(message.projectId);
|
|
3444
4606
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3445
|
-
|
|
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;
|
|
3446
4632
|
stageProjectBranchDeletion(project, message.branchName, true);
|
|
3447
4633
|
projectConfigById.set(project.projectId, {
|
|
3448
4634
|
...project,
|
|
3449
4635
|
branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
|
|
3450
4636
|
});
|
|
4637
|
+
return true;
|
|
3451
4638
|
});
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
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
|
+
}
|
|
3468
4657
|
}
|
|
3469
4658
|
sendWorkerMessage(ws, {
|
|
3470
4659
|
type: "operation_result",
|
|
3471
4660
|
requestId: message.requestId,
|
|
3472
4661
|
result: {
|
|
3473
4662
|
type: "delete_project_branch",
|
|
4663
|
+
branchId: message.branchId,
|
|
3474
4664
|
branchName: message.branchName
|
|
3475
4665
|
}
|
|
3476
4666
|
});
|
|
@@ -3658,9 +4848,13 @@ async function startWorker(options) {
|
|
|
3658
4848
|
});
|
|
3659
4849
|
return;
|
|
3660
4850
|
}
|
|
3661
|
-
|
|
4851
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
4852
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
4853
|
+
});
|
|
4854
|
+
let releaseWorkspaceMutation;
|
|
3662
4855
|
let mutationLeaseTransferred = false;
|
|
3663
4856
|
try {
|
|
4857
|
+
releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
|
|
3664
4858
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3665
4859
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
3666
4860
|
`);
|
|
@@ -3669,6 +4863,7 @@ async function startWorker(options) {
|
|
|
3669
4863
|
message,
|
|
3670
4864
|
resolvedTarget,
|
|
3671
4865
|
planRoot,
|
|
4866
|
+
assertAdmission: assertMessageAdmission,
|
|
3672
4867
|
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
3673
4868
|
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
3674
4869
|
onTerminal: () => {
|
|
@@ -3678,13 +4873,14 @@ async function startWorker(options) {
|
|
|
3678
4873
|
});
|
|
3679
4874
|
mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
|
|
3680
4875
|
} catch (error) {
|
|
3681
|
-
|
|
4876
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3682
4877
|
type: "pty_error",
|
|
3683
4878
|
requestId: message.requestId,
|
|
3684
4879
|
ptyId: message.ptyId,
|
|
3685
4880
|
error: error instanceof Error ? error.message : String(error)
|
|
3686
4881
|
});
|
|
3687
4882
|
} finally {
|
|
4883
|
+
workspaceSyncPriorityPtyTargets.delete(message.ptyId);
|
|
3688
4884
|
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
3689
4885
|
}
|
|
3690
4886
|
return;
|
|
@@ -3721,15 +4917,29 @@ async function startWorker(options) {
|
|
|
3721
4917
|
return;
|
|
3722
4918
|
}
|
|
3723
4919
|
let result;
|
|
4920
|
+
let targetReserved = false;
|
|
3724
4921
|
try {
|
|
4922
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
4923
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
4924
|
+
targetReserved = true;
|
|
4925
|
+
});
|
|
3725
4926
|
const runCommand = async () => {
|
|
3726
4927
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3727
4928
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
3728
4929
|
`);
|
|
3729
|
-
return await executeCommand({
|
|
4930
|
+
return await executeCommand({
|
|
4931
|
+
message,
|
|
4932
|
+
resolvedTarget,
|
|
4933
|
+
baseUrl,
|
|
4934
|
+
token,
|
|
4935
|
+
artifactRoot,
|
|
4936
|
+
planRoot,
|
|
4937
|
+
assertAdmission: assertMessageAdmission
|
|
4938
|
+
});
|
|
3730
4939
|
};
|
|
3731
4940
|
result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
|
|
3732
4941
|
} catch (error) {
|
|
4942
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
3733
4943
|
result = {
|
|
3734
4944
|
type: "exec_result",
|
|
3735
4945
|
requestId: message.requestId,
|
|
@@ -3738,6 +4948,8 @@ async function startWorker(options) {
|
|
|
3738
4948
|
exitCode: 1,
|
|
3739
4949
|
error: error instanceof Error ? error.message : String(error)
|
|
3740
4950
|
};
|
|
4951
|
+
} finally {
|
|
4952
|
+
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
3741
4953
|
}
|
|
3742
4954
|
ws.send(JSON.stringify(result));
|
|
3743
4955
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3775,7 +4987,8 @@ async function startWorker(options) {
|
|
|
3775
4987
|
baseUrl,
|
|
3776
4988
|
token,
|
|
3777
4989
|
artifactRoot,
|
|
3778
|
-
planRoot
|
|
4990
|
+
planRoot,
|
|
4991
|
+
assertAdmission: assertMessageAdmission
|
|
3779
4992
|
});
|
|
3780
4993
|
} finally {
|
|
3781
4994
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3791,9 +5004,21 @@ async function startWorker(options) {
|
|
|
3791
5004
|
}
|
|
3792
5005
|
}
|
|
3793
5006
|
};
|
|
3794
|
-
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
|
+
})();
|
|
3795
5019
|
void execution.catch((error) => {
|
|
3796
|
-
|
|
5020
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
5021
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3797
5022
|
type: "exec_start_error",
|
|
3798
5023
|
requestId: message.requestId,
|
|
3799
5024
|
runId: message.runId,
|
|
@@ -3803,6 +5028,14 @@ async function startWorker(options) {
|
|
|
3803
5028
|
return;
|
|
3804
5029
|
}
|
|
3805
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;
|
|
3806
5039
|
try {
|
|
3807
5040
|
const result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, async () => {
|
|
3808
5041
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
@@ -3822,13 +5055,14 @@ async function startWorker(options) {
|
|
|
3822
5055
|
result
|
|
3823
5056
|
})
|
|
3824
5057
|
);
|
|
3825
|
-
if (
|
|
3826
|
-
|
|
5058
|
+
if (mutatesVisibleWorkspace) {
|
|
5059
|
+
dirtyTrigger = {
|
|
3827
5060
|
type: message.type,
|
|
3828
5061
|
sessionId: message.sessionId,
|
|
3829
5062
|
toolCallId: message.requestId,
|
|
3830
5063
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
3831
|
-
}
|
|
5064
|
+
};
|
|
5065
|
+
markWorkspaceDirty(dirtyTrigger);
|
|
3832
5066
|
}
|
|
3833
5067
|
} catch (error) {
|
|
3834
5068
|
ws.send(
|
|
@@ -3838,6 +5072,13 @@ async function startWorker(options) {
|
|
|
3838
5072
|
error: error instanceof Error ? error.message : String(error)
|
|
3839
5073
|
})
|
|
3840
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
|
+
}
|
|
3841
5082
|
}
|
|
3842
5083
|
return;
|
|
3843
5084
|
}
|
|
@@ -3875,6 +5116,7 @@ async function startWorker(options) {
|
|
|
3875
5116
|
terminalReplayTimer = void 0;
|
|
3876
5117
|
}
|
|
3877
5118
|
if (currentWorkerSocket === ws) {
|
|
5119
|
+
advanceWorkerAdmissionGeneration();
|
|
3878
5120
|
currentWorkerSocket = null;
|
|
3879
5121
|
}
|
|
3880
5122
|
closeAllPtys();
|
|
@@ -3887,16 +5129,16 @@ async function startWorker(options) {
|
|
|
3887
5129
|
if (shutdownAfterClose) {
|
|
3888
5130
|
process.exit(0);
|
|
3889
5131
|
}
|
|
3890
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5132
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3891
5133
|
process.stderr.write(
|
|
3892
|
-
`[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
|
|
3893
5135
|
`
|
|
3894
5136
|
);
|
|
3895
5137
|
const reconnect = () => {
|
|
3896
5138
|
void startWorker(options).catch((error) => {
|
|
3897
5139
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
3898
5140
|
`);
|
|
3899
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5141
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3900
5142
|
setTimeout(reconnect, WORKER_RECONNECT_DELAY_MS);
|
|
3901
5143
|
return;
|
|
3902
5144
|
}
|
|
@@ -3978,6 +5220,7 @@ export {
|
|
|
3978
5220
|
resolveWorkerFilePath,
|
|
3979
5221
|
resolveWorkerSessionTarget,
|
|
3980
5222
|
syncSessionArtifacts,
|
|
5223
|
+
workerChildProcessEnvironment,
|
|
3981
5224
|
workerGitSecurityTestHarness,
|
|
3982
5225
|
writeWorkerTextFile
|
|
3983
5226
|
};
|