@ricsam/r5d-worker 0.0.77 → 0.0.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cjs/main.cjs +1562 -335
- package/dist/cjs/managed-paths.cjs +101 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +184 -5
- package/dist/cjs/project-worktrees.cjs +641 -56
- package/dist/cjs/registry-auth.cjs +310 -0
- package/dist/cjs/repository-transition-policy.cjs +49 -0
- package/dist/cjs/supervisor.cjs +62 -11
- package/dist/cjs/working-tree-mirror.cjs +191 -34
- package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
- package/dist/cjs/workspace-git-sync.cjs +1028 -54
- package/dist/cjs/workspace-mount-boundary.cjs +71 -0
- package/dist/cjs/workspace-path-move.cjs +195 -0
- package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
- package/dist/cjs/workspace-project-config-policy.cjs +46 -0
- package/dist/cjs/workspace-publication-evidence.cjs +46 -0
- package/dist/mjs/main.mjs +1596 -341
- package/dist/mjs/managed-paths.mjs +100 -1
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +181 -5
- package/dist/mjs/project-worktrees.mjs +632 -55
- package/dist/mjs/registry-auth.mjs +269 -0
- package/dist/mjs/repository-transition-policy.mjs +22 -0
- package/dist/mjs/supervisor.mjs +61 -11
- package/dist/mjs/working-tree-mirror.mjs +190 -34
- package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
- package/dist/mjs/workspace-git-sync.mjs +1023 -54
- package/dist/mjs/workspace-mount-boundary.mjs +37 -0
- package/dist/mjs/workspace-path-move.mjs +160 -0
- package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
- package/dist/mjs/workspace-project-config-policy.mjs +21 -0
- package/dist/mjs/workspace-publication-evidence.mjs +21 -0
- package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
- package/dist/types/main.d.ts +175 -1
- package/dist/types/managed-paths.d.ts +11 -0
- package/dist/types/project-workspace-state.d.ts +45 -1
- package/dist/types/project-worktrees.d.ts +119 -2
- package/dist/types/registry-auth.d.ts +47 -0
- package/dist/types/repository-transition-policy.d.ts +26 -0
- package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
- package/dist/types/supervisor-signal-fixture.d.ts +1 -0
- package/dist/types/supervisor.d.ts +6 -4
- package/dist/types/working-tree-mirror.d.ts +7 -0
- package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
- package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
- package/dist/types/workspace-command-sync-policy.d.ts +9 -0
- package/dist/types/workspace-git-sync.d.ts +33 -3
- package/dist/types/workspace-mount-boundary.d.ts +10 -0
- package/dist/types/workspace-path-move.d.ts +21 -0
- package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
- package/dist/types/workspace-project-config-policy.d.ts +34 -0
- package/dist/types/workspace-publication-evidence.d.ts +17 -0
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -48,6 +48,7 @@ __export(main_exports, {
|
|
|
48
48
|
resolveWorkerFilePath: () => resolveWorkerFilePath,
|
|
49
49
|
resolveWorkerSessionTarget: () => resolveWorkerSessionTarget,
|
|
50
50
|
syncSessionArtifacts: () => syncSessionArtifacts,
|
|
51
|
+
workerChildProcessEnvironment: () => workerChildProcessEnvironment,
|
|
51
52
|
workerGitSecurityTestHarness: () => workerGitSecurityTestHarness,
|
|
52
53
|
writeWorkerTextFile: () => writeWorkerTextFile
|
|
53
54
|
});
|
|
@@ -57,21 +58,32 @@ var import_node_path = __toESM(require("node:path"), 1);
|
|
|
57
58
|
var import_node_crypto = require("node:crypto");
|
|
58
59
|
var import_node_os = __toESM(require("node:os"), 1);
|
|
59
60
|
var import_node_child_process = require("node:child_process");
|
|
61
|
+
var import_bun_sqlite = require("bun:sqlite");
|
|
60
62
|
var import_cli_update = require("./cli-update.cjs");
|
|
61
63
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
62
64
|
var import_heartbeat = require("./heartbeat.cjs");
|
|
63
65
|
var import_process_tree = require("./process-tree.cjs");
|
|
64
66
|
var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
|
|
65
67
|
var import_port_forward_client = require("./port-forward-client.cjs");
|
|
68
|
+
var import_registry_auth = require("./registry-auth.cjs");
|
|
66
69
|
var import_workspace_incident_state = require("./workspace-incident-state.cjs");
|
|
70
|
+
var import_workspace_automatic_sync_policy = require("./workspace-automatic-sync-policy.cjs");
|
|
67
71
|
var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
|
|
68
72
|
var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
|
|
73
|
+
var import_workspace_path_move = require("./workspace-path-move.cjs");
|
|
74
|
+
var import_workspace_project_config_policy = require("./workspace-project-config-policy.cjs");
|
|
75
|
+
var import_workspace_preserve_only_policy = require("./workspace-preserve-only-policy.cjs");
|
|
76
|
+
var import_workspace_branch_incarnation_policy = require("./workspace-branch-incarnation-policy.cjs");
|
|
77
|
+
var import_workspace_publication_evidence = require("./workspace-publication-evidence.cjs");
|
|
78
|
+
var import_repository_transition_policy = require("./repository-transition-policy.cjs");
|
|
69
79
|
var import_supervisor = require("./supervisor.cjs");
|
|
70
80
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
71
81
|
var import_project_worktrees = require("./project-worktrees.cjs");
|
|
72
82
|
var import_project_workspace_state = require("./project-workspace-state.cjs");
|
|
73
83
|
var import_workspace_git_sync = require("./workspace-git-sync.cjs");
|
|
74
84
|
const import_meta = {};
|
|
85
|
+
class ProjectWorkspaceConfigurationDeferredError extends Error {
|
|
86
|
+
}
|
|
75
87
|
class WorkerServerUnavailableError extends Error {
|
|
76
88
|
name = "WorkerServerUnavailableError";
|
|
77
89
|
}
|
|
@@ -84,12 +96,19 @@ const MAX_LINE_LENGTH = 2e3;
|
|
|
84
96
|
const WORKSPACE_GIT_QUIET_MS = 5e3;
|
|
85
97
|
const WORKSPACE_GIT_PERIODIC_MS = 6e4;
|
|
86
98
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
99
|
+
const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
|
|
100
|
+
const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
|
|
101
|
+
const workspaceSyncPriorityProcessTargets = /* @__PURE__ */ new Map();
|
|
102
|
+
const workspaceSyncPriorityOperationTargets = /* @__PURE__ */ new Map();
|
|
87
103
|
const pendingProcessTerminals = /* @__PURE__ */ new Map();
|
|
88
104
|
const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
89
105
|
const activePtys = /* @__PURE__ */ new Map();
|
|
106
|
+
const workspaceSyncPriorityPtyTargets = /* @__PURE__ */ new Map();
|
|
90
107
|
let currentWorkerSocket = null;
|
|
108
|
+
let workerAdmissionGeneration = 0;
|
|
91
109
|
const workspaceMutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
|
|
92
110
|
let workspaceSyncQueue = Promise.resolve();
|
|
111
|
+
let startupProjectSnapshotRecoveryCompleted = false;
|
|
93
112
|
const workspaceSyncSingleFlight = {
|
|
94
113
|
runExclusive(operation) {
|
|
95
114
|
const queued = workspaceMutationGate.runSync(operation);
|
|
@@ -110,7 +129,29 @@ const workspaceSyncSingleFlight = {
|
|
|
110
129
|
}
|
|
111
130
|
};
|
|
112
131
|
let githubCredential = null;
|
|
132
|
+
let configuredCredentialGenerationFingerprint = null;
|
|
133
|
+
let pendingCredentialGenerationIntentAtProcessStart = null;
|
|
134
|
+
let bootstrapCredentialGenerationFingerprintAtProcessStart = null;
|
|
135
|
+
let bootSessionIdentifierAtProcessStart = null;
|
|
136
|
+
let machineIdentifierAtProcessStart = null;
|
|
137
|
+
let systemdInvocationIdentifierAtProcessStart = null;
|
|
138
|
+
let initialCredentialBootstrapProvenEmpty = false;
|
|
113
139
|
let visibleGitIdentity = null;
|
|
140
|
+
class StaleWorkerAdmissionError extends Error {
|
|
141
|
+
constructor() {
|
|
142
|
+
super("Worker command admission was invalidated before child creation");
|
|
143
|
+
this.name = "StaleWorkerAdmissionError";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function advanceWorkerAdmissionGeneration() {
|
|
147
|
+
workerAdmissionGeneration += 1;
|
|
148
|
+
return workerAdmissionGeneration;
|
|
149
|
+
}
|
|
150
|
+
function assertWorkerChildAdmission(input) {
|
|
151
|
+
if (input.capturedGeneration !== input.currentGeneration || !input.sourceSocketIsCurrent || !input.sourceSocketOpen || !input.workspaceConfigured || !input.runtimeHealthy) {
|
|
152
|
+
throw new StaleWorkerAdmissionError();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
114
155
|
function defaultConfigPath() {
|
|
115
156
|
return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
116
157
|
}
|
|
@@ -812,6 +853,183 @@ function readOnlyCredentialStoreHelper(storePath) {
|
|
|
812
853
|
function credentialStoreDirectory() {
|
|
813
854
|
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "git-credentials");
|
|
814
855
|
}
|
|
856
|
+
function credentialGenerationReceiptPath(directoryPath = credentialStoreDirectory()) {
|
|
857
|
+
return import_node_path.default.join(directoryPath, ".auth-generation");
|
|
858
|
+
}
|
|
859
|
+
function pendingCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
|
|
860
|
+
return import_node_path.default.join(directoryPath, ".auth-generation-pending");
|
|
861
|
+
}
|
|
862
|
+
function bootstrapCredentialGenerationPath(directoryPath = credentialStoreDirectory()) {
|
|
863
|
+
return import_node_path.default.join(directoryPath, ".auth-generation-bootstrap");
|
|
864
|
+
}
|
|
865
|
+
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})$/;
|
|
866
|
+
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})$/;
|
|
867
|
+
const SYSTEMD_INVOCATION_IDENTIFIER_PATTERN = /^[0-9a-f]{32}$/;
|
|
868
|
+
function exactObjectKeys(value, expected) {
|
|
869
|
+
const actual = Object.keys(value).sort();
|
|
870
|
+
const expectedKeys = [...expected].sort();
|
|
871
|
+
return actual.length === expectedKeys.length && actual.every((key, index) => key === expectedKeys[index]);
|
|
872
|
+
}
|
|
873
|
+
function pendingCredentialGenerationIntentContent(intent) {
|
|
874
|
+
return `${JSON.stringify({ version: 1, fingerprint: intent.fingerprint, boundary: intent.boundary })}
|
|
875
|
+
`;
|
|
876
|
+
}
|
|
877
|
+
function readPendingCredentialGenerationIntent(intentPath = pendingCredentialGenerationPath()) {
|
|
878
|
+
const stat = lstatIfExists(intentPath);
|
|
879
|
+
if (!stat) return null;
|
|
880
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
881
|
+
throw new import_registry_auth.RegistryAuthConfigurationError(new Error(`Pending credential generation intent must be a regular file: ${intentPath}`));
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
const parsed = JSON.parse(import_node_fs.default.readFileSync(intentPath, "utf8"));
|
|
885
|
+
const boundary = parsed.boundary;
|
|
886
|
+
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)) {
|
|
887
|
+
throw new Error("invalid intent fields");
|
|
888
|
+
}
|
|
889
|
+
const boundaryRecord = boundary;
|
|
890
|
+
const machineId = boundaryRecord.machineId;
|
|
891
|
+
if (!(machineId === null || typeof machineId === "string" && MACHINE_IDENTIFIER_PATTERN.test(machineId))) {
|
|
892
|
+
throw new Error("invalid machine identifier");
|
|
893
|
+
}
|
|
894
|
+
let parsedBoundary;
|
|
895
|
+
if (boundaryRecord.kind === "systemd" && exactObjectKeys(boundaryRecord, ["invocationId", "kind", "machineId"]) && typeof boundaryRecord.invocationId === "string" && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(boundaryRecord.invocationId)) {
|
|
896
|
+
parsedBoundary = {
|
|
897
|
+
kind: "systemd",
|
|
898
|
+
machineId,
|
|
899
|
+
invocationId: boundaryRecord.invocationId
|
|
900
|
+
};
|
|
901
|
+
} else if (boundaryRecord.kind === "boot" && exactObjectKeys(boundaryRecord, ["bootSessionId", "kind", "machineId"]) && (boundaryRecord.bootSessionId === null || typeof boundaryRecord.bootSessionId === "string" && BOOT_SESSION_IDENTIFIER_PATTERN.test(boundaryRecord.bootSessionId))) {
|
|
902
|
+
parsedBoundary = {
|
|
903
|
+
kind: "boot",
|
|
904
|
+
machineId,
|
|
905
|
+
bootSessionId: boundaryRecord.bootSessionId
|
|
906
|
+
};
|
|
907
|
+
} else {
|
|
908
|
+
throw new Error("invalid credential boundary");
|
|
909
|
+
}
|
|
910
|
+
return { fingerprint: parsed.fingerprint, boundary: parsedBoundary };
|
|
911
|
+
} catch (error) {
|
|
912
|
+
throw new import_registry_auth.RegistryAuthConfigurationError(
|
|
913
|
+
new Error(`Pending credential generation intent is malformed: ${intentPath}`, { cause: error })
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function parseBootSessionIdentifier(probe) {
|
|
918
|
+
const value = probe.kernelValue.trim();
|
|
919
|
+
if (probe.platform === "linux") {
|
|
920
|
+
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;
|
|
921
|
+
}
|
|
922
|
+
if (probe.platform === "darwin") {
|
|
923
|
+
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;
|
|
924
|
+
}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
function readBootSessionIdentifier(probe) {
|
|
928
|
+
if (probe) return parseBootSessionIdentifier(probe);
|
|
929
|
+
try {
|
|
930
|
+
if (process.platform === "linux") {
|
|
931
|
+
return parseBootSessionIdentifier({
|
|
932
|
+
platform: process.platform,
|
|
933
|
+
kernelValue: import_node_fs.default.readFileSync("/proc/sys/kernel/random/boot_id", "utf8")
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
const command = process.platform === "darwin" ? ["/usr/sbin/sysctl", "-n", "kern.bootsessionuuid"] : null;
|
|
937
|
+
if (!command) return null;
|
|
938
|
+
const result = Bun.spawnSync(command, {
|
|
939
|
+
stdout: "pipe",
|
|
940
|
+
stderr: "pipe",
|
|
941
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
|
|
942
|
+
timeout: 2e3
|
|
943
|
+
});
|
|
944
|
+
if (result.exitCode !== 0) return null;
|
|
945
|
+
return parseBootSessionIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
|
|
946
|
+
} catch {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function parseMachineIdentifier(probe) {
|
|
951
|
+
const value = probe.kernelValue.trim();
|
|
952
|
+
if (probe.platform === "linux") {
|
|
953
|
+
return /^[0-9a-f]{32}$/i.test(value) ? `linux:${value.toLowerCase()}` : null;
|
|
954
|
+
}
|
|
955
|
+
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;
|
|
956
|
+
return uuid ? `${probe.platform}:${uuid.toLowerCase()}` : null;
|
|
957
|
+
}
|
|
958
|
+
function readMachineIdentifier(probe) {
|
|
959
|
+
if (probe) return parseMachineIdentifier(probe);
|
|
960
|
+
try {
|
|
961
|
+
if (process.platform === "linux") {
|
|
962
|
+
return parseMachineIdentifier({ platform: process.platform, kernelValue: import_node_fs.default.readFileSync("/etc/machine-id", "utf8") });
|
|
963
|
+
}
|
|
964
|
+
const command = process.platform === "darwin" ? ["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"] : null;
|
|
965
|
+
if (!command) return null;
|
|
966
|
+
const result = Bun.spawnSync(command, {
|
|
967
|
+
stdout: "pipe",
|
|
968
|
+
stderr: "pipe",
|
|
969
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
|
|
970
|
+
timeout: 2e3
|
|
971
|
+
});
|
|
972
|
+
if (result.exitCode !== 0) return null;
|
|
973
|
+
return parseMachineIdentifier({ platform: process.platform, kernelValue: result.stdout.toString() });
|
|
974
|
+
} catch {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function credentialAuthorityLockPath(rootPath = import_node_path.default.join(import_node_os.default.homedir(), ".r5d")) {
|
|
979
|
+
return import_node_path.default.join(rootPath, ".credential-authority.sqlite");
|
|
980
|
+
}
|
|
981
|
+
function acquireCredentialAuthorityLock(input) {
|
|
982
|
+
const rootPath = import_node_path.default.resolve(input.rootPath ?? import_node_path.default.join(import_node_os.default.homedir(), ".r5d"));
|
|
983
|
+
const rootStat = lstatIfExists(rootPath);
|
|
984
|
+
if (rootStat) {
|
|
985
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
986
|
+
throw new Error(`Worker credential authority root must be a directory: ${rootPath}`);
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
import_node_fs.default.mkdirSync(rootPath, { recursive: true, mode: 448 });
|
|
990
|
+
}
|
|
991
|
+
import_node_fs.default.chmodSync(rootPath, 448);
|
|
992
|
+
const lockPath = credentialAuthorityLockPath(rootPath);
|
|
993
|
+
const existing = lstatIfExists(lockPath);
|
|
994
|
+
if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
|
|
995
|
+
throw new Error(`Worker credential authority database must be a regular file: ${lockPath}`);
|
|
996
|
+
}
|
|
997
|
+
const database = new import_bun_sqlite.Database(lockPath, { create: true, strict: true });
|
|
998
|
+
try {
|
|
999
|
+
database.exec("PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE");
|
|
1000
|
+
database.exec("CREATE TABLE IF NOT EXISTS authority_metadata (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), machine_id TEXT)");
|
|
1001
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1002
|
+
try {
|
|
1003
|
+
const row = database.query("SELECT machine_id AS machineId FROM authority_metadata WHERE singleton = 1").get();
|
|
1004
|
+
if (row) {
|
|
1005
|
+
if (row.machineId === null || input.machineId === null) {
|
|
1006
|
+
if (row.machineId !== input.machineId) {
|
|
1007
|
+
throw new Error("Cannot prove that the worker credential authority database belongs to this machine");
|
|
1008
|
+
}
|
|
1009
|
+
} else if (row.machineId !== input.machineId) {
|
|
1010
|
+
throw new Error("Worker credential authority home belongs to a different machine; refusing to move or share credential state");
|
|
1011
|
+
}
|
|
1012
|
+
} else {
|
|
1013
|
+
database.query("INSERT INTO authority_metadata (singleton, machine_id) VALUES (1, ?)").run(input.machineId);
|
|
1014
|
+
}
|
|
1015
|
+
database.exec("COMMIT");
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
database.exec("ROLLBACK");
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
database.exec("BEGIN EXCLUSIVE");
|
|
1021
|
+
return { lockPath, database };
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
database.close(false);
|
|
1024
|
+
if (error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message)) {
|
|
1025
|
+
throw new Error("Another r5d-worker credential authority is already running under this OS account", { cause: error });
|
|
1026
|
+
}
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function releaseCredentialAuthorityLockHandle(handle) {
|
|
1031
|
+
handle.database.close(false);
|
|
1032
|
+
}
|
|
815
1033
|
function legacySharedCredentialStorePath() {
|
|
816
1034
|
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "github-credentials");
|
|
817
1035
|
}
|
|
@@ -842,9 +1060,24 @@ function ensurePrivateCredentialStoreDirectory(directoryPath) {
|
|
|
842
1060
|
import_node_fs.default.chmodSync(directoryPath, 448);
|
|
843
1061
|
}
|
|
844
1062
|
function removeLegacySharedCredentialStore(storePath = legacySharedCredentialStorePath()) {
|
|
845
|
-
|
|
1063
|
+
const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
|
|
1064
|
+
prepared.commit();
|
|
846
1065
|
}
|
|
847
1066
|
let credentialStoreDirectoryInitialized = false;
|
|
1067
|
+
let credentialAuthorityLockHandle = null;
|
|
1068
|
+
function releaseCredentialAuthorityLock() {
|
|
1069
|
+
const handle = credentialAuthorityLockHandle;
|
|
1070
|
+
credentialAuthorityLockHandle = null;
|
|
1071
|
+
if (!handle) return;
|
|
1072
|
+
try {
|
|
1073
|
+
releaseCredentialAuthorityLockHandle(handle);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
process.stderr.write(
|
|
1076
|
+
`[r5d-worker] could not release credential authority lock: ${error instanceof Error ? error.message : String(error)}
|
|
1077
|
+
`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
848
1081
|
function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory()) {
|
|
849
1082
|
const stat = lstatIfExists(directoryPath);
|
|
850
1083
|
if (stat) {
|
|
@@ -855,11 +1088,73 @@ function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory(
|
|
|
855
1088
|
}
|
|
856
1089
|
function initializeCredentialStoreDirectoryOnce() {
|
|
857
1090
|
if (credentialStoreDirectoryInitialized) return;
|
|
858
|
-
|
|
859
|
-
|
|
1091
|
+
systemdInvocationIdentifierAtProcessStart = SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? process.env.INVOCATION_ID.toLowerCase() : null;
|
|
1092
|
+
bootSessionIdentifierAtProcessStart = readBootSessionIdentifier();
|
|
1093
|
+
machineIdentifierAtProcessStart = readMachineIdentifier();
|
|
1094
|
+
credentialAuthorityLockHandle = acquireCredentialAuthorityLock({
|
|
1095
|
+
machineId: machineIdentifierAtProcessStart
|
|
1096
|
+
});
|
|
1097
|
+
process.once("exit", releaseCredentialAuthorityLock);
|
|
1098
|
+
initialCredentialBootstrapProvenEmpty = noPriorCredentialArtifactsAtProcessStart();
|
|
1099
|
+
ensurePrivateCredentialStoreDirectory(credentialStoreDirectory());
|
|
1100
|
+
configuredCredentialGenerationFingerprint = readCredentialGenerationReceipt();
|
|
1101
|
+
pendingCredentialGenerationIntentAtProcessStart = readPendingCredentialGenerationIntent();
|
|
1102
|
+
bootstrapCredentialGenerationFingerprintAtProcessStart = readCredentialGenerationReceipt(bootstrapCredentialGenerationPath());
|
|
860
1103
|
credentialStoreDirectoryInitialized = true;
|
|
861
1104
|
}
|
|
1105
|
+
function noPriorCredentialArtifactsAtProcessStart() {
|
|
1106
|
+
if (lstatIfExists(credentialStoreDirectory()) || lstatIfExists(legacySharedCredentialStorePath())) return false;
|
|
1107
|
+
try {
|
|
1108
|
+
return ![dockerConfigPath(), containersAuthPath()].some(import_registry_auth.registryAuthHasAppManagedGitHubCredential);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function readCredentialGenerationReceipt(receiptPath = credentialGenerationReceiptPath()) {
|
|
1114
|
+
const stat = lstatIfExists(receiptPath);
|
|
1115
|
+
if (!stat) return null;
|
|
1116
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
1117
|
+
throw new import_registry_auth.RegistryAuthConfigurationError(new Error(`Credential generation receipt must be a regular file: ${receiptPath}`));
|
|
1118
|
+
}
|
|
1119
|
+
const content = import_node_fs.default.readFileSync(receiptPath, "utf8");
|
|
1120
|
+
const match = /^([0-9a-f]{64})\n$/.exec(content);
|
|
1121
|
+
if (!match) {
|
|
1122
|
+
throw new import_registry_auth.RegistryAuthConfigurationError(new Error(`Credential generation receipt is malformed: ${receiptPath}`));
|
|
1123
|
+
}
|
|
1124
|
+
return match[1];
|
|
1125
|
+
}
|
|
862
1126
|
const APP_CREDENTIAL_STORE_FILE = /^(?:project|workspace)-[0-9a-f]{64}\.store$/;
|
|
1127
|
+
function planCredentialStoreGeneration(desiredFiles, directoryPath = credentialStoreDirectory()) {
|
|
1128
|
+
ensurePrivateCredentialStoreDirectory(directoryPath);
|
|
1129
|
+
const resolvedDirectory = import_node_path.default.resolve(directoryPath);
|
|
1130
|
+
const updates = /* @__PURE__ */ new Map();
|
|
1131
|
+
const symlinkRemovals = /* @__PURE__ */ new Set();
|
|
1132
|
+
for (const [storePath, content] of desiredFiles) {
|
|
1133
|
+
const resolvedStorePath = import_node_path.default.resolve(storePath);
|
|
1134
|
+
if (import_node_path.default.dirname(resolvedStorePath) !== resolvedDirectory || !APP_CREDENTIAL_STORE_FILE.test(import_node_path.default.basename(resolvedStorePath))) {
|
|
1135
|
+
throw new Error("Invalid app-owned Git credential-store path");
|
|
1136
|
+
}
|
|
1137
|
+
updates.set(resolvedStorePath, content);
|
|
1138
|
+
}
|
|
1139
|
+
for (const name of import_node_fs.default.readdirSync(directoryPath)) {
|
|
1140
|
+
if (!APP_CREDENTIAL_STORE_FILE.test(name)) continue;
|
|
1141
|
+
const storePath = import_node_path.default.join(resolvedDirectory, name);
|
|
1142
|
+
const stat = import_node_fs.default.lstatSync(storePath);
|
|
1143
|
+
if (stat.isSymbolicLink()) {
|
|
1144
|
+
if (updates.has(storePath)) throw new Error(`Required Git credential store must not be a symlink: ${storePath}`);
|
|
1145
|
+
updates.set(storePath, null);
|
|
1146
|
+
symlinkRemovals.add(storePath);
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (!stat.isFile()) throw new Error(`Git credential store must be a regular file: ${storePath}`);
|
|
1150
|
+
if (!updates.has(storePath)) updates.set(storePath, null);
|
|
1151
|
+
}
|
|
1152
|
+
return [...updates].map(([filePath, content]) => ({
|
|
1153
|
+
filePath,
|
|
1154
|
+
content,
|
|
1155
|
+
...symlinkRemovals.has(filePath) ? { allowSymlinkRemoval: true } : {}
|
|
1156
|
+
}));
|
|
1157
|
+
}
|
|
863
1158
|
function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentialStoreDirectory()) {
|
|
864
1159
|
ensurePrivateCredentialStoreDirectory(directoryPath);
|
|
865
1160
|
const resolvedDirectory = import_node_path.default.resolve(directoryPath);
|
|
@@ -877,9 +1172,10 @@ function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentia
|
|
|
877
1172
|
const storePath = import_node_path.default.join(directoryPath, name);
|
|
878
1173
|
const stat = import_node_fs.default.lstatSync(storePath);
|
|
879
1174
|
if (!stat.isFile() && !stat.isSymbolicLink()) continue;
|
|
880
|
-
import_node_fs.default.unlinkSync(storePath);
|
|
881
1175
|
removed.push(storePath);
|
|
882
1176
|
}
|
|
1177
|
+
const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)(removed.map((filePath) => ({ filePath, content: null, allowSymlinkRemoval: true })));
|
|
1178
|
+
prepared.commit();
|
|
883
1179
|
return removed;
|
|
884
1180
|
}
|
|
885
1181
|
function removeProjectCredentialStore(projectId) {
|
|
@@ -887,7 +1183,8 @@ function removeProjectCredentialStore(projectId) {
|
|
|
887
1183
|
const stat = lstatIfExists(storePath);
|
|
888
1184
|
if (!stat) return;
|
|
889
1185
|
if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("Git credential store must be a regular file");
|
|
890
|
-
|
|
1186
|
+
const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content: null, allowSymlinkRemoval: true }]);
|
|
1187
|
+
prepared.commit();
|
|
891
1188
|
}
|
|
892
1189
|
function decodeGitAuthHeader(authHeader) {
|
|
893
1190
|
const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
|
|
@@ -907,16 +1204,10 @@ function credentialUsernameForAuthHeader(authHeader) {
|
|
|
907
1204
|
return credentials.username;
|
|
908
1205
|
}
|
|
909
1206
|
function replaceCredentialStoreFile(storePath, content) {
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
import_node_fs.default.writeFileSync(temporaryPath, content, { flag: "wx", mode: 384 });
|
|
913
|
-
import_node_fs.default.chmodSync(temporaryPath, 384);
|
|
914
|
-
import_node_fs.default.renameSync(temporaryPath, storePath);
|
|
915
|
-
} finally {
|
|
916
|
-
import_node_fs.default.rmSync(temporaryPath, { force: true });
|
|
917
|
-
}
|
|
1207
|
+
const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content }]);
|
|
1208
|
+
prepared.commit();
|
|
918
1209
|
}
|
|
919
|
-
function
|
|
1210
|
+
function credentialStoreContent(remotes) {
|
|
920
1211
|
const authoritativeEntries = /* @__PURE__ */ new Map();
|
|
921
1212
|
for (const input of remotes) {
|
|
922
1213
|
let remote;
|
|
@@ -948,23 +1239,179 @@ function writeCredentialStoreEntries(remotes, storePath) {
|
|
|
948
1239
|
}
|
|
949
1240
|
authoritativeEntries.set(hostKey, replacement);
|
|
950
1241
|
}
|
|
951
|
-
const storeStat = lstatIfExists(storePath);
|
|
952
|
-
if (storeStat) {
|
|
953
|
-
if (storeStat.isSymbolicLink() || !storeStat.isFile()) throw new Error("Git credential store must be a regular file");
|
|
954
|
-
}
|
|
955
1242
|
const replacements = [...authoritativeEntries.values()].filter((entry) => entry !== null);
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1243
|
+
return replacements.length > 0 ? `${replacements.join("\n")}
|
|
1244
|
+
` : null;
|
|
1245
|
+
}
|
|
1246
|
+
function writeCredentialStoreEntries(remotes, storePath) {
|
|
1247
|
+
const content = credentialStoreContent(remotes);
|
|
1248
|
+
const prepared = (0, import_registry_auth.preparePrivateAuthFileGeneration)([{ filePath: storePath, content }]);
|
|
1249
|
+
prepared.commit();
|
|
1250
|
+
return content === null ? null : { hasCredentials: true, storePath };
|
|
1251
|
+
}
|
|
1252
|
+
function credentialHelperForRemotes(remotes, storePath, desiredFiles) {
|
|
1253
|
+
if (!desiredFiles) {
|
|
1254
|
+
const credentialStore = writeCredentialStoreEntries(remotes, storePath);
|
|
1255
|
+
return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
|
|
1256
|
+
}
|
|
1257
|
+
const resolvedStorePath = import_node_path.default.resolve(storePath);
|
|
1258
|
+
const content = credentialStoreContent(remotes);
|
|
1259
|
+
const previous = desiredFiles.get(resolvedStorePath);
|
|
1260
|
+
if (desiredFiles.has(resolvedStorePath) && previous !== content) {
|
|
1261
|
+
throw new Error(`Conflicting desired Git credentials for ${resolvedStorePath}`);
|
|
1262
|
+
}
|
|
1263
|
+
desiredFiles.set(resolvedStorePath, content);
|
|
1264
|
+
return content === null ? null : readOnlyCredentialStoreHelper(resolvedStorePath);
|
|
1265
|
+
}
|
|
1266
|
+
function authenticatedRemoteCredentialSecret(remoteUrl, authHeader) {
|
|
1267
|
+
if (authHeader == null) return null;
|
|
1268
|
+
let remote;
|
|
1269
|
+
try {
|
|
1270
|
+
remote = new URL(remoteUrl);
|
|
1271
|
+
} catch {
|
|
1272
|
+
throw new Error("Invalid authenticated Git remote URL");
|
|
959
1273
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1274
|
+
if (remote.protocol !== "https:" && remote.protocol !== "http:") {
|
|
1275
|
+
throw new Error("Unsupported authenticated Git remote protocol");
|
|
1276
|
+
}
|
|
1277
|
+
if (remote.username || remote.password) throw new Error("Git credentials must not be embedded in a remote URL");
|
|
1278
|
+
const credentials = decodeGitAuthHeader(authHeader);
|
|
1279
|
+
if (!credentials) throw new Error("Unsupported Git authorization header");
|
|
1280
|
+
return credentials.password;
|
|
1281
|
+
}
|
|
1282
|
+
function credentialMaterialCommitment(kind, values) {
|
|
1283
|
+
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify([`r5d-credential-${kind}-v1`, ...values])).digest("hex");
|
|
1284
|
+
}
|
|
1285
|
+
function credentialGenerationFingerprint(config) {
|
|
1286
|
+
const workerApiUrl = new URL(config.workerBaseUrl);
|
|
1287
|
+
if (workerApiUrl.protocol !== "https:" && workerApiUrl.protocol !== "http:") {
|
|
1288
|
+
throw new Error("Unsupported worker API protocol");
|
|
1289
|
+
}
|
|
1290
|
+
const workerCredentials = decodeGitAuthHeader(config.workerAuthHeader);
|
|
1291
|
+
if (!workerCredentials) throw new Error("Unsupported worker authorization header");
|
|
1292
|
+
const materialCommitments = /* @__PURE__ */ new Set();
|
|
1293
|
+
const addSecret = (secret) => {
|
|
1294
|
+
materialCommitments.add(`secret:${credentialMaterialCommitment("secret", [secret])}`);
|
|
1295
|
+
};
|
|
1296
|
+
addSecret(workerCredentials.password);
|
|
1297
|
+
const workspaceSecret = authenticatedRemoteCredentialSecret(config.workspaceRemoteUrl, config.workerAuthHeader);
|
|
1298
|
+
if (workspaceSecret) addSecret(workspaceSecret);
|
|
1299
|
+
for (const project of config.projects) {
|
|
1300
|
+
if (!project.repoHttpUrl) continue;
|
|
1301
|
+
const secret = authenticatedRemoteCredentialSecret(project.repoHttpUrl, project.repoAuthHeader);
|
|
1302
|
+
if (secret) addSecret(secret);
|
|
1303
|
+
}
|
|
1304
|
+
if (config.githubCredential) {
|
|
1305
|
+
addSecret(config.githubCredential.token);
|
|
1306
|
+
materialCommitments.add(
|
|
1307
|
+
`github_authority:${credentialMaterialCommitment("github_authority", [
|
|
1308
|
+
config.githubCredential.token,
|
|
1309
|
+
...[...config.githubCredential.missingScopes].sort()
|
|
1310
|
+
])}`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify([...materialCommitments].sort())).digest("hex");
|
|
1314
|
+
}
|
|
1315
|
+
function credentialGenerationTransitionPhase(input) {
|
|
1316
|
+
if (input.committedFingerprint === input.incomingFingerprint) return "current";
|
|
1317
|
+
if (input.pendingFingerprintAtProcessStart !== null) {
|
|
1318
|
+
return input.pendingFingerprintAtProcessStart === input.incomingFingerprint ? "publish_from_clean_restart" : "stage_pending_restart";
|
|
1319
|
+
}
|
|
1320
|
+
return "stage_pending_restart";
|
|
1321
|
+
}
|
|
1322
|
+
function initialCredentialBootstrapIsSafe(input) {
|
|
1323
|
+
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;
|
|
1324
|
+
}
|
|
1325
|
+
async function fenceCredentialGenerationAtConfigureEntry(input) {
|
|
1326
|
+
if (input.currentFingerprint === input.incomingFingerprint) return false;
|
|
1327
|
+
input.revokeInMemory();
|
|
1328
|
+
await input.terminatePreviousGeneration();
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
function requireCredentialGenerationRestart(required, fatalExit = (exitCode2) => process.exit(exitCode2), exitCode = 1) {
|
|
1332
|
+
if (!required) return;
|
|
1333
|
+
fatalExit(exitCode);
|
|
1334
|
+
throw new Error("Credential generation restart callback returned unexpectedly");
|
|
1335
|
+
}
|
|
1336
|
+
function pendingCredentialGenerationBoundary(input) {
|
|
1337
|
+
if (input.systemdContract && input.systemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.systemdInvocationId)) {
|
|
1338
|
+
return { kind: "systemd", machineId: input.machineId, invocationId: input.systemdInvocationId };
|
|
1339
|
+
}
|
|
1340
|
+
return { kind: "boot", machineId: input.machineId, bootSessionId: input.bootSessionId };
|
|
964
1341
|
}
|
|
965
|
-
function
|
|
966
|
-
|
|
967
|
-
|
|
1342
|
+
function credentialGenerationBoundaryIsComplete(boundary) {
|
|
1343
|
+
return boundary.machineId !== null && (boundary.kind === "systemd" || boundary.bootSessionId !== null);
|
|
1344
|
+
}
|
|
1345
|
+
function credentialGenerationRestartIsContained(input) {
|
|
1346
|
+
const pending = input.pendingBoundary;
|
|
1347
|
+
if (pending === null || pending.machineId === null || input.currentMachineId === null || pending.machineId !== input.currentMachineId) {
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
if (pending.kind === "systemd") {
|
|
1351
|
+
return input.systemdContract && input.currentSystemdInvocationId !== null && SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(input.currentSystemdInvocationId) && pending.invocationId !== input.currentSystemdInvocationId;
|
|
1352
|
+
}
|
|
1353
|
+
return pending.bootSessionId !== null && input.currentBootSessionId !== null && pending.bootSessionId !== input.currentBootSessionId;
|
|
1354
|
+
}
|
|
1355
|
+
function credentialReapContractStatus(probe) {
|
|
1356
|
+
let resolved = probe;
|
|
1357
|
+
if (!resolved) {
|
|
1358
|
+
if (process.platform !== "linux") return "direct";
|
|
1359
|
+
try {
|
|
1360
|
+
const cgroupText = import_node_fs.default.readFileSync("/proc/self/cgroup", "utf8");
|
|
1361
|
+
const unit = cgroupText.match(/(?:^|\/)r5d-worker\.service(?:$|\n)/m)?.[0]?.replace(/^\//, "").trim();
|
|
1362
|
+
if (unit !== "r5d-worker.service") {
|
|
1363
|
+
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(process.env.INVOCATION_ID ?? "") ? "systemd_unverified" : "direct";
|
|
1364
|
+
}
|
|
1365
|
+
const result = Bun.spawnSync(
|
|
1366
|
+
[
|
|
1367
|
+
"/usr/bin/systemctl",
|
|
1368
|
+
"show",
|
|
1369
|
+
unit,
|
|
1370
|
+
"--no-pager",
|
|
1371
|
+
"--property=KillMode",
|
|
1372
|
+
"--property=Restart",
|
|
1373
|
+
"--property=SendSIGKILL",
|
|
1374
|
+
"--property=InvocationID"
|
|
1375
|
+
],
|
|
1376
|
+
{
|
|
1377
|
+
stdout: "pipe",
|
|
1378
|
+
stderr: "pipe",
|
|
1379
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)(),
|
|
1380
|
+
timeout: 2e3
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
if (result.exitCode !== 0) return "systemd_unverified";
|
|
1384
|
+
resolved = {
|
|
1385
|
+
platform: process.platform,
|
|
1386
|
+
invocationId: process.env.INVOCATION_ID,
|
|
1387
|
+
cgroupText,
|
|
1388
|
+
unitProperties: result.stdout.toString()
|
|
1389
|
+
};
|
|
1390
|
+
} catch {
|
|
1391
|
+
return "systemd_unverified";
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (resolved.platform !== "linux") return "direct";
|
|
1395
|
+
if (!/(?:^|\/)r5d-worker\.service(?:$|\n)/m.test(resolved.cgroupText)) {
|
|
1396
|
+
return SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "") ? "systemd_unverified" : "direct";
|
|
1397
|
+
}
|
|
1398
|
+
if (!SYSTEMD_INVOCATION_IDENTIFIER_PATTERN.test(resolved.invocationId ?? "")) return "systemd_unverified";
|
|
1399
|
+
const properties = new Map(
|
|
1400
|
+
resolved.unitProperties.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
1401
|
+
const separator = line.indexOf("=");
|
|
1402
|
+
return separator < 1 ? [line, ""] : [line.slice(0, separator), line.slice(separator + 1)];
|
|
1403
|
+
})
|
|
1404
|
+
);
|
|
1405
|
+
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";
|
|
1406
|
+
}
|
|
1407
|
+
function verifiedCredentialReapContract(probe) {
|
|
1408
|
+
return credentialReapContractStatus(probe) === "verified_systemd";
|
|
1409
|
+
}
|
|
1410
|
+
function fenceUnsafeWorkspaceSyncFailure(input) {
|
|
1411
|
+
if (input.hydrationCurrent) return false;
|
|
1412
|
+
input.invalidateExecution();
|
|
1413
|
+
requireCredentialGenerationRestart(true, input.fatalExit, input.exitCode);
|
|
1414
|
+
return true;
|
|
968
1415
|
}
|
|
969
1416
|
const workerGitSecurityTestHarness = {
|
|
970
1417
|
commandArgs: workerGitCommand.commandArgs,
|
|
@@ -976,7 +1423,45 @@ const workerGitSecurityTestHarness = {
|
|
|
976
1423
|
pruneCredentialStoreFiles,
|
|
977
1424
|
readOnlyCredentialStoreHelper,
|
|
978
1425
|
resetCredentialStoreDirectory,
|
|
979
|
-
replaceCredentialStoreFile
|
|
1426
|
+
replaceCredentialStoreFile,
|
|
1427
|
+
configureGitHubAuth(credential, filePaths, configureFiles = import_registry_auth.configureGitHubRegistryAuthFiles) {
|
|
1428
|
+
configureGitHubAuth(credential, filePaths, configureFiles);
|
|
1429
|
+
},
|
|
1430
|
+
githubCredential: () => githubCredential,
|
|
1431
|
+
credentialGenerationFingerprint,
|
|
1432
|
+
credentialGenerationTransitionPhase,
|
|
1433
|
+
initialCredentialBootstrapIsSafe,
|
|
1434
|
+
fenceCredentialGenerationAtConfigureEntry,
|
|
1435
|
+
requireCredentialGenerationRestart,
|
|
1436
|
+
pendingCredentialGenerationBoundary,
|
|
1437
|
+
credentialGenerationBoundaryIsComplete,
|
|
1438
|
+
credentialGenerationRestartIsContained,
|
|
1439
|
+
credentialReapContractStatus,
|
|
1440
|
+
verifiedCredentialReapContract,
|
|
1441
|
+
fenceUnsafeWorkspaceSyncFailure,
|
|
1442
|
+
assertWorkerChildAdmission,
|
|
1443
|
+
terminateCredentialBearingChildren,
|
|
1444
|
+
terminateCredentialBearingChildrenWithRetention,
|
|
1445
|
+
async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
|
|
1446
|
+
return await commitCredentialGeneration(
|
|
1447
|
+
prepared,
|
|
1448
|
+
credential,
|
|
1449
|
+
async () => await terminateCredentialBearingChildren(children),
|
|
1450
|
+
beforeMutation,
|
|
1451
|
+
previousGenerationFenced
|
|
1452
|
+
);
|
|
1453
|
+
},
|
|
1454
|
+
credentialGenerationReceiptPath,
|
|
1455
|
+
credentialAuthorityLockPath,
|
|
1456
|
+
acquireCredentialAuthorityLock,
|
|
1457
|
+
releaseCredentialAuthorityLockHandle,
|
|
1458
|
+
pendingCredentialGenerationPath,
|
|
1459
|
+
pendingCredentialGenerationIntentContent,
|
|
1460
|
+
readPendingCredentialGenerationIntent,
|
|
1461
|
+
readBootSessionIdentifier,
|
|
1462
|
+
readMachineIdentifier,
|
|
1463
|
+
bootstrapCredentialGenerationPath,
|
|
1464
|
+
readCredentialGenerationReceipt
|
|
980
1465
|
};
|
|
981
1466
|
function dockerConfigPath() {
|
|
982
1467
|
return import_node_path.default.join(import_node_os.default.homedir(), ".docker", "config.json");
|
|
@@ -984,39 +1469,14 @@ function dockerConfigPath() {
|
|
|
984
1469
|
function containersAuthPath() {
|
|
985
1470
|
return import_node_path.default.join(import_node_os.default.homedir(), ".config", "containers", "auth.json");
|
|
986
1471
|
}
|
|
987
|
-
function
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
try {
|
|
992
|
-
const parsed = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf8"));
|
|
993
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
994
|
-
} catch {
|
|
995
|
-
return {};
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
function writeRegistryAuthFile(filePath, credential) {
|
|
999
|
-
const config = readJsonFile(filePath);
|
|
1000
|
-
const existingAuths = config.auths && typeof config.auths === "object" && !Array.isArray(config.auths) ? config.auths : {};
|
|
1001
|
-
const auths = existingAuths;
|
|
1002
|
-
auths[credential.registry] = {
|
|
1003
|
-
username: credential.username,
|
|
1004
|
-
password: credential.token,
|
|
1005
|
-
auth: Buffer.from(`${credential.username}:${credential.token}`).toString("base64")
|
|
1006
|
-
};
|
|
1007
|
-
config.auths = auths;
|
|
1008
|
-
import_node_fs.default.mkdirSync(import_node_path.default.dirname(filePath), { recursive: true });
|
|
1009
|
-
import_node_fs.default.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
|
|
1010
|
-
`, { mode: 384 });
|
|
1011
|
-
import_node_fs.default.chmodSync(filePath, 384);
|
|
1472
|
+
function configureGitHubAuth(credential, filePaths = [dockerConfigPath(), containersAuthPath()], configureFiles = import_registry_auth.configureGitHubRegistryAuthFiles) {
|
|
1473
|
+
githubCredential = null;
|
|
1474
|
+
configureFiles(filePaths, credential);
|
|
1475
|
+
installGitHubCredentialGeneration(credential);
|
|
1012
1476
|
}
|
|
1013
|
-
function
|
|
1477
|
+
function installGitHubCredentialGeneration(credential) {
|
|
1014
1478
|
githubCredential = credential;
|
|
1015
|
-
if (!credential)
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
writeRegistryAuthFile(dockerConfigPath(), credential);
|
|
1019
|
-
writeRegistryAuthFile(containersAuthPath(), credential);
|
|
1479
|
+
if (!credential) return;
|
|
1020
1480
|
if (credential.missingScopes.length > 0) {
|
|
1021
1481
|
process.stderr.write(
|
|
1022
1482
|
`[r5d-worker] GitHub token is missing scope(s): ${credential.missingScopes.join(", ")}. Sign in with GitHub again if gh or GHCR access fails.
|
|
@@ -1027,6 +1487,21 @@ function configureGitHubAuth(credential) {
|
|
|
1027
1487
|
`);
|
|
1028
1488
|
}
|
|
1029
1489
|
}
|
|
1490
|
+
async function commitCredentialGeneration(prepared, credential, terminatePreviousGeneration, beforeMutation, previousGenerationFenced = false) {
|
|
1491
|
+
const changed = prepared.changed;
|
|
1492
|
+
try {
|
|
1493
|
+
if (changed && !previousGenerationFenced) {
|
|
1494
|
+
githubCredential = null;
|
|
1495
|
+
await terminatePreviousGeneration();
|
|
1496
|
+
}
|
|
1497
|
+
prepared.commit(beforeMutation);
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
prepared.discard();
|
|
1500
|
+
throw error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
|
|
1501
|
+
}
|
|
1502
|
+
installGitHubCredentialGeneration(credential);
|
|
1503
|
+
return changed;
|
|
1504
|
+
}
|
|
1030
1505
|
function containerRegistryEnv() {
|
|
1031
1506
|
if (!githubCredential) {
|
|
1032
1507
|
return {};
|
|
@@ -1056,6 +1531,44 @@ function githubProcessEnv() {
|
|
|
1056
1531
|
...githubCliEnv(githubCredential?.token)
|
|
1057
1532
|
};
|
|
1058
1533
|
}
|
|
1534
|
+
const INHERITED_WORKER_CREDENTIAL_ENV_KEYS = [
|
|
1535
|
+
import_supervisor.WORKER_RUNTIME_ENV,
|
|
1536
|
+
"INVOCATION_ID",
|
|
1537
|
+
"R5D_WORKER_TOKEN",
|
|
1538
|
+
"R5D_TOKEN",
|
|
1539
|
+
"R5DCTL_TOKEN",
|
|
1540
|
+
"R5D_API_KEY",
|
|
1541
|
+
"R5DCTL_API_KEY",
|
|
1542
|
+
"GH_TOKEN",
|
|
1543
|
+
"GITHUB_TOKEN",
|
|
1544
|
+
"GH_ENTERPRISE_TOKEN",
|
|
1545
|
+
"DOCKER_AUTH_CONFIG",
|
|
1546
|
+
"REGISTRY_AUTH_FILE",
|
|
1547
|
+
"R5D_GHCR_AUTH_FILE",
|
|
1548
|
+
"NODE_AUTH_TOKEN",
|
|
1549
|
+
"NPM_TOKEN",
|
|
1550
|
+
"BUN_AUTH_TOKEN"
|
|
1551
|
+
];
|
|
1552
|
+
function workerChildProcessEnvironment(layers, inherited = process.env, platform = process.platform) {
|
|
1553
|
+
const environment = {};
|
|
1554
|
+
const reservedExact = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS);
|
|
1555
|
+
const reservedFolded = new Set(INHERITED_WORKER_CREDENTIAL_ENV_KEYS.map((key) => key.toLowerCase()));
|
|
1556
|
+
const assignLayer = (source, scrubInheritedCredentials) => {
|
|
1557
|
+
for (const [key, value] of Object.entries(source)) {
|
|
1558
|
+
const folded = key.toLowerCase();
|
|
1559
|
+
if (scrubInheritedCredentials && (platform === "win32" ? reservedFolded.has(folded) : reservedExact.has(key))) continue;
|
|
1560
|
+
if (platform === "win32") {
|
|
1561
|
+
for (const existing of Object.keys(environment)) {
|
|
1562
|
+
if (existing.toLowerCase() === folded) delete environment[existing];
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
environment[key] = value;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
assignLayer(inherited, true);
|
|
1569
|
+
for (const layer of layers) assignLayer(layer, false);
|
|
1570
|
+
return environment;
|
|
1571
|
+
}
|
|
1059
1572
|
function primaryProjectBranch(config) {
|
|
1060
1573
|
if (config.branches.length === 0) throw new Error(`Project ${config.projectId} has no configured branches`);
|
|
1061
1574
|
if (config.branches.some(({ branchName }) => branchName === "main")) return "main";
|
|
@@ -1083,6 +1596,8 @@ function describeWorkerSessionTarget(target) {
|
|
|
1083
1596
|
function resolveWorkerSessionTarget(input) {
|
|
1084
1597
|
if (input.target.type === "workspace") {
|
|
1085
1598
|
if (input.target.rootProfile === "visible_projects") {
|
|
1599
|
+
const disabled = [...input.projectConfigById.values()].find(({ executionDisabled }) => executionDisabled);
|
|
1600
|
+
if (disabled) (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(disabled);
|
|
1086
1601
|
return { target: input.target, rootPath: input.projectsRoot };
|
|
1087
1602
|
}
|
|
1088
1603
|
if (!import_node_fs.default.existsSync(import_node_path.default.join(input.workspaceShadowRoot, ".git"))) {
|
|
@@ -1093,6 +1608,7 @@ function resolveWorkerSessionTarget(input) {
|
|
|
1093
1608
|
const target = input.target;
|
|
1094
1609
|
const config = input.projectConfigById.get(target.projectId);
|
|
1095
1610
|
if (!config) throw new Error(`Project ${target.projectId} is missing from the worker workspace configuration`);
|
|
1611
|
+
(0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(config);
|
|
1096
1612
|
if (!config.branches.some(({ branchName }) => branchName === target.branchName)) {
|
|
1097
1613
|
throw new Error(`Project branch ${target.projectId}/${target.branchName} is missing from the worker workspace configuration`);
|
|
1098
1614
|
}
|
|
@@ -1792,6 +2308,7 @@ async function prepareShellEnvForTarget(input) {
|
|
|
1792
2308
|
async function executeCommand(input) {
|
|
1793
2309
|
const startedAt = Date.now();
|
|
1794
2310
|
let timeout;
|
|
2311
|
+
let spawnedProcess;
|
|
1795
2312
|
try {
|
|
1796
2313
|
const targetProcessEnv = await prepareShellEnvForTarget({
|
|
1797
2314
|
target: input.resolvedTarget.target,
|
|
@@ -1803,18 +2320,17 @@ async function executeCommand(input) {
|
|
|
1803
2320
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1804
2321
|
});
|
|
1805
2322
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
2323
|
+
input.assertAdmission();
|
|
1806
2324
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1807
2325
|
cwd,
|
|
1808
2326
|
stdout: "pipe",
|
|
1809
2327
|
stderr: "pipe",
|
|
1810
2328
|
detached: true,
|
|
1811
|
-
env: {
|
|
1812
|
-
...process.env,
|
|
1813
|
-
...githubProcessEnv(),
|
|
1814
|
-
...input.message.env ?? {},
|
|
1815
|
-
...targetProcessEnv
|
|
1816
|
-
}
|
|
2329
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
|
|
1817
2330
|
});
|
|
2331
|
+
spawnedProcess = subprocess;
|
|
2332
|
+
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2333
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
1818
2334
|
activeProcesses.set(input.message.runId, {
|
|
1819
2335
|
process: subprocess,
|
|
1820
2336
|
target: input.resolvedTarget.target,
|
|
@@ -1851,6 +2367,7 @@ async function executeCommand(input) {
|
|
|
1851
2367
|
durationMs: Date.now() - startedAt
|
|
1852
2368
|
};
|
|
1853
2369
|
} catch (error) {
|
|
2370
|
+
if (error instanceof StaleWorkerAdmissionError) throw error;
|
|
1854
2371
|
return {
|
|
1855
2372
|
type: "exec_result",
|
|
1856
2373
|
requestId: input.message.requestId,
|
|
@@ -1864,6 +2381,9 @@ async function executeCommand(input) {
|
|
|
1864
2381
|
if (timeout) {
|
|
1865
2382
|
clearTimeout(timeout);
|
|
1866
2383
|
}
|
|
2384
|
+
if (spawnedProcess) {
|
|
2385
|
+
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2386
|
+
}
|
|
1867
2387
|
activeProcesses.delete(input.message.runId);
|
|
1868
2388
|
}
|
|
1869
2389
|
}
|
|
@@ -1872,6 +2392,7 @@ async function executeStreamingCommand(input) {
|
|
|
1872
2392
|
let started = false;
|
|
1873
2393
|
let timedOut = false;
|
|
1874
2394
|
let timeout;
|
|
2395
|
+
let spawnedProcess;
|
|
1875
2396
|
try {
|
|
1876
2397
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1877
2398
|
sendWorkerMessage(input.ws, {
|
|
@@ -1893,6 +2414,7 @@ async function executeStreamingCommand(input) {
|
|
|
1893
2414
|
});
|
|
1894
2415
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
1895
2416
|
const interactive = input.message.interactive === true;
|
|
2417
|
+
input.assertAdmission();
|
|
1896
2418
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1897
2419
|
cwd,
|
|
1898
2420
|
// Without an explicit stdin the process reads /dev/null and interactive
|
|
@@ -1901,13 +2423,11 @@ async function executeStreamingCommand(input) {
|
|
|
1901
2423
|
stdout: "pipe",
|
|
1902
2424
|
stderr: "pipe",
|
|
1903
2425
|
detached: true,
|
|
1904
|
-
env: {
|
|
1905
|
-
...process.env,
|
|
1906
|
-
...githubProcessEnv(),
|
|
1907
|
-
...input.message.env ?? {},
|
|
1908
|
-
...targetProcessEnv
|
|
1909
|
-
}
|
|
2426
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv])
|
|
1910
2427
|
});
|
|
2428
|
+
spawnedProcess = subprocess;
|
|
2429
|
+
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2430
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
1911
2431
|
activeProcesses.set(input.message.runId, {
|
|
1912
2432
|
process: subprocess,
|
|
1913
2433
|
target: input.resolvedTarget.target,
|
|
@@ -1972,6 +2492,7 @@ async function executeStreamingCommand(input) {
|
|
|
1972
2492
|
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
1973
2493
|
sendWorkerMessage(input.ws, terminal);
|
|
1974
2494
|
} catch (error) {
|
|
2495
|
+
if (!started && error instanceof StaleWorkerAdmissionError) throw error;
|
|
1975
2496
|
const message = error instanceof Error ? error.message : String(error);
|
|
1976
2497
|
if (started) {
|
|
1977
2498
|
const terminal = {
|
|
@@ -1995,10 +2516,27 @@ async function executeStreamingCommand(input) {
|
|
|
1995
2516
|
clearTimeout(timeout);
|
|
1996
2517
|
}
|
|
1997
2518
|
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2519
|
+
if (spawnedProcess) {
|
|
2520
|
+
await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
|
|
2521
|
+
}
|
|
1998
2522
|
activeProcesses.delete(input.message.runId);
|
|
1999
2523
|
cancelledProcessRuns.delete(input.message.runId);
|
|
2000
2524
|
}
|
|
2001
2525
|
}
|
|
2526
|
+
async function reapCompletedCredentialBearingProcessGroup(runId, subprocess) {
|
|
2527
|
+
try {
|
|
2528
|
+
await (0, import_process_tree.terminateProcessTree)(subprocess);
|
|
2529
|
+
if (credentialBearingProcessGroups.get(subprocess.pid) === subprocess) {
|
|
2530
|
+
credentialBearingProcessGroups.delete(subprocess.pid);
|
|
2531
|
+
credentialBearingProcessGroupTargets.delete(subprocess.pid);
|
|
2532
|
+
}
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
process.stderr.write(
|
|
2535
|
+
`[r5d-worker] retained unreaped credential-bearing process group ${subprocess.pid} from ${runId}: ${error instanceof Error ? error.message : String(error)}
|
|
2536
|
+
`
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2002
2540
|
function closeProcessStdin(active) {
|
|
2003
2541
|
if (!active?.stdin) {
|
|
2004
2542
|
return;
|
|
@@ -2018,6 +2556,15 @@ function sendWorkerMessage(ws, message) {
|
|
|
2018
2556
|
`);
|
|
2019
2557
|
}
|
|
2020
2558
|
}
|
|
2559
|
+
function sendWorkerMessageFromCurrentSource(ws, message) {
|
|
2560
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) return;
|
|
2561
|
+
try {
|
|
2562
|
+
ws.send(JSON.stringify(message));
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
|
|
2565
|
+
`);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2021
2568
|
function buildActiveProcessReports() {
|
|
2022
2569
|
return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
|
|
2023
2570
|
runId,
|
|
@@ -2097,6 +2644,19 @@ rl.on("line", (line) => {
|
|
|
2097
2644
|
|
|
2098
2645
|
if (message.type === "close") {
|
|
2099
2646
|
ptyProcess.kill();
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
if (message.type === "force_close") {
|
|
2651
|
+
if (process.platform !== "win32" && Number.isSafeInteger(ptyProcess.pid) && ptyProcess.pid > 0) {
|
|
2652
|
+
try {
|
|
2653
|
+
process.kill(-ptyProcess.pid, "SIGKILL");
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
if (!error || error.code !== "ESRCH") throw error;
|
|
2656
|
+
}
|
|
2657
|
+
} else {
|
|
2658
|
+
ptyProcess.kill("SIGKILL");
|
|
2659
|
+
}
|
|
2100
2660
|
}
|
|
2101
2661
|
} catch (error) {
|
|
2102
2662
|
send({ type: "error", error: error instanceof Error ? error.message : String(error) });
|
|
@@ -2128,14 +2688,26 @@ function nodePtyModulePaths() {
|
|
|
2128
2688
|
}
|
|
2129
2689
|
function createNodePtyBridge(options) {
|
|
2130
2690
|
const child = (0, import_node_child_process.spawn)("node", ["-e", PTY_BRIDGE_SCRIPT], {
|
|
2131
|
-
env: {
|
|
2132
|
-
...process.env,
|
|
2133
|
-
NODE_PATH: nodePtyModulePaths()
|
|
2134
|
-
}
|
|
2691
|
+
env: workerChildProcessEnvironment([{ NODE_PATH: nodePtyModulePaths() }])
|
|
2135
2692
|
});
|
|
2136
2693
|
let lineBuffer = "";
|
|
2137
2694
|
let emittedTerminalEvent = false;
|
|
2138
2695
|
let stderr = "";
|
|
2696
|
+
let resolveTerminal;
|
|
2697
|
+
const terminal = new Promise((resolve) => {
|
|
2698
|
+
resolveTerminal = resolve;
|
|
2699
|
+
});
|
|
2700
|
+
const markTerminal = () => {
|
|
2701
|
+
resolveTerminal?.();
|
|
2702
|
+
resolveTerminal = void 0;
|
|
2703
|
+
};
|
|
2704
|
+
const terminalWithin = async (timeoutMs) => await Promise.race([
|
|
2705
|
+
terminal.then(() => true),
|
|
2706
|
+
new Promise((resolve) => {
|
|
2707
|
+
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
2708
|
+
timer.unref();
|
|
2709
|
+
})
|
|
2710
|
+
]);
|
|
2139
2711
|
const sendToChild = (message) => {
|
|
2140
2712
|
if (child.stdin.writable) {
|
|
2141
2713
|
child.stdin.write(`${JSON.stringify(message)}
|
|
@@ -2153,11 +2725,13 @@ function createNodePtyBridge(options) {
|
|
|
2153
2725
|
}
|
|
2154
2726
|
if (event.type === "exit") {
|
|
2155
2727
|
emittedTerminalEvent = true;
|
|
2728
|
+
markTerminal();
|
|
2156
2729
|
options.onExit({ exitCode: event.exitCode, signal: event.signal });
|
|
2157
2730
|
return;
|
|
2158
2731
|
}
|
|
2159
2732
|
if (event.type === "error") {
|
|
2160
2733
|
emittedTerminalEvent = true;
|
|
2734
|
+
markTerminal();
|
|
2161
2735
|
options.onError(new Error(event.error));
|
|
2162
2736
|
}
|
|
2163
2737
|
};
|
|
@@ -2186,9 +2760,11 @@ function createNodePtyBridge(options) {
|
|
|
2186
2760
|
});
|
|
2187
2761
|
child.on("error", (error) => {
|
|
2188
2762
|
emittedTerminalEvent = true;
|
|
2763
|
+
markTerminal();
|
|
2189
2764
|
options.onError(error);
|
|
2190
2765
|
});
|
|
2191
2766
|
child.on("exit", (code, signal) => {
|
|
2767
|
+
markTerminal();
|
|
2192
2768
|
if (emittedTerminalEvent) {
|
|
2193
2769
|
return;
|
|
2194
2770
|
}
|
|
@@ -2209,12 +2785,15 @@ function createNodePtyBridge(options) {
|
|
|
2209
2785
|
sendToChild({ type: "resize", cols, rows });
|
|
2210
2786
|
},
|
|
2211
2787
|
kill() {
|
|
2788
|
+
void this.terminate().catch((error) => options.onTerminationError(error instanceof Error ? error : new Error(String(error))));
|
|
2789
|
+
},
|
|
2790
|
+
async terminate() {
|
|
2212
2791
|
sendToChild({ type: "close" });
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2792
|
+
if (await terminalWithin(500)) return;
|
|
2793
|
+
sendToChild({ type: "force_close" });
|
|
2794
|
+
if (await terminalWithin(500)) return;
|
|
2795
|
+
child.kill("SIGKILL");
|
|
2796
|
+
if (!await terminalWithin(500)) throw new Error(`PTY bridge ${child.pid ?? "unknown"} did not terminate after SIGKILL`);
|
|
2218
2797
|
}
|
|
2219
2798
|
};
|
|
2220
2799
|
}
|
|
@@ -2252,6 +2831,7 @@ async function openPty(input) {
|
|
|
2252
2831
|
});
|
|
2253
2832
|
}
|
|
2254
2833
|
});
|
|
2834
|
+
input.assertAdmission();
|
|
2255
2835
|
const ptyProcess = createNodePtyBridge({
|
|
2256
2836
|
file: shell.file,
|
|
2257
2837
|
args: shell.args,
|
|
@@ -2260,14 +2840,7 @@ async function openPty(input) {
|
|
|
2260
2840
|
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
2261
2841
|
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
2262
2842
|
cwd: input.resolvedTarget.rootPath,
|
|
2263
|
-
env: {
|
|
2264
|
-
...process.env,
|
|
2265
|
-
...githubProcessEnv(),
|
|
2266
|
-
...input.message.env ?? {},
|
|
2267
|
-
...planProcessEnv,
|
|
2268
|
-
...targetProcessEnv,
|
|
2269
|
-
...shell.env ?? {}
|
|
2270
|
-
}
|
|
2843
|
+
env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, planProcessEnv, targetProcessEnv, shell.env ?? {}])
|
|
2271
2844
|
},
|
|
2272
2845
|
onOpened: () => {
|
|
2273
2846
|
sendWorkerMessage(input.ws, {
|
|
@@ -2302,6 +2875,14 @@ async function openPty(input) {
|
|
|
2302
2875
|
ptyId: input.message.ptyId,
|
|
2303
2876
|
error: error.message
|
|
2304
2877
|
});
|
|
2878
|
+
},
|
|
2879
|
+
onTerminationError: (error) => {
|
|
2880
|
+
sendWorkerMessage(input.ws, {
|
|
2881
|
+
type: "pty_error",
|
|
2882
|
+
requestId: input.message.requestId,
|
|
2883
|
+
ptyId: input.message.ptyId,
|
|
2884
|
+
error: error.message
|
|
2885
|
+
});
|
|
2305
2886
|
}
|
|
2306
2887
|
});
|
|
2307
2888
|
activePtys.set(input.message.ptyId, {
|
|
@@ -2334,14 +2915,62 @@ function closePty(message) {
|
|
|
2334
2915
|
if (!ptyProcess) {
|
|
2335
2916
|
return;
|
|
2336
2917
|
}
|
|
2337
|
-
activePtys.delete(message.ptyId);
|
|
2338
2918
|
ptyProcess.kill();
|
|
2339
2919
|
}
|
|
2340
2920
|
function closeAllPtys() {
|
|
2341
2921
|
for (const ptyProcess of activePtys.values()) {
|
|
2342
2922
|
ptyProcess.kill();
|
|
2343
2923
|
}
|
|
2344
|
-
|
|
2924
|
+
}
|
|
2925
|
+
async function terminateCredentialBearingChildren(children) {
|
|
2926
|
+
const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
|
|
2927
|
+
const failures = results.flatMap(
|
|
2928
|
+
(result, index) => result.status === "rejected" ? [new Error(`Failed to terminate ${children[index].label}`, { cause: result.reason })] : []
|
|
2929
|
+
);
|
|
2930
|
+
if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
|
|
2931
|
+
}
|
|
2932
|
+
async function terminateCredentialBearingChildrenWithRetention(children) {
|
|
2933
|
+
const results = await Promise.allSettled(children.map(async ({ terminate }) => await terminate()));
|
|
2934
|
+
const failures = [];
|
|
2935
|
+
results.forEach((result, index) => {
|
|
2936
|
+
const child = children[index];
|
|
2937
|
+
if (result.status === "fulfilled") {
|
|
2938
|
+
child.onTerminated();
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
failures.push(new Error(`Failed to terminate ${child.label}`, { cause: result.reason }));
|
|
2942
|
+
});
|
|
2943
|
+
if (failures.length > 0) throw new AggregateError(failures, "Credential-bearing child termination was incomplete");
|
|
2944
|
+
}
|
|
2945
|
+
async function terminateActiveCredentialBearingChildren() {
|
|
2946
|
+
const processGroups = [...credentialBearingProcessGroups.entries()];
|
|
2947
|
+
const ptyEntries = [...activePtys.entries()];
|
|
2948
|
+
for (const active of activeProcesses.values()) closeProcessStdin(active);
|
|
2949
|
+
await terminateCredentialBearingChildrenWithRetention([
|
|
2950
|
+
...processGroups.map(([processGroupId, subprocess]) => ({
|
|
2951
|
+
label: `process group ${processGroupId}`,
|
|
2952
|
+
terminate: async () => await (0, import_process_tree.terminateProcessTree)(subprocess),
|
|
2953
|
+
onTerminated: () => {
|
|
2954
|
+
if (credentialBearingProcessGroups.get(processGroupId) === subprocess) {
|
|
2955
|
+
credentialBearingProcessGroups.delete(processGroupId);
|
|
2956
|
+
credentialBearingProcessGroupTargets.delete(processGroupId);
|
|
2957
|
+
}
|
|
2958
|
+
for (const [runId, active] of activeProcesses) {
|
|
2959
|
+
if (active.process === subprocess) activeProcesses.delete(runId);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
})),
|
|
2963
|
+
...ptyEntries.map(([ptyId, active]) => ({
|
|
2964
|
+
label: `PTY ${ptyId}`,
|
|
2965
|
+
terminate: async () => await active.terminate(),
|
|
2966
|
+
onTerminated: () => {
|
|
2967
|
+
if (activePtys.get(ptyId) === active) activePtys.delete(ptyId);
|
|
2968
|
+
}
|
|
2969
|
+
}))
|
|
2970
|
+
]);
|
|
2971
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || credentialBearingProcessGroupTargets.size > 0 || activePtys.size > 0) {
|
|
2972
|
+
throw new Error("Credential-bearing child ownership remained after termination");
|
|
2973
|
+
}
|
|
2345
2974
|
}
|
|
2346
2975
|
function websocketUrl(baseUrl, label) {
|
|
2347
2976
|
const url = new URL("/worker/ws", baseUrl);
|
|
@@ -2351,6 +2980,7 @@ function websocketUrl(baseUrl, label) {
|
|
|
2351
2980
|
return url.toString();
|
|
2352
2981
|
}
|
|
2353
2982
|
async function startWorker(options) {
|
|
2983
|
+
advanceWorkerAdmissionGeneration();
|
|
2354
2984
|
process.on("uncaughtException", (error) => {
|
|
2355
2985
|
process.stderr.write(`[r5d-worker] uncaught exception: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
2356
2986
|
`);
|
|
@@ -2369,8 +2999,15 @@ async function startWorker(options) {
|
|
|
2369
2999
|
const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
2370
3000
|
const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
2371
3001
|
const workspaceShadowRoot = import_node_path.default.join(syncRoot, "workspace");
|
|
3002
|
+
const projectWorktreeSnapshotsRoot = import_node_path.default.join(syncRoot, "project-worktree-snapshots");
|
|
2372
3003
|
const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2373
3004
|
const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
3005
|
+
(0, import_managed_paths.assertDisjointManagedRoots)([
|
|
3006
|
+
{ label: "projects root", rootPath: projectsRoot },
|
|
3007
|
+
{ label: "sync root", rootPath: syncRoot },
|
|
3008
|
+
{ label: "artifacts root", rootPath: artifactRoot },
|
|
3009
|
+
{ label: "plans root", rootPath: planRoot }
|
|
3010
|
+
]);
|
|
2374
3011
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
2375
3012
|
`);
|
|
2376
3013
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
@@ -2383,26 +3020,43 @@ async function startWorker(options) {
|
|
|
2383
3020
|
`);
|
|
2384
3021
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
2385
3022
|
`);
|
|
2386
|
-
const snapshotCleanup = (0, import_project_worktrees.cleanupStaleProjectWorktreeSnapshots)();
|
|
2387
|
-
if (snapshotCleanup.removed.length > 0) {
|
|
2388
|
-
process.stdout.write(`[r5d-worker] removed ${snapshotCleanup.removed.length} stale project snapshot(s)
|
|
2389
|
-
`);
|
|
2390
|
-
}
|
|
2391
|
-
for (const failure of snapshotCleanup.failed) {
|
|
2392
|
-
process.stderr.write(`[r5d-worker] failed to remove stale project snapshot ${failure.path}: ${failure.error}
|
|
2393
|
-
`);
|
|
2394
|
-
}
|
|
2395
3023
|
runGit(["--version"]);
|
|
2396
3024
|
await verifyR5dctlAuth(baseUrl, token);
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
3025
|
+
const initializedWorkspaceState = await workspaceSyncSingleFlight.runExclusive(() => {
|
|
3026
|
+
if (!startupProjectSnapshotRecoveryCompleted) {
|
|
3027
|
+
const snapshotRecovery = (0, import_project_worktrees.recoverStaleProjectWorktreeSnapshots)({
|
|
3028
|
+
projectsRoot,
|
|
3029
|
+
temporaryRoot: projectWorktreeSnapshotsRoot
|
|
3030
|
+
});
|
|
3031
|
+
if (snapshotRecovery.restored.length > 0) {
|
|
3032
|
+
process.stdout.write(`[r5d-worker] restored ${snapshotRecovery.restored.length} interrupted project snapshot(s)
|
|
3033
|
+
`);
|
|
3034
|
+
}
|
|
3035
|
+
if (snapshotRecovery.removed.length > 0) {
|
|
3036
|
+
process.stdout.write(`[r5d-worker] removed ${snapshotRecovery.removed.length} completed/incomplete project snapshot(s)
|
|
3037
|
+
`);
|
|
3038
|
+
}
|
|
3039
|
+
for (const failure of snapshotRecovery.failed) {
|
|
3040
|
+
process.stderr.write(`[r5d-worker] failed to recover stale project snapshot ${failure.path}: ${failure.error}
|
|
3041
|
+
`);
|
|
3042
|
+
}
|
|
3043
|
+
if (snapshotRecovery.failed.length > 0) {
|
|
3044
|
+
throw new Error("Project snapshot recovery is incomplete; refusing to mutate project checkouts");
|
|
3045
|
+
}
|
|
3046
|
+
startupProjectSnapshotRecoveryCompleted = true;
|
|
3047
|
+
}
|
|
3048
|
+
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
3049
|
+
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
3050
|
+
import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
|
|
3051
|
+
import_node_fs.default.mkdirSync(planRoot, { recursive: true });
|
|
3052
|
+
const store = new import_project_workspace_state.ProjectWorkspaceStateStore({
|
|
3053
|
+
stateRoot: import_node_path.default.join(syncRoot, "project-state"),
|
|
3054
|
+
projectsRoot
|
|
3055
|
+
});
|
|
3056
|
+
return { store, state: store.read() };
|
|
2404
3057
|
});
|
|
2405
|
-
|
|
3058
|
+
const projectWorkspaceStateStore = initializedWorkspaceState.store;
|
|
3059
|
+
let projectWorkspaceState = initializedWorkspaceState.state;
|
|
2406
3060
|
const projectConfigById = /* @__PURE__ */ new Map();
|
|
2407
3061
|
const pendingCheckouts = /* @__PURE__ */ new Map();
|
|
2408
3062
|
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
@@ -2418,6 +3072,7 @@ async function startWorker(options) {
|
|
|
2418
3072
|
let workspaceAutomaticTimer;
|
|
2419
3073
|
let workspacePeriodicTimer;
|
|
2420
3074
|
let pendingAutomaticTrigger;
|
|
3075
|
+
const pendingCreatedBranchPublicationNotBefore = /* @__PURE__ */ new Map();
|
|
2421
3076
|
let automaticSyncInFlight = false;
|
|
2422
3077
|
let terminalReplayTimer;
|
|
2423
3078
|
let workspaceSyncRequestsInFlight = 0;
|
|
@@ -2432,7 +3087,7 @@ async function startWorker(options) {
|
|
|
2432
3087
|
const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
|
|
2433
3088
|
const workspaceProjectRelativePath = (projectId, branchName) => import_node_path.default.posix.join("projects", projectId, "branches", encodeURIComponent(branchName));
|
|
2434
3089
|
const workspacePlanRelativePath = (projectId, branchName) => import_node_path.default.posix.join("plans", projectId, encodeURIComponent(branchName));
|
|
2435
|
-
const projectConnection = (project) => {
|
|
3090
|
+
const projectConnection = (project, desiredFiles) => {
|
|
2436
3091
|
const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
|
|
2437
3092
|
const originUrl = project.repoHttpUrl ?? mirrorUrl;
|
|
2438
3093
|
const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
|
|
@@ -2441,7 +3096,8 @@ async function startWorker(options) {
|
|
|
2441
3096
|
{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader },
|
|
2442
3097
|
{ remoteUrl: originUrl, authHeader: originHeader }
|
|
2443
3098
|
],
|
|
2444
|
-
credentialStorePathForProject(project.projectId)
|
|
3099
|
+
credentialStorePathForProject(project.projectId),
|
|
3100
|
+
desiredFiles
|
|
2445
3101
|
);
|
|
2446
3102
|
if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
|
|
2447
3103
|
return {
|
|
@@ -2452,11 +3108,12 @@ async function startWorker(options) {
|
|
|
2452
3108
|
credentialHelper
|
|
2453
3109
|
};
|
|
2454
3110
|
};
|
|
2455
|
-
const projectMirrorConnection = (projectId) => {
|
|
3111
|
+
const projectMirrorConnection = (projectId, desiredFiles) => {
|
|
2456
3112
|
const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, projectId);
|
|
2457
3113
|
const credentialHelper = credentialHelperForRemotes(
|
|
2458
3114
|
[{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader }],
|
|
2459
|
-
credentialStorePathForProject(projectId)
|
|
3115
|
+
credentialStorePathForProject(projectId),
|
|
3116
|
+
desiredFiles
|
|
2460
3117
|
);
|
|
2461
3118
|
if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
|
|
2462
3119
|
return { mirrorUrl, credentialHelper, mirrorCredentialUsername: workerCredentialUsername };
|
|
@@ -2529,6 +3186,29 @@ async function startWorker(options) {
|
|
|
2529
3186
|
readyProjectIds.delete(project.projectId);
|
|
2530
3187
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
2531
3188
|
};
|
|
3189
|
+
const deactivatePreserveOnlyProjectBranch = (project, branchName) => {
|
|
3190
|
+
const branchPath = configuredProjectBranchPath(projectsRoot, project, branchName);
|
|
3191
|
+
pendingCheckouts.delete(projectBranchKey(project.projectId, branchName));
|
|
3192
|
+
if (!import_node_fs.default.existsSync(branchPath)) return;
|
|
3193
|
+
if (!hasProjectWorktree(branchPath)) {
|
|
3194
|
+
throw new Error(`Preserve-only project branch path ${project.projectId}/${branchName} is not a managed Git checkout`);
|
|
3195
|
+
}
|
|
3196
|
+
if (project.branches.length === 0) {
|
|
3197
|
+
throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} without an active anchor branch`);
|
|
3198
|
+
}
|
|
3199
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
3200
|
+
if (branchName === primaryBranchName) {
|
|
3201
|
+
throw new Error(`Cannot hide preserve-only project branch ${project.projectId}/${branchName} because it is the active anchor`);
|
|
3202
|
+
}
|
|
3203
|
+
(0, import_project_worktrees.deleteLinkedProjectBranch)({
|
|
3204
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
3205
|
+
primaryBranchName,
|
|
3206
|
+
branchName
|
|
3207
|
+
});
|
|
3208
|
+
import_node_fs.default.rmSync(import_node_path.default.join(planRoot, project.projectId, ...branchName.split("/")), { recursive: true, force: true });
|
|
3209
|
+
readyProjectIds.delete(project.projectId);
|
|
3210
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
3211
|
+
};
|
|
2532
3212
|
const cleanupConfigForDurableDeletion = (deletion) => {
|
|
2533
3213
|
const desiredLayout = projectWorkspaceState.desiredProjects.find(
|
|
2534
3214
|
(project) => project.projectId === deletion.projectId && project.checkoutPathSegments[0] === deletion.checkoutPathSegments[0] && project.checkoutPathSegments[1] === deletion.checkoutPathSegments[1]
|
|
@@ -2543,8 +3223,13 @@ async function startWorker(options) {
|
|
|
2543
3223
|
repoHttpUrl: liveConfig?.repoHttpUrl ?? null,
|
|
2544
3224
|
repoAuthHeader: liveConfig?.repoAuthHeader ?? null,
|
|
2545
3225
|
defaultBranch,
|
|
3226
|
+
repositoryTransitionId: liveConfig?.repositoryTransitionId ?? null,
|
|
3227
|
+
executionDisabled: false,
|
|
3228
|
+
mirrorWritesDisabled: false,
|
|
3229
|
+
preserveOnlyBranches: [],
|
|
2546
3230
|
branches: branchNames.map(
|
|
2547
3231
|
(branchName) => liveConfig?.branches.find((branch) => branch.branchName === branchName) ?? {
|
|
3232
|
+
branchId: "deleted",
|
|
2548
3233
|
branchName,
|
|
2549
3234
|
sourceBranchName: null,
|
|
2550
3235
|
// Deletion never consumes the baseline, but retain a structurally
|
|
@@ -2559,9 +3244,18 @@ async function startWorker(options) {
|
|
|
2559
3244
|
const cleanupConfig = cleanupConfigForDurableDeletion(deletion);
|
|
2560
3245
|
if (deletion.kind === "project") {
|
|
2561
3246
|
if (!deletion.projectDeleted) {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
3247
|
+
const nextProject = projectConfigById.get(deletion.projectId);
|
|
3248
|
+
if (!nextProject) throw new Error(`Checkout-path move lost desired project ${deletion.projectId}`);
|
|
3249
|
+
(0, import_workspace_path_move.preserveProjectCheckoutPathMove)({
|
|
3250
|
+
oldProjectRoot: deletion.managedPath,
|
|
3251
|
+
newProjectRoot: configuredProjectRoot(projectsRoot, nextProject),
|
|
3252
|
+
projectsDurabilityRoot: projectsRoot,
|
|
3253
|
+
outerWorkspaceRoot: workspaceShadowRoot,
|
|
3254
|
+
branches: deletion.branchNames.map((branchName) => ({
|
|
3255
|
+
branchName,
|
|
3256
|
+
outerWorkspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, branchName)
|
|
3257
|
+
}))
|
|
3258
|
+
});
|
|
2565
3259
|
readyProjectIds.delete(deletion.projectId);
|
|
2566
3260
|
reconciledProjectConfigFingerprints.delete(deletion.projectId);
|
|
2567
3261
|
continue;
|
|
@@ -2594,32 +3288,15 @@ async function startWorker(options) {
|
|
|
2594
3288
|
});
|
|
2595
3289
|
}
|
|
2596
3290
|
};
|
|
2597
|
-
const pruneStaleOuterWorkspaceEntries = (entries) => {
|
|
2598
|
-
const remove = (candidate) => {
|
|
2599
|
-
if (candidate) import_node_fs.default.rmSync(candidate, { recursive: true, force: true });
|
|
2600
|
-
};
|
|
2601
|
-
for (const entry of entries) {
|
|
2602
|
-
if (entry.kind === "branch") {
|
|
2603
|
-
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
2604
|
-
projectId: entry.projectId,
|
|
2605
|
-
branchName: entry.branchName
|
|
2606
|
-
});
|
|
2607
|
-
remove(entry.projectTreePath);
|
|
2608
|
-
remove(entry.planTreePath);
|
|
2609
|
-
continue;
|
|
2610
|
-
}
|
|
2611
|
-
remove(entry.projectTreePath);
|
|
2612
|
-
remove(entry.planTreePath);
|
|
2613
|
-
for (const branch of entry.branches) {
|
|
2614
|
-
remove(branch.projectTreePath);
|
|
2615
|
-
remove(branch.planTreePath);
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
};
|
|
2619
3291
|
const effectiveWorkerProjects = (projects) => {
|
|
2620
3292
|
const incomingById = new Map(projects.map((project) => [project.projectId, project]));
|
|
2621
|
-
const
|
|
2622
|
-
|
|
3293
|
+
const preserveOnlyByKey = new Map(
|
|
3294
|
+
projects.flatMap(
|
|
3295
|
+
(project) => project.preserveOnlyBranches.map((branch) => [projectBranchKey(project.projectId, branch.branchName), branch])
|
|
3296
|
+
)
|
|
3297
|
+
);
|
|
3298
|
+
const pending = new Map(
|
|
3299
|
+
projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [projectBranchKey(branch.projectId, branch.branchName), branch])
|
|
2623
3300
|
);
|
|
2624
3301
|
return projectWorkspaceState.effectiveDesiredProjects.flatMap((layout) => {
|
|
2625
3302
|
const incoming = incomingById.get(layout.projectId);
|
|
@@ -2627,10 +3304,21 @@ async function startWorker(options) {
|
|
|
2627
3304
|
const existing = projectConfigById.get(layout.projectId);
|
|
2628
3305
|
const branches = layout.branches.flatMap(({ branchName }) => {
|
|
2629
3306
|
const configured = incoming.branches.find((branch) => branch.branchName === branchName);
|
|
2630
|
-
|
|
3307
|
+
const pendingBranch = pending.get(projectBranchKey(layout.projectId, branchName));
|
|
3308
|
+
const preservedBranch = preserveOnlyByKey.get(projectBranchKey(layout.projectId, branchName));
|
|
3309
|
+
const pendingIncarnationIsAuthorized = (0, import_workspace_preserve_only_policy.creatorLocalProjectBranchIsAuthorized)({
|
|
3310
|
+
pendingBranchId: pendingBranch?.branchId,
|
|
3311
|
+
preserveOnlyBranchId: preservedBranch?.branchId
|
|
3312
|
+
});
|
|
3313
|
+
const disposition = (0, import_workspace_preserve_only_policy.workerProjectBranchDisposition)({
|
|
3314
|
+
activeConfigured: Boolean(configured),
|
|
3315
|
+
locallyPendingCreated: pendingIncarnationIsAuthorized
|
|
3316
|
+
});
|
|
3317
|
+
if (configured && disposition === "active") return [configured];
|
|
3318
|
+
if (disposition === "preserve_only") return [];
|
|
2631
3319
|
const retained = existing?.branches.find((branch) => branch.branchName === branchName);
|
|
2632
3320
|
if (retained) return [retained];
|
|
2633
|
-
if (!
|
|
3321
|
+
if (!pendingBranch?.branchId) return [];
|
|
2634
3322
|
const branchPath = configuredProjectBranchPath(
|
|
2635
3323
|
projectsRoot,
|
|
2636
3324
|
{ ...incoming, checkoutPathSegments: layout.checkoutPathSegments },
|
|
@@ -2639,6 +3327,7 @@ async function startWorker(options) {
|
|
|
2639
3327
|
if (!hasProjectWorktree(branchPath)) return [];
|
|
2640
3328
|
return [
|
|
2641
3329
|
{
|
|
3330
|
+
branchId: pendingBranch.branchId,
|
|
2642
3331
|
branchName,
|
|
2643
3332
|
sourceBranchName: null,
|
|
2644
3333
|
baseCommitHash: runGit(["rev-parse", "HEAD"], { cwd: branchPath })
|
|
@@ -2648,41 +3337,94 @@ async function startWorker(options) {
|
|
|
2648
3337
|
return [{ ...incoming, checkoutPathSegments: [...layout.checkoutPathSegments], branches }];
|
|
2649
3338
|
});
|
|
2650
3339
|
};
|
|
2651
|
-
const
|
|
3340
|
+
const activeWorkspaceMutationTargets = () => [
|
|
3341
|
+
...Array.from(activeProcesses.values(), ({ target }) => target),
|
|
3342
|
+
...credentialBearingProcessGroupTargets.values(),
|
|
3343
|
+
...workspaceSyncPriorityProcessTargets.values(),
|
|
3344
|
+
...workspaceSyncPriorityOperationTargets.values(),
|
|
3345
|
+
...Array.from(activePtys.values(), ({ target }) => target),
|
|
3346
|
+
...workspaceSyncPriorityPtyTargets.values()
|
|
3347
|
+
];
|
|
3348
|
+
const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
|
|
3349
|
+
const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
|
|
3350
|
+
const activeTargets = activeWorkspaceMutationTargets();
|
|
3351
|
+
return projectConfigById.get(projectId)?.executionDisabled === true || hasProjectWorktree(branchPath) && (0, import_project_worktrees.projectWorktreeOperationInProgress)(branchPath) || canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.projectBranchHasActiveWorkspaceTarget)(projectId, branchName, activeTargets);
|
|
3352
|
+
};
|
|
3353
|
+
const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
|
|
3354
|
+
const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
|
|
2652
3355
|
const mounts = [];
|
|
2653
|
-
|
|
3356
|
+
const creatorLocalIncarnations = new Map(
|
|
3357
|
+
projectWorkspaceState.locallyPendingCreatedBranches.flatMap(
|
|
3358
|
+
(branch) => branch.branchId ? [[projectBranchKey(branch.projectId, branch.branchName), branch.branchId]] : []
|
|
3359
|
+
)
|
|
3360
|
+
);
|
|
3361
|
+
const checkoutPathMoveBranches = new Set(
|
|
3362
|
+
projectWorkspaceState.pendingTreeDeletions.flatMap(
|
|
3363
|
+
(deletion) => deletion.kind === "project" && !deletion.projectDeleted ? deletion.branchNames.map((branchName) => projectBranchKey(deletion.projectId, branchName)) : []
|
|
3364
|
+
)
|
|
3365
|
+
);
|
|
3366
|
+
for (const project of [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
|
|
2654
3367
|
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2655
3368
|
for (const branch of [...project.branches].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
2656
3369
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
3370
|
+
const branchKey = projectBranchKey(project.projectId, branch.branchName);
|
|
3371
|
+
const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
|
|
3372
|
+
const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
|
|
2657
3373
|
mounts.push({
|
|
2658
3374
|
id: projectMountId(project.projectId, branch.branchName),
|
|
3375
|
+
hydrationIncarnationKey: branch.branchId,
|
|
2659
3376
|
sourcePath: branchPath,
|
|
3377
|
+
durabilityRootPath: projectsRoot,
|
|
2660
3378
|
workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
|
|
2661
3379
|
sourceMode: "git",
|
|
2662
3380
|
hydrateDeletionMode: "git",
|
|
2663
|
-
|
|
3381
|
+
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3382
|
+
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3383
|
+
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3384
|
+
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
2664
3385
|
});
|
|
2665
3386
|
mounts.push({
|
|
2666
3387
|
id: projectPlanMountId(project.projectId, branch.branchName),
|
|
3388
|
+
hydrationIncarnationKey: branch.branchId,
|
|
2667
3389
|
sourcePath: import_node_path.default.join(planRoot, project.projectId, ...branch.branchName.split("/")),
|
|
3390
|
+
durabilityRootPath: planRoot,
|
|
2668
3391
|
workspaceRelativePath: workspacePlanRelativePath(project.projectId, branch.branchName),
|
|
2669
3392
|
sourceMode: "all",
|
|
2670
|
-
hydrateDeletionMode: "all"
|
|
3393
|
+
hydrateDeletionMode: "all",
|
|
3394
|
+
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3395
|
+
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3396
|
+
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3397
|
+
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
2671
3398
|
});
|
|
2672
3399
|
}
|
|
2673
3400
|
void projectRoot;
|
|
2674
3401
|
}
|
|
2675
3402
|
mounts.push({
|
|
2676
3403
|
id: "workspace-plans",
|
|
3404
|
+
hydrationIncarnationKey: "workspace-plans-v1",
|
|
2677
3405
|
sourcePath: import_node_path.default.join(planRoot, "workspace"),
|
|
3406
|
+
durabilityRootPath: planRoot,
|
|
2678
3407
|
workspaceRelativePath: "workspace-plans",
|
|
2679
3408
|
sourceMode: "all",
|
|
2680
|
-
hydrateDeletionMode: "all"
|
|
3409
|
+
hydrateDeletionMode: "all",
|
|
3410
|
+
// Workspace-level agents receive this directory through R5D_PLANS_DIR.
|
|
3411
|
+
// Keep both projection and hydration away from it while any visible-
|
|
3412
|
+
// projects process, PTY, or built-in write/edit is active or reserved.
|
|
3413
|
+
busy: () => {
|
|
3414
|
+
const activeTargets = activeWorkspaceMutationTargets();
|
|
3415
|
+
return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
|
|
3416
|
+
},
|
|
3417
|
+
busyForRecovery: () => {
|
|
3418
|
+
const activeTargets = activeWorkspaceMutationTargets();
|
|
3419
|
+
return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
|
|
3420
|
+
}
|
|
2681
3421
|
});
|
|
2682
3422
|
for (const deletion of pendingMirrorDeletes.values()) {
|
|
2683
3423
|
mounts.push({
|
|
2684
3424
|
id: deletion.id,
|
|
3425
|
+
hydrationIncarnationKey: deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`,
|
|
2685
3426
|
sourcePath: deletion.sourcePath,
|
|
3427
|
+
durabilityRootPath: projectsRoot,
|
|
2686
3428
|
workspaceRelativePath: deletion.workspaceRelativePath,
|
|
2687
3429
|
sourceMode: "git",
|
|
2688
3430
|
hydrateDeletionMode: "git",
|
|
@@ -2690,7 +3432,9 @@ async function startWorker(options) {
|
|
|
2690
3432
|
});
|
|
2691
3433
|
mounts.push({
|
|
2692
3434
|
id: `${deletion.id}:plan`,
|
|
3435
|
+
hydrationIncarnationKey: `${deletion.tombstoneId ?? `deleted:${deletion.projectId}:${deletion.branchName}`}:plan`,
|
|
2693
3436
|
sourcePath: deletion.planSourcePath,
|
|
3437
|
+
durabilityRootPath: planRoot,
|
|
2694
3438
|
workspaceRelativePath: deletion.planWorkspaceRelativePath,
|
|
2695
3439
|
sourceMode: "all",
|
|
2696
3440
|
hydrateDeletionMode: "all",
|
|
@@ -2718,10 +3462,34 @@ async function startWorker(options) {
|
|
|
2718
3462
|
const ensureConfiguredProjects = () => {
|
|
2719
3463
|
for (const project of projectConfigById.values()) {
|
|
2720
3464
|
validateProjectId(project.projectId);
|
|
3465
|
+
(0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
|
|
2721
3466
|
for (const branch of project.branches) validateBranchName(branch.branchName);
|
|
3467
|
+
if (project.branches.length === 0) {
|
|
3468
|
+
readyProjectIds.delete(project.projectId);
|
|
3469
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
3470
|
+
continue;
|
|
3471
|
+
}
|
|
3472
|
+
if (project.executionDisabled) {
|
|
3473
|
+
readyProjectIds.delete(project.projectId);
|
|
3474
|
+
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
3475
|
+
for (const branch of project.branches) {
|
|
3476
|
+
pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
|
|
3477
|
+
}
|
|
3478
|
+
continue;
|
|
3479
|
+
}
|
|
2722
3480
|
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
2723
3481
|
const configFingerprint = (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: workspaceGitIdentity });
|
|
2724
|
-
const
|
|
3482
|
+
const durableEnabledTransition = projectWorkspaceState.enabledRepositoryTransitions.find(
|
|
3483
|
+
({ projectId }) => projectId === project.projectId
|
|
3484
|
+
);
|
|
3485
|
+
const transitionReseedRequired = (0, import_repository_transition_policy.enabledRepositoryTransitionRequiresReseed)({
|
|
3486
|
+
project,
|
|
3487
|
+
lastEnabled: {
|
|
3488
|
+
known: Boolean(durableEnabledTransition),
|
|
3489
|
+
transitionId: durableEnabledTransition?.transitionId ?? null
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
const alreadyReady = !transitionReseedRequired && readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === configFingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
|
|
2725
3493
|
if (alreadyReady) {
|
|
2726
3494
|
for (const branch of project.branches) pendingCheckouts.delete(projectBranchKey(project.projectId, branch.branchName));
|
|
2727
3495
|
continue;
|
|
@@ -2730,8 +3498,11 @@ async function startWorker(options) {
|
|
|
2730
3498
|
const connection = projectConnection(project);
|
|
2731
3499
|
const states = (0, import_project_worktrees.ensureProjectWorktrees)({
|
|
2732
3500
|
projectRoot,
|
|
3501
|
+
snapshotRoot: projectWorktreeSnapshotsRoot,
|
|
2733
3502
|
primaryBranchName: primaryProjectBranch(project),
|
|
2734
3503
|
branches: project.branches,
|
|
3504
|
+
defaultBranch: project.defaultBranch,
|
|
3505
|
+
...transitionReseedRequired && project.repositoryTransitionId !== null ? { exactMirrorBranches: /* @__PURE__ */ new Set([primaryProjectBranch(project)]) } : {},
|
|
2735
3506
|
originUrl: connection.originUrl,
|
|
2736
3507
|
originCredentialUsername: connection.originCredentialUsername,
|
|
2737
3508
|
mirrorUrl: connection.mirrorUrl,
|
|
@@ -2744,7 +3515,7 @@ async function startWorker(options) {
|
|
|
2744
3515
|
for (const state of states) {
|
|
2745
3516
|
pendingCheckouts.delete(projectBranchKey(project.projectId, state.branchName));
|
|
2746
3517
|
const key = projectBranchKey(project.projectId, state.branchName);
|
|
2747
|
-
if (!lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
|
|
3518
|
+
if (transitionReseedRequired || !lastObservedProjectHeads.has(key)) lastObservedProjectHeads.set(key, state.mirrorHead);
|
|
2748
3519
|
if ((state.ahead ?? 0) > 0) {
|
|
2749
3520
|
process.stderr.write(
|
|
2750
3521
|
`[r5d-worker] ${project.projectPath}/${state.branchName} is ${state.ahead} commit(s) ahead of origin; switching workers may require publishing or reconciling those commits
|
|
@@ -2752,6 +3523,12 @@ async function startWorker(options) {
|
|
|
2752
3523
|
);
|
|
2753
3524
|
}
|
|
2754
3525
|
}
|
|
3526
|
+
if (transitionReseedRequired) {
|
|
3527
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordEnabledRepositoryTransition({
|
|
3528
|
+
projectId: project.projectId,
|
|
3529
|
+
transitionId: project.repositoryTransitionId
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
2755
3532
|
} catch (error) {
|
|
2756
3533
|
readyProjectIds.delete(project.projectId);
|
|
2757
3534
|
reconciledProjectConfigFingerprints.delete(project.projectId);
|
|
@@ -2771,7 +3548,7 @@ async function startWorker(options) {
|
|
|
2771
3548
|
const collectAheadOfOriginBranches = () => {
|
|
2772
3549
|
const aheadBranches = [];
|
|
2773
3550
|
for (const project of projectConfigById.values()) {
|
|
2774
|
-
if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
3551
|
+
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
2775
3552
|
const connection = projectConnection(project);
|
|
2776
3553
|
const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
|
|
2777
3554
|
if (!tryGit(
|
|
@@ -2793,7 +3570,11 @@ async function startWorker(options) {
|
|
|
2793
3570
|
if (!hasProjectWorktree(branchPath)) continue;
|
|
2794
3571
|
try {
|
|
2795
3572
|
let ahead = 0;
|
|
2796
|
-
const originRef = `refs/remotes/origin/${
|
|
3573
|
+
const originRef = `refs/remotes/origin/${(0, import_project_worktrees.projectOriginBranchName)({
|
|
3574
|
+
branchName: branch.branchName,
|
|
3575
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
3576
|
+
defaultBranch: project.defaultBranch
|
|
3577
|
+
})}`;
|
|
2797
3578
|
if (tryGit(["show-ref", "--verify", "--quiet", originRef], { cwd: branchPath })) {
|
|
2798
3579
|
const counts = runGit(["rev-list", "--left-right", "--count", `${originRef}...HEAD`], { cwd: branchPath }).split(/\s+/).map(Number);
|
|
2799
3580
|
ahead = Number.isSafeInteger(counts[1]) ? counts[1] : 0;
|
|
@@ -2814,18 +3595,30 @@ async function startWorker(options) {
|
|
|
2814
3595
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
2815
3596
|
);
|
|
2816
3597
|
};
|
|
2817
|
-
const
|
|
2818
|
-
const
|
|
3598
|
+
const captureProjectHeadsForWorkspacePublication = () => {
|
|
3599
|
+
const heads = /* @__PURE__ */ new Map();
|
|
2819
3600
|
for (const project of projectConfigById.values()) {
|
|
2820
3601
|
if (!readyProjectIds.has(project.projectId)) continue;
|
|
2821
|
-
const changed = /* @__PURE__ */ new Set();
|
|
2822
3602
|
for (const branch of project.branches) {
|
|
2823
|
-
if (!active.has(projectMountId(project.projectId, branch.branchName))) continue;
|
|
2824
3603
|
const branchPath = configuredProjectBranchPath(projectsRoot, project, branch.branchName);
|
|
2825
3604
|
if (!hasProjectWorktree(branchPath)) continue;
|
|
2826
|
-
|
|
3605
|
+
heads.set(projectMountId(project.projectId, branch.branchName), runGit(["rev-parse", "HEAD"], { cwd: branchPath }));
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
return heads;
|
|
3609
|
+
};
|
|
3610
|
+
const pushChangedProjectHeads = (activeMountIds, publishedHead, publicationHeads, suppressedMountIds = /* @__PURE__ */ new Set()) => {
|
|
3611
|
+
const active = new Set(activeMountIds);
|
|
3612
|
+
for (const project of projectConfigById.values()) {
|
|
3613
|
+
if (!readyProjectIds.has(project.projectId) || !(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) continue;
|
|
3614
|
+
const changed = /* @__PURE__ */ new Map();
|
|
3615
|
+
for (const branch of project.branches) {
|
|
3616
|
+
const mountId = projectMountId(project.projectId, branch.branchName);
|
|
3617
|
+
if (!active.has(mountId) || suppressedMountIds.has(mountId)) continue;
|
|
3618
|
+
const head = publicationHeads.get(mountId);
|
|
3619
|
+
if (!head) throw new Error(`Missing pre-projection head for active project mount ${mountId}`);
|
|
2827
3620
|
if (lastObservedProjectHeads.get(projectBranchKey(project.projectId, branch.branchName)) !== head) {
|
|
2828
|
-
changed.
|
|
3621
|
+
changed.set(branch.branchName, head);
|
|
2829
3622
|
}
|
|
2830
3623
|
}
|
|
2831
3624
|
if (changed.size === 0) continue;
|
|
@@ -2836,10 +3629,12 @@ async function startWorker(options) {
|
|
|
2836
3629
|
mirrorUrl: connection.mirrorUrl,
|
|
2837
3630
|
credentialHelper: connection.credentialHelper,
|
|
2838
3631
|
credentialUsername: connection.mirrorCredentialUsername,
|
|
2839
|
-
onlyBranches: changed
|
|
3632
|
+
onlyBranches: new Set(changed.keys()),
|
|
3633
|
+
publicationHeads: changed
|
|
2840
3634
|
});
|
|
3635
|
+
(0, import_project_worktrees.assertProjectMirrorHeadsPushed)(results);
|
|
2841
3636
|
for (const result of results) {
|
|
2842
|
-
|
|
3637
|
+
lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
|
|
2843
3638
|
}
|
|
2844
3639
|
}
|
|
2845
3640
|
const publishedTombstoneIds = [
|
|
@@ -2857,6 +3652,7 @@ async function startWorker(options) {
|
|
|
2857
3652
|
}
|
|
2858
3653
|
for (const [key, deletion] of pendingMirrorDeletes) {
|
|
2859
3654
|
if (!active.has(deletion.id)) continue;
|
|
3655
|
+
if (!(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(projectConfigById.get(deletion.projectId))) continue;
|
|
2860
3656
|
if (!deletion.projectDeleted) {
|
|
2861
3657
|
(0, import_project_worktrees.deleteProjectMirrorBranch)({
|
|
2862
3658
|
gitDirectory: deletion.gitDirectory,
|
|
@@ -2879,73 +3675,105 @@ async function startWorker(options) {
|
|
|
2879
3675
|
lastObservedProjectHeads.delete(key);
|
|
2880
3676
|
}
|
|
2881
3677
|
};
|
|
2882
|
-
const
|
|
3678
|
+
const observeProjectHeadsBeforeOuterWorkspace = (input) => {
|
|
3679
|
+
const bundles = [];
|
|
2883
3680
|
for (const project of projectConfigById.values()) {
|
|
2884
|
-
if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
3681
|
+
if (project.executionDisabled || !readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
|
|
2885
3682
|
try {
|
|
2886
3683
|
const connection = projectConnection(project);
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3684
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
3685
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
3686
|
+
bundles.push({
|
|
3687
|
+
projectId: project.projectId,
|
|
3688
|
+
projectRoot,
|
|
3689
|
+
primaryBranchName,
|
|
3690
|
+
observations: (0, import_project_worktrees.observeProjectMirrorHeads)({
|
|
3691
|
+
projectRoot,
|
|
3692
|
+
primaryBranchName,
|
|
3693
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
3694
|
+
mirrorUrl: connection.mirrorUrl,
|
|
3695
|
+
credentialHelper: connection.credentialHelper,
|
|
3696
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
3697
|
+
allowNonFastForward: input.allowNonFastForward
|
|
3698
|
+
})
|
|
3699
|
+
});
|
|
3700
|
+
} catch (error) {
|
|
3701
|
+
if (input.failClosed) throw error;
|
|
3702
|
+
process.stderr.write(
|
|
3703
|
+
`[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
3704
|
+
`
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
return bundles;
|
|
3709
|
+
};
|
|
3710
|
+
const applyObservedProjectHeadsAfterInboundWorkspace = (bundles, activeMountIds, requireObservedHeads) => {
|
|
3711
|
+
const active = new Set(activeMountIds);
|
|
3712
|
+
for (const bundle of bundles) {
|
|
3713
|
+
const project = projectConfigById.get(bundle.projectId);
|
|
3714
|
+
if (!project || project.executionDisabled || !readyProjectIds.has(project.projectId)) continue;
|
|
3715
|
+
const observations = bundle.observations.filter(({ branchName }) => active.has(projectMountId(project.projectId, branchName)));
|
|
3716
|
+
if (observations.length === 0) continue;
|
|
3717
|
+
try {
|
|
3718
|
+
const refreshed = (0, import_project_worktrees.applyObservedProjectMirrorHeads)({
|
|
3719
|
+
projectRoot: bundle.projectRoot,
|
|
3720
|
+
primaryBranchName: bundle.primaryBranchName,
|
|
3721
|
+
observations,
|
|
3722
|
+
branchMutationAllowed: (branchName, checkoutPath) => !projectBranchMountBusy(project.projectId, branchName, checkoutPath)
|
|
2894
3723
|
});
|
|
3724
|
+
if (requireObservedHeads) {
|
|
3725
|
+
for (const observation of observations) {
|
|
3726
|
+
if (!observation.shouldMove || !observation.mirrorHead) continue;
|
|
3727
|
+
const checkoutPath = configuredProjectBranchPath(projectsRoot, project, observation.branchName);
|
|
3728
|
+
const currentHead = runGit(["rev-parse", "HEAD"], { cwd: checkoutPath });
|
|
3729
|
+
if (currentHead !== observation.mirrorHead) {
|
|
3730
|
+
throw new Error(
|
|
3731
|
+
`Project ${project.projectId}/${observation.branchName} did not apply observed hidden mirror head ${observation.mirrorHead}`
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
2895
3736
|
for (const state of refreshed) {
|
|
2896
3737
|
lastObservedProjectHeads.set(
|
|
2897
3738
|
projectBranchKey(project.projectId, state.branchName),
|
|
2898
|
-
state.
|
|
3739
|
+
state.moved && state.mirrorHead ? state.mirrorHead : state.previousHead
|
|
2899
3740
|
);
|
|
2900
3741
|
}
|
|
2901
3742
|
} catch (error) {
|
|
3743
|
+
if (requireObservedHeads) throw error;
|
|
2902
3744
|
process.stderr.write(
|
|
2903
|
-
`[r5d-worker] failed to
|
|
3745
|
+
`[r5d-worker] failed to apply observed project heads after workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
2904
3746
|
`
|
|
2905
3747
|
);
|
|
2906
3748
|
}
|
|
2907
3749
|
}
|
|
2908
3750
|
};
|
|
2909
|
-
const
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3751
|
+
const mapWorkspaceGitResult = (attemptId, trigger, result) => {
|
|
3752
|
+
const activeProjectBranchPublications = (0, import_workspace_publication_evidence.activeProjectBranchPublicationEvidence)([...projectConfigById.values()], result.activeMountIds);
|
|
3753
|
+
return {
|
|
3754
|
+
type: "workspace_sync",
|
|
3755
|
+
attemptId,
|
|
3756
|
+
workerLabel: label,
|
|
3757
|
+
trigger,
|
|
3758
|
+
outcome: result.outcome === "pushed" ? "published" : result.outcome,
|
|
3759
|
+
startingHead: result.startingHead,
|
|
3760
|
+
...result.localHead ? { localHead: result.localHead } : {},
|
|
3761
|
+
...result.publishedHead ? { publishedHead: result.publishedHead } : {},
|
|
3762
|
+
rebaseCount: result.rebaseCount,
|
|
3763
|
+
diffSizeBytes: result.diffSizeBytes,
|
|
3764
|
+
gitStatus: workspaceGitStatus(),
|
|
3765
|
+
affectedProjects: affectedProjects(result.affectedPaths),
|
|
3766
|
+
affectedPaths: result.affectedPaths,
|
|
3767
|
+
activeMountIds: result.activeMountIds,
|
|
3768
|
+
skippedMountIds: result.skippedMountIds,
|
|
3769
|
+
activeProjectBranchPublications,
|
|
3770
|
+
discardedPaths: [],
|
|
3771
|
+
localChangesDiscarded: false,
|
|
3772
|
+
...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
|
|
3773
|
+
...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
|
|
3774
|
+
...result.error ? { error: result.error } : {}
|
|
3775
|
+
};
|
|
2928
3776
|
};
|
|
2929
|
-
const mapWorkspaceGitResult = (attemptId, trigger, result) => ({
|
|
2930
|
-
type: "workspace_sync",
|
|
2931
|
-
attemptId,
|
|
2932
|
-
workerLabel: label,
|
|
2933
|
-
trigger,
|
|
2934
|
-
outcome: result.outcome === "pushed" ? "published" : result.outcome,
|
|
2935
|
-
startingHead: result.startingHead,
|
|
2936
|
-
...result.localHead ? { localHead: result.localHead } : {},
|
|
2937
|
-
...result.publishedHead ? { publishedHead: result.publishedHead } : {},
|
|
2938
|
-
rebaseCount: result.rebaseCount,
|
|
2939
|
-
diffSizeBytes: result.diffSizeBytes,
|
|
2940
|
-
gitStatus: workspaceGitStatus(),
|
|
2941
|
-
affectedProjects: affectedProjects(result.affectedPaths),
|
|
2942
|
-
affectedPaths: result.affectedPaths,
|
|
2943
|
-
discardedPaths: [],
|
|
2944
|
-
localChangesDiscarded: false,
|
|
2945
|
-
...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
|
|
2946
|
-
...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
|
|
2947
|
-
...result.error ? { error: result.error } : {}
|
|
2948
|
-
});
|
|
2949
3777
|
const failedWorkspaceSyncResult = (attemptId, trigger, error) => ({
|
|
2950
3778
|
type: "workspace_sync",
|
|
2951
3779
|
attemptId,
|
|
@@ -2969,6 +3797,16 @@ async function startWorker(options) {
|
|
|
2969
3797
|
}
|
|
2970
3798
|
const mounts = buildWorkspaceMounts();
|
|
2971
3799
|
if (input.resetToCanonical) {
|
|
3800
|
+
const resetHasBusyLiveMount = mounts.some(
|
|
3801
|
+
(mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
|
|
3802
|
+
);
|
|
3803
|
+
if (resetHasBusyLiveMount) {
|
|
3804
|
+
throw new Error("Workspace reset cannot observe project mirror heads while a live mount is busy; remediation remains active");
|
|
3805
|
+
}
|
|
3806
|
+
const observedProjectHeads2 = observeProjectHeadsBeforeOuterWorkspace({
|
|
3807
|
+
allowNonFastForward: true,
|
|
3808
|
+
failClosed: true
|
|
3809
|
+
});
|
|
2972
3810
|
const reset = (0, import_workspace_git_sync.resetWorkspaceGit)({
|
|
2973
3811
|
workspacePath: workspaceShadowRoot,
|
|
2974
3812
|
remoteUrl: workspaceRemoteUrl,
|
|
@@ -2977,7 +3815,7 @@ async function startWorker(options) {
|
|
|
2977
3815
|
gitIdentity: workspaceGitIdentity,
|
|
2978
3816
|
mounts
|
|
2979
3817
|
});
|
|
2980
|
-
|
|
3818
|
+
applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
|
|
2981
3819
|
return {
|
|
2982
3820
|
type: "workspace_sync",
|
|
2983
3821
|
attemptId: input.attemptId,
|
|
@@ -2992,11 +3830,23 @@ async function startWorker(options) {
|
|
|
2992
3830
|
gitStatus: workspaceGitStatus(),
|
|
2993
3831
|
affectedProjects: affectedProjects(reset.discardedPaths),
|
|
2994
3832
|
affectedPaths: reset.discardedPaths,
|
|
3833
|
+
activeMountIds: reset.activeMountIds,
|
|
3834
|
+
skippedMountIds: reset.skippedMountIds,
|
|
2995
3835
|
discardedPaths: reset.discardedPaths,
|
|
2996
3836
|
localChangesDiscarded: reset.discardedPaths.length > 0 || reset.startingHead !== reset.remoteHead
|
|
2997
3837
|
};
|
|
2998
3838
|
}
|
|
2999
3839
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
3840
|
+
const ordinaryCycleHasBusyLiveMount = mounts.some(
|
|
3841
|
+
(mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
|
|
3842
|
+
);
|
|
3843
|
+
const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
3844
|
+
const inboundMoveMountIds = new Set(
|
|
3845
|
+
observedProjectHeads.flatMap(
|
|
3846
|
+
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
3847
|
+
)
|
|
3848
|
+
);
|
|
3849
|
+
const publicationHeads = outerRemediation ? /* @__PURE__ */ new Map() : captureProjectHeadsForWorkspacePublication();
|
|
3000
3850
|
const result = await (0, import_workspace_git_sync.synchronizeWorkspaceGit)({
|
|
3001
3851
|
attemptId: input.attemptId,
|
|
3002
3852
|
workerLabel: label,
|
|
@@ -3009,13 +3859,12 @@ async function startWorker(options) {
|
|
|
3009
3859
|
commitDetail: input.confirmationReason ?? input.trigger.detail,
|
|
3010
3860
|
allowLargeDiff: input.confirmedLargeDiff,
|
|
3011
3861
|
skipMountMirror: outerRemediation,
|
|
3012
|
-
skipMountHydration: outerRemediation,
|
|
3013
3862
|
afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
|
|
3014
|
-
pushChangedProjectHeads(activeMountIds, publishedHead);
|
|
3863
|
+
pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
|
|
3015
3864
|
}
|
|
3016
3865
|
});
|
|
3017
3866
|
if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
|
|
3018
|
-
|
|
3867
|
+
applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
|
|
3019
3868
|
}
|
|
3020
3869
|
return mapWorkspaceGitResult(input.attemptId, input.trigger, result);
|
|
3021
3870
|
};
|
|
@@ -3036,6 +3885,12 @@ async function startWorker(options) {
|
|
|
3036
3885
|
const runWorkspaceSync = async (input) => {
|
|
3037
3886
|
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
3038
3887
|
const requestedAt = Date.now();
|
|
3888
|
+
if (!input.requestId && input.sendResult !== false) {
|
|
3889
|
+
if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
|
|
3890
|
+
throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
|
|
3891
|
+
}
|
|
3892
|
+
ws.send(JSON.stringify({ type: "workspace_sync_started", attemptId }));
|
|
3893
|
+
}
|
|
3039
3894
|
let queueEnteredAt = requestedAt;
|
|
3040
3895
|
let syncStartedAt = requestedAt;
|
|
3041
3896
|
workspaceSyncRequestsInFlight += 1;
|
|
@@ -3053,6 +3908,28 @@ async function startWorker(options) {
|
|
|
3053
3908
|
resetToCanonical: input.resetToCanonical
|
|
3054
3909
|
});
|
|
3055
3910
|
} catch (error) {
|
|
3911
|
+
let hydrationCurrent = input.resetToCanonical !== true;
|
|
3912
|
+
if (hydrationCurrent) {
|
|
3913
|
+
try {
|
|
3914
|
+
hydrationCurrent = (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts());
|
|
3915
|
+
} catch {
|
|
3916
|
+
hydrationCurrent = false;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
if (!hydrationCurrent) {
|
|
3920
|
+
process.stderr.write(
|
|
3921
|
+
`[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
|
|
3922
|
+
`
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
fenceUnsafeWorkspaceSyncFailure({
|
|
3926
|
+
hydrationCurrent,
|
|
3927
|
+
invalidateExecution: () => {
|
|
3928
|
+
workspaceConfigured = false;
|
|
3929
|
+
advanceWorkerAdmissionGeneration();
|
|
3930
|
+
},
|
|
3931
|
+
exitCode: 1
|
|
3932
|
+
});
|
|
3056
3933
|
return failedWorkspaceSyncResult(attemptId, input.trigger, error);
|
|
3057
3934
|
}
|
|
3058
3935
|
});
|
|
@@ -3074,7 +3951,13 @@ async function startWorker(options) {
|
|
|
3074
3951
|
};
|
|
3075
3952
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
3076
3953
|
pendingAutomaticTrigger = trigger;
|
|
3077
|
-
|
|
3954
|
+
const pendingBranchDeferral = () => (0, import_workspace_automatic_sync_policy.pendingCreatedBranchAutomaticSyncDeferral)(
|
|
3955
|
+
projectWorkspaceState.locallyPendingCreatedBranches,
|
|
3956
|
+
activeWorkspaceMutationTargets(),
|
|
3957
|
+
pendingCreatedBranchPublicationNotBefore
|
|
3958
|
+
);
|
|
3959
|
+
const initialDeferral = pendingBranchDeferral();
|
|
3960
|
+
if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
|
|
3078
3961
|
return;
|
|
3079
3962
|
}
|
|
3080
3963
|
if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
|
|
@@ -3082,13 +3965,25 @@ async function startWorker(options) {
|
|
|
3082
3965
|
() => {
|
|
3083
3966
|
workspaceAutomaticTimer = void 0;
|
|
3084
3967
|
const scheduledTrigger = pendingAutomaticTrigger;
|
|
3085
|
-
pendingAutomaticTrigger = void 0;
|
|
3086
3968
|
if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
|
|
3969
|
+
const currentDeferral = pendingBranchDeferral();
|
|
3970
|
+
if (currentDeferral?.kind === "active_target") return;
|
|
3971
|
+
if (currentDeferral?.kind === "creation_grace") {
|
|
3972
|
+
scheduleAutomaticWorkspaceSync(scheduledTrigger, currentDeferral.retryAfterMs);
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
pendingAutomaticTrigger = void 0;
|
|
3087
3976
|
automaticSyncInFlight = true;
|
|
3088
3977
|
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
3089
3978
|
if (result.outcome === "failed") {
|
|
3090
3979
|
pendingAutomaticTrigger = scheduledTrigger;
|
|
3091
3980
|
}
|
|
3981
|
+
}).catch((error) => {
|
|
3982
|
+
pendingAutomaticTrigger = scheduledTrigger;
|
|
3983
|
+
process.stderr.write(
|
|
3984
|
+
`[r5d-worker] failed to start automatic workspace sync: ${error instanceof Error ? error.message : String(error)}
|
|
3985
|
+
`
|
|
3986
|
+
);
|
|
3092
3987
|
}).finally(() => {
|
|
3093
3988
|
automaticSyncInFlight = false;
|
|
3094
3989
|
heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
|
|
@@ -3097,7 +3992,7 @@ async function startWorker(options) {
|
|
|
3097
3992
|
}
|
|
3098
3993
|
});
|
|
3099
3994
|
},
|
|
3100
|
-
Math.max(0, delayMs)
|
|
3995
|
+
Math.max(0, delayMs, initialDeferral?.kind === "creation_grace" ? initialDeferral.retryAfterMs : 0)
|
|
3101
3996
|
);
|
|
3102
3997
|
workspaceAutomaticTimer.unref();
|
|
3103
3998
|
};
|
|
@@ -3105,32 +4000,190 @@ async function startWorker(options) {
|
|
|
3105
4000
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
3106
4001
|
};
|
|
3107
4002
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
3108
|
-
const resolveMessageTarget = (target) =>
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
4003
|
+
const resolveMessageTarget = (target) => {
|
|
4004
|
+
if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
|
|
4005
|
+
if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
|
|
4006
|
+
throw new Error(`Project ${target.projectId} is not ready on this worker`);
|
|
4007
|
+
}
|
|
4008
|
+
if (target.type === "workspace" && target.rootProfile === "visible_projects" && [...projectConfigById.keys()].some((projectId) => !readyProjectIds.has(projectId))) {
|
|
4009
|
+
throw new Error("Visible project execution is unavailable while project configuration is incomplete");
|
|
4010
|
+
}
|
|
4011
|
+
return resolveWorkerSessionTarget({
|
|
4012
|
+
target,
|
|
4013
|
+
projectsRoot,
|
|
4014
|
+
workspaceShadowRoot,
|
|
4015
|
+
projectConfigById
|
|
4016
|
+
});
|
|
4017
|
+
};
|
|
3114
4018
|
const configureWorkerWorkspace = async (message) => {
|
|
4019
|
+
const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
|
|
4020
|
+
...message,
|
|
4021
|
+
workerBaseUrl: baseUrl,
|
|
4022
|
+
workerAuthHeader: bearerAuthHeader
|
|
4023
|
+
});
|
|
4024
|
+
const credentialTransitionPhase = credentialGenerationTransitionPhase({
|
|
4025
|
+
committedFingerprint: configuredCredentialGenerationFingerprint,
|
|
4026
|
+
pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
|
|
4027
|
+
incomingFingerprint: incomingCredentialGenerationFingerprint
|
|
4028
|
+
});
|
|
4029
|
+
const credentialReapStatus = credentialReapContractStatus();
|
|
4030
|
+
const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
|
|
4031
|
+
const credentialRestartIsContained = credentialGenerationRestartIsContained({
|
|
4032
|
+
systemdContract: verifiedCredentialReapContractNow,
|
|
4033
|
+
pendingBoundary: pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null,
|
|
4034
|
+
currentSystemdInvocationId: systemdInvocationIdentifierAtProcessStart,
|
|
4035
|
+
currentBootSessionId: bootSessionIdentifierAtProcessStart,
|
|
4036
|
+
currentMachineId: machineIdentifierAtProcessStart
|
|
4037
|
+
});
|
|
4038
|
+
let trackedChildTerminationProven = true;
|
|
4039
|
+
if (credentialTransitionPhase !== "current") {
|
|
4040
|
+
workspaceConfigured = false;
|
|
4041
|
+
githubCredential = null;
|
|
4042
|
+
advanceWorkerAdmissionGeneration();
|
|
4043
|
+
try {
|
|
4044
|
+
await terminateActiveCredentialBearingChildren();
|
|
4045
|
+
} catch (error) {
|
|
4046
|
+
trackedChildTerminationProven = false;
|
|
4047
|
+
process.stderr.write(
|
|
4048
|
+
`[r5d-worker] tracked credential-bearing child termination was incomplete at the credential transition boundary: ${error instanceof Error ? error.message : String(error)}
|
|
4049
|
+
`
|
|
4050
|
+
);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const initialBootstrapAuthorized = initialCredentialBootstrapIsSafe({
|
|
4054
|
+
transitionPhase: credentialTransitionPhase,
|
|
4055
|
+
committedFingerprint: configuredCredentialGenerationFingerprint,
|
|
4056
|
+
pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
|
|
4057
|
+
bootstrapFingerprintAtProcessStart: bootstrapCredentialGenerationFingerprintAtProcessStart,
|
|
4058
|
+
incomingFingerprint: incomingCredentialGenerationFingerprint,
|
|
4059
|
+
credentialArtifactsProvenAbsentAtProcessStart: initialCredentialBootstrapProvenEmpty,
|
|
4060
|
+
trackedChildTerminationProven,
|
|
4061
|
+
activeProcessCount: activeProcesses.size,
|
|
4062
|
+
credentialProcessGroupCount: credentialBearingProcessGroups.size,
|
|
4063
|
+
activePtyCount: activePtys.size
|
|
4064
|
+
});
|
|
4065
|
+
const staleIntentCleanupAuthorized = credentialTransitionPhase === "current" && (pendingCredentialGenerationIntentAtProcessStart !== null || bootstrapCredentialGenerationFingerprintAtProcessStart !== null);
|
|
4066
|
+
const credentialPublicationPreauthorized = credentialTransitionPhase === "publish_from_clean_restart" && credentialRestartIsContained || initialBootstrapAuthorized || staleIntentCleanupAuthorized;
|
|
4067
|
+
if (initialBootstrapAuthorized && bootstrapCredentialGenerationFingerprintAtProcessStart === null) {
|
|
4068
|
+
try {
|
|
4069
|
+
const bootstrap = (0, import_registry_auth.preparePrivateAuthFileGeneration)([
|
|
4070
|
+
{ filePath: bootstrapCredentialGenerationPath(), content: `${incomingCredentialGenerationFingerprint}
|
|
4071
|
+
` }
|
|
4072
|
+
]);
|
|
4073
|
+
bootstrap.commit();
|
|
4074
|
+
bootstrapCredentialGenerationFingerprintAtProcessStart = incomingCredentialGenerationFingerprint;
|
|
4075
|
+
} catch (error) {
|
|
4076
|
+
const wrapped = error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
|
|
4077
|
+
return {
|
|
4078
|
+
result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, wrapped),
|
|
4079
|
+
pending: [],
|
|
4080
|
+
aheadOfOriginBranches: []
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
if (credentialTransitionPhase === "stage_pending_restart" && !initialBootstrapAuthorized) {
|
|
4085
|
+
if (credentialReapStatus === "systemd_unverified") {
|
|
4086
|
+
process.stderr.write(
|
|
4087
|
+
"[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"
|
|
4088
|
+
);
|
|
4089
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4090
|
+
}
|
|
4091
|
+
const boundary = pendingCredentialGenerationBoundary({
|
|
4092
|
+
systemdContract: verifiedCredentialReapContractNow,
|
|
4093
|
+
systemdInvocationId: systemdInvocationIdentifierAtProcessStart,
|
|
4094
|
+
bootSessionId: bootSessionIdentifierAtProcessStart,
|
|
4095
|
+
machineId: machineIdentifierAtProcessStart
|
|
4096
|
+
});
|
|
4097
|
+
if (!credentialGenerationBoundaryIsComplete(boundary)) {
|
|
4098
|
+
process.stderr.write(
|
|
4099
|
+
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"
|
|
4100
|
+
);
|
|
4101
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4102
|
+
}
|
|
4103
|
+
try {
|
|
4104
|
+
const pending = (0, import_registry_auth.preparePrivateAuthFileGeneration)([
|
|
4105
|
+
{
|
|
4106
|
+
filePath: pendingCredentialGenerationPath(),
|
|
4107
|
+
content: pendingCredentialGenerationIntentContent({
|
|
4108
|
+
fingerprint: incomingCredentialGenerationFingerprint,
|
|
4109
|
+
boundary
|
|
4110
|
+
})
|
|
4111
|
+
}
|
|
4112
|
+
]);
|
|
4113
|
+
pending.commit();
|
|
4114
|
+
} catch (error) {
|
|
4115
|
+
process.stderr.write(
|
|
4116
|
+
`[r5d-worker] could not stage pending credential generation before fatal restart: ${error instanceof Error ? error.message : String(error)}
|
|
4117
|
+
`
|
|
4118
|
+
);
|
|
4119
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4120
|
+
}
|
|
4121
|
+
process.stderr.write(
|
|
4122
|
+
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"
|
|
4123
|
+
);
|
|
4124
|
+
requireCredentialGenerationRestart(true, void 0, import_supervisor.WORKER_CREDENTIAL_RESTART_EXIT_CODE);
|
|
4125
|
+
}
|
|
4126
|
+
if (credentialTransitionPhase !== "current" && !trackedChildTerminationProven) {
|
|
4127
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4128
|
+
}
|
|
4129
|
+
if (credentialTransitionPhase === "publish_from_clean_restart" && !credentialRestartIsContained) {
|
|
4130
|
+
const pendingBoundary = pendingCredentialGenerationIntentAtProcessStart?.boundary ?? null;
|
|
4131
|
+
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";
|
|
4132
|
+
process.stderr.write(
|
|
4133
|
+
`[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."}
|
|
4134
|
+
`
|
|
4135
|
+
);
|
|
4136
|
+
requireCredentialGenerationRestart(true, void 0, import_supervisor.WORKER_CREDENTIAL_RESTART_EXIT_CODE);
|
|
4137
|
+
}
|
|
4138
|
+
if (credentialPublicationPreauthorized) workspaceConfigured = false;
|
|
3115
4139
|
workspaceSyncRequestsInFlight += 1;
|
|
3116
4140
|
try {
|
|
3117
4141
|
return await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
4142
|
+
for (const project of message.projects) (0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
|
|
4143
|
+
const busyConfigurationChanges = (0, import_workspace_project_config_policy.busyProjectConfigurationChangeIds)({
|
|
4144
|
+
currentProjects: [...projectConfigById.values()],
|
|
4145
|
+
incomingProjects: message.projects,
|
|
4146
|
+
activeTargets: activeWorkspaceMutationTargets(),
|
|
4147
|
+
currentFingerprint: (project) => (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: workspaceGitIdentity }),
|
|
4148
|
+
incomingFingerprint: (project) => (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: message.gitIdentity }),
|
|
4149
|
+
currentProjectReady: (project) => {
|
|
4150
|
+
const fingerprint = (0, import_project_worktrees.projectWorktreeConfigurationFingerprint)({ ...project, gitIdentity: workspaceGitIdentity });
|
|
4151
|
+
return readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === fingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
|
|
4152
|
+
}
|
|
4153
|
+
});
|
|
4154
|
+
if (busyConfigurationChanges.length > 0) {
|
|
4155
|
+
throw new ProjectWorkspaceConfigurationDeferredError(
|
|
4156
|
+
`Project workspace configuration deferred while visible checkout activity is active: ${busyConfigurationChanges.join(", ")}`
|
|
4157
|
+
);
|
|
4158
|
+
}
|
|
3118
4159
|
workspaceConfigured = false;
|
|
4160
|
+
const preserveOnlyBranches = message.projects.flatMap(
|
|
4161
|
+
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
4162
|
+
);
|
|
3119
4163
|
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
3120
|
-
desiredProjects: message.projects
|
|
4164
|
+
desiredProjects: message.projects,
|
|
4165
|
+
preserveOnlyBranches
|
|
3121
4166
|
});
|
|
3122
|
-
const
|
|
4167
|
+
const stillPendingCreatedBranches = new Map(
|
|
4168
|
+
projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
|
|
4169
|
+
(0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(branch.projectId, branch.branchName),
|
|
4170
|
+
branch
|
|
4171
|
+
])
|
|
4172
|
+
);
|
|
4173
|
+
for (const key of pendingCreatedBranchPublicationNotBefore.keys()) {
|
|
4174
|
+
if (!stillPendingCreatedBranches.has(key)) pendingCreatedBranchPublicationNotBefore.delete(key);
|
|
4175
|
+
}
|
|
4176
|
+
const desiredCredentialStoreFiles = /* @__PURE__ */ new Map();
|
|
3123
4177
|
const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
|
|
3124
|
-
requiredCredentialStorePaths.add(workspaceStorePath);
|
|
3125
4178
|
const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
|
|
3126
4179
|
[{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
|
|
3127
|
-
workspaceStorePath
|
|
4180
|
+
workspaceStorePath,
|
|
4181
|
+
desiredCredentialStoreFiles
|
|
3128
4182
|
);
|
|
3129
4183
|
if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
|
|
3130
4184
|
const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
|
|
3131
4185
|
for (const project of message.projects) {
|
|
3132
|
-
|
|
3133
|
-
projectConnection(project);
|
|
4186
|
+
projectConnection(project, desiredCredentialStoreFiles);
|
|
3134
4187
|
}
|
|
3135
4188
|
const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
|
|
3136
4189
|
...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
|
|
@@ -3139,30 +4192,81 @@ async function startWorker(options) {
|
|
|
3139
4192
|
]);
|
|
3140
4193
|
for (const projectId of pendingCredentialProjectIds) {
|
|
3141
4194
|
const desiredProject = desiredProjectConfigById.get(projectId);
|
|
3142
|
-
if (desiredProject) projectConnection(desiredProject);
|
|
3143
|
-
else projectMirrorConnection(projectId);
|
|
3144
|
-
requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
|
|
4195
|
+
if (desiredProject) projectConnection(desiredProject, desiredCredentialStoreFiles);
|
|
4196
|
+
else projectMirrorConnection(projectId, desiredCredentialStoreFiles);
|
|
3145
4197
|
}
|
|
3146
4198
|
for (const deletion of pendingMirrorDeletes.values()) {
|
|
3147
4199
|
const desiredProject = desiredProjectConfigById.get(deletion.projectId);
|
|
3148
|
-
const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
|
|
4200
|
+
const connection = desiredProject ? projectConnection(desiredProject, desiredCredentialStoreFiles) : projectMirrorConnection(deletion.projectId, desiredCredentialStoreFiles);
|
|
3149
4201
|
deletion.mirrorUrl = connection.mirrorUrl;
|
|
3150
4202
|
deletion.credentialHelper = connection.credentialHelper;
|
|
3151
4203
|
deletion.credentialUsername = connection.mirrorCredentialUsername;
|
|
3152
4204
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
4205
|
+
let preparedAuthGeneration;
|
|
4206
|
+
try {
|
|
4207
|
+
preparedAuthGeneration = (0, import_registry_auth.preparePrivateAuthFileGeneration)([
|
|
4208
|
+
...planCredentialStoreGeneration(desiredCredentialStoreFiles),
|
|
4209
|
+
...(0, import_registry_auth.planGitHubRegistryAuthFiles)([dockerConfigPath(), containersAuthPath()], message.githubCredential),
|
|
4210
|
+
{ filePath: legacySharedCredentialStorePath(), content: null, allowSymlinkRemoval: true },
|
|
4211
|
+
{ filePath: credentialGenerationReceiptPath(), content: `${incomingCredentialGenerationFingerprint}
|
|
4212
|
+
` },
|
|
4213
|
+
{ filePath: pendingCredentialGenerationPath(), content: null },
|
|
4214
|
+
{ filePath: bootstrapCredentialGenerationPath(), content: null }
|
|
4215
|
+
]);
|
|
4216
|
+
} catch (error) {
|
|
4217
|
+
throw error instanceof import_registry_auth.RegistryAuthConfigurationError ? error : new import_registry_auth.RegistryAuthConfigurationError(error);
|
|
4218
|
+
}
|
|
4219
|
+
await commitCredentialGeneration(
|
|
4220
|
+
preparedAuthGeneration,
|
|
4221
|
+
message.githubCredential,
|
|
4222
|
+
terminateActiveCredentialBearingChildren,
|
|
4223
|
+
void 0,
|
|
4224
|
+
credentialPublicationPreauthorized
|
|
4225
|
+
);
|
|
4226
|
+
configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
|
|
4227
|
+
pendingCredentialGenerationIntentAtProcessStart = null;
|
|
4228
|
+
bootstrapCredentialGenerationFingerprintAtProcessStart = null;
|
|
3155
4229
|
visibleGitIdentity = message.gitIdentity;
|
|
3156
4230
|
workspaceGitIdentity = message.gitIdentity;
|
|
3157
4231
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
3158
4232
|
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
3159
4233
|
workspaceCredentialUsername = workerCredentialUsername;
|
|
3160
|
-
(0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes,
|
|
4234
|
+
(0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, projectWorkspaceState.desiredProjects);
|
|
3161
4235
|
const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
|
|
3162
4236
|
...project,
|
|
3163
4237
|
branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
|
|
3164
4238
|
}));
|
|
3165
4239
|
const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
|
|
4240
|
+
(0, import_workspace_git_sync.ensureWorkspaceGitClone)({
|
|
4241
|
+
workspacePath: workspaceShadowRoot,
|
|
4242
|
+
remoteUrl: message.workspaceRemoteUrl,
|
|
4243
|
+
credentialHelper: nextWorkspaceCredentialHelper,
|
|
4244
|
+
credentialUsername: workerCredentialUsername,
|
|
4245
|
+
gitIdentity: message.gitIdentity
|
|
4246
|
+
});
|
|
4247
|
+
const recoverySourceOverrides = /* @__PURE__ */ new Map();
|
|
4248
|
+
for (const deletion of projectWorkspaceState.pendingTreeDeletions) {
|
|
4249
|
+
if (deletion.kind !== "project" || deletion.projectDeleted) continue;
|
|
4250
|
+
for (const branchName of deletion.branchNames) {
|
|
4251
|
+
const oldBranchPath = import_node_path.default.join(deletion.managedPath, ...branchName.split("/"));
|
|
4252
|
+
if (import_node_fs.default.existsSync(oldBranchPath)) recoverySourceOverrides.set(projectMountId(deletion.projectId, branchName), oldBranchPath);
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
const recoveryMounts = buildWorkspaceMounts(effectiveProjects).map((mount) => ({
|
|
4256
|
+
...mount,
|
|
4257
|
+
sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
|
|
4258
|
+
busy: mount.busyForRecovery ?? mount.busy
|
|
4259
|
+
}));
|
|
4260
|
+
(0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts);
|
|
4261
|
+
for (const project of message.projects) {
|
|
4262
|
+
for (const { branchName } of project.preserveOnlyBranches) {
|
|
4263
|
+
if (stillPendingCreatedBranches.has((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(project.projectId, branchName))) {
|
|
4264
|
+
continue;
|
|
4265
|
+
}
|
|
4266
|
+
deactivatePreserveOnlyProjectBranch(project, branchName);
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
const preserveOnlyKeys = new Set(preserveOnlyBranches.map(({ projectId, branchName }) => projectBranchKey(projectId, branchName)));
|
|
3166
4270
|
for (const existingProject of projectConfigById.values()) {
|
|
3167
4271
|
const nextProject = nextProjectConfigById.get(existingProject.projectId);
|
|
3168
4272
|
if (!nextProject) {
|
|
@@ -3172,6 +4276,7 @@ async function startWorker(options) {
|
|
|
3172
4276
|
const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
|
|
3173
4277
|
for (const existingBranch of existingProject.branches) {
|
|
3174
4278
|
if (!nextBranches.has(existingBranch.branchName)) {
|
|
4279
|
+
if (preserveOnlyKeys.has(projectBranchKey(existingProject.projectId, existingBranch.branchName))) continue;
|
|
3175
4280
|
stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
|
|
3176
4281
|
}
|
|
3177
4282
|
}
|
|
@@ -3187,40 +4292,30 @@ async function startWorker(options) {
|
|
|
3187
4292
|
for (const projectId of [...readyProjectIds]) {
|
|
3188
4293
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
3189
4294
|
}
|
|
3190
|
-
(0, import_workspace_git_sync.ensureWorkspaceGitClone)({
|
|
3191
|
-
workspacePath: workspaceShadowRoot,
|
|
3192
|
-
remoteUrl: message.workspaceRemoteUrl,
|
|
3193
|
-
credentialHelper: nextWorkspaceCredentialHelper,
|
|
3194
|
-
credentialUsername: workerCredentialUsername,
|
|
3195
|
-
gitIdentity: message.gitIdentity
|
|
3196
|
-
});
|
|
3197
|
-
pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
|
|
3198
4295
|
stageDurableWorkspaceDeletions();
|
|
3199
4296
|
const mountsBeforeGit = buildWorkspaceMounts();
|
|
3200
4297
|
(0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
|
|
3201
4298
|
workspaceShadowRoot,
|
|
3202
|
-
mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath))
|
|
4299
|
+
mountsBeforeGit.filter((mount) => !mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath) && mount.busy?.() !== true),
|
|
4300
|
+
// These mounts are intentionally hydrated before their project Git
|
|
4301
|
+
// layouts can be marked ready. No process can resolve an unready
|
|
4302
|
+
// target, and preserving the fetched outer tree here prevents clean
|
|
4303
|
+
// checkout initialization from erasing synchronized dirt.
|
|
4304
|
+
{ ignoreBusy: true }
|
|
3203
4305
|
);
|
|
3204
4306
|
ensureConfiguredProjects();
|
|
3205
|
-
|
|
4307
|
+
const result = await performWorkspaceSync({
|
|
3206
4308
|
attemptId: crypto.randomUUID(),
|
|
3207
4309
|
trigger: { type: "connect" }
|
|
3208
4310
|
});
|
|
3209
|
-
if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
3210
|
-
const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
|
|
3211
|
-
if (staleAfterInbound.length > 0) {
|
|
3212
|
-
pruneStaleOuterWorkspaceEntries(staleAfterInbound);
|
|
3213
|
-
stageDurableWorkspaceDeletions();
|
|
3214
|
-
result = await performWorkspaceSync({
|
|
3215
|
-
attemptId: crypto.randomUUID(),
|
|
3216
|
-
trigger: { type: "connect" }
|
|
3217
|
-
});
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
4311
|
const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
|
|
3221
4312
|
if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
4313
|
+
const activeMountIds = new Set(result.activeMountIds ?? []);
|
|
3222
4314
|
const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
|
|
3223
|
-
(deletion) => deletion.kind === "project" && !deletion.projectDeleted
|
|
4315
|
+
(deletion) => deletion.kind === "project" && !deletion.projectDeleted && (0, import_workspace_path_move.checkoutPathMovePublicationComplete)(
|
|
4316
|
+
deletion.branchNames.map((branchName) => projectMountId(deletion.projectId, branchName)),
|
|
4317
|
+
activeMountIds
|
|
4318
|
+
) ? [deletion.tombstoneId] : []
|
|
3224
4319
|
);
|
|
3225
4320
|
if (layoutCleanupIds.length > 0) {
|
|
3226
4321
|
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
@@ -3236,18 +4331,37 @@ async function startWorker(options) {
|
|
|
3236
4331
|
return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
|
|
3237
4332
|
});
|
|
3238
4333
|
} catch (error) {
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
4334
|
+
if (error instanceof import_registry_auth.RegistryAuthConfigurationError) {
|
|
4335
|
+
workspaceConfigured = false;
|
|
4336
|
+
githubCredential = null;
|
|
4337
|
+
configuredCredentialGenerationFingerprint = null;
|
|
4338
|
+
let childTerminationProven = true;
|
|
4339
|
+
try {
|
|
4340
|
+
await terminateActiveCredentialBearingChildren();
|
|
4341
|
+
} catch (childError) {
|
|
4342
|
+
childTerminationProven = false;
|
|
4343
|
+
process.stderr.write(
|
|
4344
|
+
`[r5d-worker] failed to fully terminate credential-bearing children: ${childError instanceof Error ? childError.message : String(childError)}
|
|
4345
|
+
`
|
|
4346
|
+
);
|
|
3246
4347
|
}
|
|
4348
|
+
process.stderr.write(
|
|
4349
|
+
`[r5d-worker] fatal credential-generation transition failure; exiting for whole-runtime cleanup: ${error instanceof Error ? error.message : String(error)}
|
|
4350
|
+
`
|
|
4351
|
+
);
|
|
4352
|
+
requireCredentialGenerationRestart(true, void 0, 1);
|
|
4353
|
+
}
|
|
4354
|
+
workspaceConfigured = false;
|
|
4355
|
+
const deferred = error instanceof ProjectWorkspaceConfigurationDeferredError || error instanceof import_project_workspace_state.ProjectWorkspacePendingBranchPathChangeError;
|
|
4356
|
+
if (deferred) pendingCheckouts.clear();
|
|
4357
|
+
for (const pending of (0, import_workspace_project_config_policy.deferredProjectConfigurationPendingBranches)(message.projects)) {
|
|
4358
|
+
pendingCheckouts.set(projectBranchKey(pending.projectId, pending.branchName), pending);
|
|
3247
4359
|
}
|
|
3248
4360
|
return {
|
|
3249
4361
|
result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
|
|
3250
|
-
pending: [...pendingCheckouts.values()]
|
|
4362
|
+
pending: [...pendingCheckouts.values()].sort(
|
|
4363
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4364
|
+
),
|
|
3251
4365
|
aheadOfOriginBranches: []
|
|
3252
4366
|
};
|
|
3253
4367
|
} finally {
|
|
@@ -3342,12 +4456,22 @@ async function startWorker(options) {
|
|
|
3342
4456
|
ws.addEventListener("message", (event) => {
|
|
3343
4457
|
void (async () => {
|
|
3344
4458
|
const message = JSON.parse(String(event.data));
|
|
4459
|
+
const messageAdmissionGeneration = workerAdmissionGeneration;
|
|
4460
|
+
const assertMessageAdmission = () => assertWorkerChildAdmission({
|
|
4461
|
+
capturedGeneration: messageAdmissionGeneration,
|
|
4462
|
+
currentGeneration: workerAdmissionGeneration,
|
|
4463
|
+
sourceSocketIsCurrent: currentWorkerSocket === ws,
|
|
4464
|
+
sourceSocketOpen: ws.readyState === WebSocket.OPEN,
|
|
4465
|
+
workspaceConfigured,
|
|
4466
|
+
runtimeHealthy: !shutdownAfterClose && !heartbeatTimedOut
|
|
4467
|
+
});
|
|
3345
4468
|
if (message.type === "connected") {
|
|
3346
4469
|
return;
|
|
3347
4470
|
}
|
|
3348
4471
|
if (message.type === "workspace_config") {
|
|
4472
|
+
advanceWorkerAdmissionGeneration();
|
|
3349
4473
|
const configured = await configureWorkerWorkspace(message);
|
|
3350
|
-
|
|
4474
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3351
4475
|
type: "workspace_configured",
|
|
3352
4476
|
requestId: message.requestId,
|
|
3353
4477
|
result: configured.result,
|
|
@@ -3382,59 +4506,85 @@ async function startWorker(options) {
|
|
|
3382
4506
|
}
|
|
3383
4507
|
if (message.type === "create_project_branch") {
|
|
3384
4508
|
try {
|
|
3385
|
-
|
|
4509
|
+
const pendingBranch = {
|
|
4510
|
+
branchId: message.branchId,
|
|
3386
4511
|
projectId: message.projectId,
|
|
3387
4512
|
branchName: message.targetBranch
|
|
3388
|
-
}
|
|
4513
|
+
};
|
|
3389
4514
|
const created = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3390
4515
|
const project = projectConfigById.get(message.projectId);
|
|
3391
4516
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3392
|
-
|
|
3393
|
-
|
|
4517
|
+
(0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
|
|
4518
|
+
if (!(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) {
|
|
4519
|
+
throw new Error(`Project ${message.projectId} branch creation is disabled while its repository connection is transitioning`);
|
|
3394
4520
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
branchName
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
{
|
|
3411
|
-
|
|
3412
|
-
sourceBranchName: message.sourceBranch,
|
|
3413
|
-
baseCommitHash: createdBranch.baseCommitHash
|
|
4521
|
+
const pendingWasRecorded = projectWorkspaceState.locallyPendingCreatedBranches.some(
|
|
4522
|
+
({ projectId, branchName }) => projectId === message.projectId && branchName === message.targetBranch
|
|
4523
|
+
);
|
|
4524
|
+
projectWorkspaceState = projectWorkspaceStateStore.recordPendingCreatedBranch(pendingBranch);
|
|
4525
|
+
let gitBranchCreated = false;
|
|
4526
|
+
try {
|
|
4527
|
+
if (!readyProjectIds.has(project.projectId)) {
|
|
4528
|
+
throw new Error(`Project ${project.projectPath} is not initialized on this worker`);
|
|
4529
|
+
}
|
|
4530
|
+
if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
|
|
4531
|
+
throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
|
|
4532
|
+
}
|
|
4533
|
+
const existingBranch = project.branches.find(({ branchName }) => branchName === message.targetBranch);
|
|
4534
|
+
if (existingBranch) {
|
|
4535
|
+
if (!pendingWasRecorded) throw new Error(`Project branch ${message.targetBranch} already exists`);
|
|
4536
|
+
if (existingBranch.branchId !== message.branchId) {
|
|
4537
|
+
throw new Error(`Pending project branch ${message.targetBranch} belongs to another durable branch incarnation`);
|
|
3414
4538
|
}
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
4539
|
+
const existingPath = configuredProjectBranchPath(projectsRoot, project, message.targetBranch);
|
|
4540
|
+
if (!hasProjectWorktree(existingPath)) {
|
|
4541
|
+
throw new Error(`Pending project branch ${message.targetBranch} is not initialized on this worker`);
|
|
4542
|
+
}
|
|
4543
|
+
return { branchPath: existingPath, baseCommitHash: existingBranch.baseCommitHash };
|
|
4544
|
+
}
|
|
4545
|
+
const createdBranch = (0, import_project_worktrees.createOrRetryLinkedProjectBranch)({
|
|
4546
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
4547
|
+
primaryBranchName: primaryProjectBranch(project),
|
|
4548
|
+
sourceBranchName: message.sourceBranch,
|
|
4549
|
+
branchName: message.targetBranch,
|
|
4550
|
+
pendingRetry: pendingWasRecorded
|
|
4551
|
+
});
|
|
4552
|
+
gitBranchCreated = true;
|
|
4553
|
+
projectConfigById.set(project.projectId, {
|
|
4554
|
+
...project,
|
|
4555
|
+
branches: [
|
|
4556
|
+
...project.branches,
|
|
4557
|
+
{
|
|
4558
|
+
branchId: message.branchId,
|
|
4559
|
+
branchName: message.targetBranch,
|
|
4560
|
+
sourceBranchName: message.sourceBranch,
|
|
4561
|
+
baseCommitHash: createdBranch.baseCommitHash
|
|
4562
|
+
}
|
|
4563
|
+
]
|
|
4564
|
+
});
|
|
4565
|
+
lastObservedProjectHeads.set(projectBranchKey(project.projectId, message.targetBranch), null);
|
|
4566
|
+
return createdBranch;
|
|
4567
|
+
} catch (error) {
|
|
4568
|
+
if (!pendingWasRecorded && !gitBranchCreated && !(0, import_project_worktrees.projectBranchMayExistAfterCreateFailure)(error)) {
|
|
4569
|
+
projectWorkspaceState = projectWorkspaceStateStore.rollbackPendingCreatedBranch(pendingBranch);
|
|
4570
|
+
}
|
|
4571
|
+
throw error;
|
|
4572
|
+
}
|
|
3419
4573
|
});
|
|
4574
|
+
pendingCreatedBranchPublicationNotBefore.set(
|
|
4575
|
+
(0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.targetBranch),
|
|
4576
|
+
Date.now() + import_workspace_automatic_sync_policy.PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS
|
|
4577
|
+
);
|
|
3420
4578
|
sendWorkerMessage(ws, {
|
|
3421
4579
|
type: "operation_result",
|
|
3422
4580
|
requestId: message.requestId,
|
|
3423
4581
|
result: {
|
|
3424
4582
|
type: "create_project_branch",
|
|
4583
|
+
branchId: message.branchId,
|
|
3425
4584
|
branchName: message.targetBranch,
|
|
3426
4585
|
baseCommitHash: created.baseCommitHash
|
|
3427
4586
|
}
|
|
3428
4587
|
});
|
|
3429
|
-
markWorkspaceDirty(
|
|
3430
|
-
{
|
|
3431
|
-
type: "manual",
|
|
3432
|
-
projectId: message.projectId,
|
|
3433
|
-
branchName: message.targetBranch,
|
|
3434
|
-
detail: `created project branch from ${message.sourceBranch}`
|
|
3435
|
-
},
|
|
3436
|
-
true
|
|
3437
|
-
);
|
|
3438
4588
|
} catch (error) {
|
|
3439
4589
|
sendWorkerMessage(ws, {
|
|
3440
4590
|
type: "operation_result",
|
|
@@ -3447,42 +4597,66 @@ async function startWorker(options) {
|
|
|
3447
4597
|
if (message.type === "delete_project_branch") {
|
|
3448
4598
|
try {
|
|
3449
4599
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
3450
|
-
|
|
3451
|
-
projectId: message.projectId,
|
|
3452
|
-
branchName: message.branchName
|
|
3453
|
-
});
|
|
3454
|
-
await workspaceSyncSingleFlight.runMutation(() => {
|
|
4600
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3455
4601
|
const project = projectConfigById.get(message.projectId);
|
|
3456
4602
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3457
|
-
|
|
4603
|
+
(0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
|
|
4604
|
+
if (!(0, import_repository_transition_policy.repositoryMirrorWritesAllowed)(project)) {
|
|
4605
|
+
throw new Error(`Project ${message.projectId} branch deletion is disabled while its repository connection is transitioning`);
|
|
4606
|
+
}
|
|
4607
|
+
const configuredBranch = project.branches.find(({ branchName }) => branchName === message.branchName);
|
|
4608
|
+
const pendingLocalBranch = projectWorkspaceState.locallyPendingCreatedBranches.find(
|
|
4609
|
+
({ projectId, branchName }) => projectId === message.projectId && branchName === message.branchName
|
|
4610
|
+
);
|
|
4611
|
+
(0, import_workspace_branch_incarnation_policy.assertProjectBranchDeletionIncarnation)({
|
|
4612
|
+
requiredBranchId: message.branchId,
|
|
4613
|
+
configuredBranch,
|
|
4614
|
+
pendingLocalBranch
|
|
4615
|
+
});
|
|
4616
|
+
const hasDurableTombstone = projectWorkspaceState.tombstones.some(
|
|
4617
|
+
(tombstone) => tombstone.kind === "branch" && tombstone.projectId === message.projectId && tombstone.branchName === message.branchName
|
|
4618
|
+
);
|
|
4619
|
+
if (!configuredBranch && !pendingLocalBranch && !hasDurableTombstone && !pendingMirrorDeletes.has(deletionKey)) {
|
|
4620
|
+
return false;
|
|
4621
|
+
}
|
|
4622
|
+
projectWorkspaceState = projectWorkspaceStateStore.beginBranchDeletion({
|
|
4623
|
+
projectId: message.projectId,
|
|
4624
|
+
branchName: message.branchName
|
|
4625
|
+
});
|
|
4626
|
+
pendingCreatedBranchPublicationNotBefore.delete((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(message.projectId, message.branchName));
|
|
4627
|
+
if (pendingMirrorDeletes.has(deletionKey)) return true;
|
|
3458
4628
|
stageProjectBranchDeletion(project, message.branchName, true);
|
|
3459
4629
|
projectConfigById.set(project.projectId, {
|
|
3460
4630
|
...project,
|
|
3461
4631
|
branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
|
|
3462
4632
|
});
|
|
4633
|
+
return true;
|
|
3463
4634
|
});
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
4635
|
+
if (deletionNeedsSync) {
|
|
4636
|
+
const result = await runWorkspaceSync({
|
|
4637
|
+
trigger: {
|
|
4638
|
+
type: "manual",
|
|
4639
|
+
projectId: message.projectId,
|
|
4640
|
+
branchName: message.branchName,
|
|
4641
|
+
detail: "deleted project branch"
|
|
4642
|
+
},
|
|
4643
|
+
confirmedLargeDiff: true,
|
|
4644
|
+
confirmationReason: "explicit project branch deletion",
|
|
4645
|
+
sendResult: false
|
|
4646
|
+
});
|
|
4647
|
+
if (result.outcome === "failed" || result.outcome === "conflict_blocked" || result.outcome === "large_diff_blocked") {
|
|
4648
|
+
throw new Error(result.error ?? `Project branch deletion workspace sync ended with ${result.outcome}`);
|
|
4649
|
+
}
|
|
4650
|
+
if (pendingMirrorDeletes.has(deletionKey)) {
|
|
4651
|
+
throw new Error("Project branch deletion did not remove the hidden mirror ref");
|
|
4652
|
+
}
|
|
3480
4653
|
}
|
|
3481
4654
|
sendWorkerMessage(ws, {
|
|
3482
4655
|
type: "operation_result",
|
|
3483
4656
|
requestId: message.requestId,
|
|
3484
4657
|
result: {
|
|
3485
4658
|
type: "delete_project_branch",
|
|
4659
|
+
branchId: message.branchId,
|
|
3486
4660
|
branchName: message.branchName
|
|
3487
4661
|
}
|
|
3488
4662
|
});
|
|
@@ -3670,9 +4844,13 @@ async function startWorker(options) {
|
|
|
3670
4844
|
});
|
|
3671
4845
|
return;
|
|
3672
4846
|
}
|
|
3673
|
-
|
|
4847
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
4848
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
4849
|
+
});
|
|
4850
|
+
let releaseWorkspaceMutation;
|
|
3674
4851
|
let mutationLeaseTransferred = false;
|
|
3675
4852
|
try {
|
|
4853
|
+
releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
|
|
3676
4854
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3677
4855
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
3678
4856
|
`);
|
|
@@ -3681,6 +4859,7 @@ async function startWorker(options) {
|
|
|
3681
4859
|
message,
|
|
3682
4860
|
resolvedTarget,
|
|
3683
4861
|
planRoot,
|
|
4862
|
+
assertAdmission: assertMessageAdmission,
|
|
3684
4863
|
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
3685
4864
|
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
3686
4865
|
onTerminal: () => {
|
|
@@ -3690,13 +4869,14 @@ async function startWorker(options) {
|
|
|
3690
4869
|
});
|
|
3691
4870
|
mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
|
|
3692
4871
|
} catch (error) {
|
|
3693
|
-
|
|
4872
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3694
4873
|
type: "pty_error",
|
|
3695
4874
|
requestId: message.requestId,
|
|
3696
4875
|
ptyId: message.ptyId,
|
|
3697
4876
|
error: error instanceof Error ? error.message : String(error)
|
|
3698
4877
|
});
|
|
3699
4878
|
} finally {
|
|
4879
|
+
workspaceSyncPriorityPtyTargets.delete(message.ptyId);
|
|
3700
4880
|
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
3701
4881
|
}
|
|
3702
4882
|
return;
|
|
@@ -3733,15 +4913,29 @@ async function startWorker(options) {
|
|
|
3733
4913
|
return;
|
|
3734
4914
|
}
|
|
3735
4915
|
let result;
|
|
4916
|
+
let targetReserved = false;
|
|
3736
4917
|
try {
|
|
4918
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
4919
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
4920
|
+
targetReserved = true;
|
|
4921
|
+
});
|
|
3737
4922
|
const runCommand = async () => {
|
|
3738
4923
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3739
4924
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
3740
4925
|
`);
|
|
3741
|
-
return await executeCommand({
|
|
4926
|
+
return await executeCommand({
|
|
4927
|
+
message,
|
|
4928
|
+
resolvedTarget,
|
|
4929
|
+
baseUrl,
|
|
4930
|
+
token,
|
|
4931
|
+
artifactRoot,
|
|
4932
|
+
planRoot,
|
|
4933
|
+
assertAdmission: assertMessageAdmission
|
|
4934
|
+
});
|
|
3742
4935
|
};
|
|
3743
4936
|
result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
|
|
3744
4937
|
} catch (error) {
|
|
4938
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
3745
4939
|
result = {
|
|
3746
4940
|
type: "exec_result",
|
|
3747
4941
|
requestId: message.requestId,
|
|
@@ -3750,6 +4944,8 @@ async function startWorker(options) {
|
|
|
3750
4944
|
exitCode: 1,
|
|
3751
4945
|
error: error instanceof Error ? error.message : String(error)
|
|
3752
4946
|
};
|
|
4947
|
+
} finally {
|
|
4948
|
+
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
3753
4949
|
}
|
|
3754
4950
|
ws.send(JSON.stringify(result));
|
|
3755
4951
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3787,7 +4983,8 @@ async function startWorker(options) {
|
|
|
3787
4983
|
baseUrl,
|
|
3788
4984
|
token,
|
|
3789
4985
|
artifactRoot,
|
|
3790
|
-
planRoot
|
|
4986
|
+
planRoot,
|
|
4987
|
+
assertAdmission: assertMessageAdmission
|
|
3791
4988
|
});
|
|
3792
4989
|
} finally {
|
|
3793
4990
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3803,9 +5000,21 @@ async function startWorker(options) {
|
|
|
3803
5000
|
}
|
|
3804
5001
|
}
|
|
3805
5002
|
};
|
|
3806
|
-
const execution = (
|
|
5003
|
+
const execution = (async () => {
|
|
5004
|
+
let targetReserved = false;
|
|
5005
|
+
try {
|
|
5006
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
5007
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
5008
|
+
targetReserved = true;
|
|
5009
|
+
});
|
|
5010
|
+
await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
|
|
5011
|
+
} finally {
|
|
5012
|
+
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
5013
|
+
}
|
|
5014
|
+
})();
|
|
3807
5015
|
void execution.catch((error) => {
|
|
3808
|
-
|
|
5016
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
5017
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3809
5018
|
type: "exec_start_error",
|
|
3810
5019
|
requestId: message.requestId,
|
|
3811
5020
|
runId: message.runId,
|
|
@@ -3815,6 +5024,14 @@ async function startWorker(options) {
|
|
|
3815
5024
|
return;
|
|
3816
5025
|
}
|
|
3817
5026
|
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") {
|
|
5027
|
+
const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
|
|
5028
|
+
const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
|
|
5029
|
+
if (reservesVisibleWorkspace) {
|
|
5030
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
5031
|
+
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
5032
|
+
});
|
|
5033
|
+
}
|
|
5034
|
+
let dirtyTrigger;
|
|
3818
5035
|
try {
|
|
3819
5036
|
const result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
|
|
3820
5037
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
@@ -3834,13 +5051,14 @@ async function startWorker(options) {
|
|
|
3834
5051
|
result
|
|
3835
5052
|
})
|
|
3836
5053
|
);
|
|
3837
|
-
if (
|
|
3838
|
-
|
|
5054
|
+
if (mutatesVisibleWorkspace) {
|
|
5055
|
+
dirtyTrigger = {
|
|
3839
5056
|
type: message.type,
|
|
3840
5057
|
sessionId: message.sessionId,
|
|
3841
5058
|
toolCallId: message.requestId,
|
|
3842
5059
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
3843
|
-
}
|
|
5060
|
+
};
|
|
5061
|
+
markWorkspaceDirty(dirtyTrigger);
|
|
3844
5062
|
}
|
|
3845
5063
|
} catch (error) {
|
|
3846
5064
|
ws.send(
|
|
@@ -3850,6 +5068,13 @@ async function startWorker(options) {
|
|
|
3850
5068
|
error: error instanceof Error ? error.message : String(error)
|
|
3851
5069
|
})
|
|
3852
5070
|
);
|
|
5071
|
+
} finally {
|
|
5072
|
+
if (reservesVisibleWorkspace) {
|
|
5073
|
+
workspaceSyncPriorityOperationTargets.delete(message.requestId);
|
|
5074
|
+
if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
|
|
5075
|
+
scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
3853
5078
|
}
|
|
3854
5079
|
return;
|
|
3855
5080
|
}
|
|
@@ -3887,6 +5112,7 @@ async function startWorker(options) {
|
|
|
3887
5112
|
terminalReplayTimer = void 0;
|
|
3888
5113
|
}
|
|
3889
5114
|
if (currentWorkerSocket === ws) {
|
|
5115
|
+
advanceWorkerAdmissionGeneration();
|
|
3890
5116
|
currentWorkerSocket = null;
|
|
3891
5117
|
}
|
|
3892
5118
|
closeAllPtys();
|
|
@@ -3899,16 +5125,16 @@ async function startWorker(options) {
|
|
|
3899
5125
|
if (shutdownAfterClose) {
|
|
3900
5126
|
process.exit(0);
|
|
3901
5127
|
}
|
|
3902
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5128
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3903
5129
|
process.stderr.write(
|
|
3904
|
-
`[r5d-worker] ${activeProcesses.size} process(es)
|
|
5130
|
+
`[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 ${import_supervisor.WORKER_RECONNECT_DELAY_MS}ms
|
|
3905
5131
|
`
|
|
3906
5132
|
);
|
|
3907
5133
|
const reconnect = () => {
|
|
3908
5134
|
void startWorker(options).catch((error) => {
|
|
3909
5135
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
3910
5136
|
`);
|
|
3911
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5137
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3912
5138
|
setTimeout(reconnect, import_supervisor.WORKER_RECONNECT_DELAY_MS);
|
|
3913
5139
|
return;
|
|
3914
5140
|
}
|
|
@@ -3991,6 +5217,7 @@ if (isCliEntrypoint()) {
|
|
|
3991
5217
|
resolveWorkerFilePath,
|
|
3992
5218
|
resolveWorkerSessionTarget,
|
|
3993
5219
|
syncSessionArtifacts,
|
|
5220
|
+
workerChildProcessEnvironment,
|
|
3994
5221
|
workerGitSecurityTestHarness,
|
|
3995
5222
|
writeWorkerTextFile
|
|
3996
5223
|
});
|