@ricsam/r5d-worker 0.0.78 → 0.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cjs/main.cjs +1550 -335
- package/dist/cjs/managed-paths.cjs +101 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-workspace-state.cjs +184 -5
- package/dist/cjs/project-worktrees.cjs +676 -58
- package/dist/cjs/registry-auth.cjs +310 -0
- package/dist/cjs/repository-transition-policy.cjs +49 -0
- package/dist/cjs/supervisor.cjs +62 -11
- package/dist/cjs/working-tree-mirror.cjs +196 -36
- package/dist/cjs/workspace-automatic-sync-policy.cjs +69 -0
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +37 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +18 -0
- package/dist/cjs/workspace-git-sync.cjs +1037 -54
- package/dist/cjs/workspace-mount-boundary.cjs +71 -0
- package/dist/cjs/workspace-path-move.cjs +195 -0
- package/dist/cjs/workspace-preserve-only-policy.cjs +36 -0
- package/dist/cjs/workspace-project-config-policy.cjs +46 -0
- package/dist/cjs/workspace-publication-evidence.cjs +46 -0
- package/dist/mjs/main.mjs +1584 -341
- package/dist/mjs/managed-paths.mjs +100 -1
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-workspace-state.mjs +181 -5
- package/dist/mjs/project-worktrees.mjs +667 -57
- package/dist/mjs/registry-auth.mjs +269 -0
- package/dist/mjs/repository-transition-policy.mjs +22 -0
- package/dist/mjs/supervisor.mjs +61 -11
- package/dist/mjs/working-tree-mirror.mjs +195 -36
- package/dist/mjs/workspace-automatic-sync-policy.mjs +40 -0
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +13 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +17 -0
- package/dist/mjs/workspace-git-sync.mjs +1032 -54
- package/dist/mjs/workspace-mount-boundary.mjs +37 -0
- package/dist/mjs/workspace-path-move.mjs +160 -0
- package/dist/mjs/workspace-preserve-only-policy.mjs +11 -0
- package/dist/mjs/workspace-project-config-policy.mjs +21 -0
- package/dist/mjs/workspace-publication-evidence.mjs +21 -0
- package/dist/types/credential-authority-lock-fixture.d.ts +1 -0
- package/dist/types/main.d.ts +175 -1
- package/dist/types/managed-paths.d.ts +11 -0
- package/dist/types/project-workspace-state.d.ts +45 -1
- package/dist/types/project-worktrees.d.ts +119 -2
- package/dist/types/registry-auth.d.ts +47 -0
- package/dist/types/repository-transition-policy.d.ts +26 -0
- package/dist/types/supervisor-daemonized-fixture.d.ts +1 -0
- package/dist/types/supervisor-signal-fixture.d.ts +1 -0
- package/dist/types/supervisor.d.ts +6 -4
- package/dist/types/working-tree-mirror.d.ts +14 -0
- package/dist/types/workspace-automatic-sync-policy.d.ts +37 -0
- package/dist/types/workspace-branch-incarnation-policy.d.ts +14 -0
- package/dist/types/workspace-command-sync-policy.d.ts +9 -0
- package/dist/types/workspace-git-sync.d.ts +33 -3
- package/dist/types/workspace-mount-boundary.d.ts +10 -0
- package/dist/types/workspace-path-move.d.ts +21 -0
- package/dist/types/workspace-preserve-only-policy.d.ts +15 -0
- package/dist/types/workspace-project-config-policy.d.ts +34 -0
- package/dist/types/workspace-publication-evidence.d.ts +17 -0
- package/package.json +1 -1
package/dist/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
|
};
|
|
@@ -3059,6 +3908,28 @@ async function startWorker(options) {
|
|
|
3059
3908
|
resetToCanonical: input.resetToCanonical
|
|
3060
3909
|
});
|
|
3061
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
|
+
});
|
|
3062
3933
|
return failedWorkspaceSyncResult(attemptId, input.trigger, error);
|
|
3063
3934
|
}
|
|
3064
3935
|
});
|
|
@@ -3080,7 +3951,13 @@ async function startWorker(options) {
|
|
|
3080
3951
|
};
|
|
3081
3952
|
const scheduleAutomaticWorkspaceSync = (trigger, delayMs = WORKSPACE_GIT_QUIET_MS) => {
|
|
3082
3953
|
pendingAutomaticTrigger = trigger;
|
|
3083
|
-
|
|
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") {
|
|
3084
3961
|
return;
|
|
3085
3962
|
}
|
|
3086
3963
|
if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
|
|
@@ -3088,8 +3965,14 @@ async function startWorker(options) {
|
|
|
3088
3965
|
() => {
|
|
3089
3966
|
workspaceAutomaticTimer = void 0;
|
|
3090
3967
|
const scheduledTrigger = pendingAutomaticTrigger;
|
|
3091
|
-
pendingAutomaticTrigger = void 0;
|
|
3092
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;
|
|
3093
3976
|
automaticSyncInFlight = true;
|
|
3094
3977
|
void runWorkspaceSync({ trigger: scheduledTrigger }).then((result) => {
|
|
3095
3978
|
if (result.outcome === "failed") {
|
|
@@ -3109,7 +3992,7 @@ async function startWorker(options) {
|
|
|
3109
3992
|
}
|
|
3110
3993
|
});
|
|
3111
3994
|
},
|
|
3112
|
-
Math.max(0, delayMs)
|
|
3995
|
+
Math.max(0, delayMs, initialDeferral?.kind === "creation_grace" ? initialDeferral.retryAfterMs : 0)
|
|
3113
3996
|
);
|
|
3114
3997
|
workspaceAutomaticTimer.unref();
|
|
3115
3998
|
};
|
|
@@ -3117,32 +4000,190 @@ async function startWorker(options) {
|
|
|
3117
4000
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
3118
4001
|
};
|
|
3119
4002
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
3120
|
-
const resolveMessageTarget = (target) =>
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
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
|
+
};
|
|
3126
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;
|
|
3127
4139
|
workspaceSyncRequestsInFlight += 1;
|
|
3128
4140
|
try {
|
|
3129
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
|
+
}
|
|
3130
4159
|
workspaceConfigured = false;
|
|
4160
|
+
const preserveOnlyBranches = message.projects.flatMap(
|
|
4161
|
+
(project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
|
|
4162
|
+
);
|
|
3131
4163
|
projectWorkspaceState = projectWorkspaceStateStore.reconcile({
|
|
3132
|
-
desiredProjects: message.projects
|
|
4164
|
+
desiredProjects: message.projects,
|
|
4165
|
+
preserveOnlyBranches
|
|
3133
4166
|
});
|
|
3134
|
-
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();
|
|
3135
4177
|
const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
|
|
3136
|
-
requiredCredentialStorePaths.add(workspaceStorePath);
|
|
3137
4178
|
const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
|
|
3138
4179
|
[{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
|
|
3139
|
-
workspaceStorePath
|
|
4180
|
+
workspaceStorePath,
|
|
4181
|
+
desiredCredentialStoreFiles
|
|
3140
4182
|
);
|
|
3141
4183
|
if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
|
|
3142
4184
|
const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
|
|
3143
4185
|
for (const project of message.projects) {
|
|
3144
|
-
|
|
3145
|
-
projectConnection(project);
|
|
4186
|
+
projectConnection(project, desiredCredentialStoreFiles);
|
|
3146
4187
|
}
|
|
3147
4188
|
const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
|
|
3148
4189
|
...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
|
|
@@ -3151,30 +4192,81 @@ async function startWorker(options) {
|
|
|
3151
4192
|
]);
|
|
3152
4193
|
for (const projectId of pendingCredentialProjectIds) {
|
|
3153
4194
|
const desiredProject = desiredProjectConfigById.get(projectId);
|
|
3154
|
-
if (desiredProject) projectConnection(desiredProject);
|
|
3155
|
-
else projectMirrorConnection(projectId);
|
|
3156
|
-
requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
|
|
4195
|
+
if (desiredProject) projectConnection(desiredProject, desiredCredentialStoreFiles);
|
|
4196
|
+
else projectMirrorConnection(projectId, desiredCredentialStoreFiles);
|
|
3157
4197
|
}
|
|
3158
4198
|
for (const deletion of pendingMirrorDeletes.values()) {
|
|
3159
4199
|
const desiredProject = desiredProjectConfigById.get(deletion.projectId);
|
|
3160
|
-
const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
|
|
4200
|
+
const connection = desiredProject ? projectConnection(desiredProject, desiredCredentialStoreFiles) : projectMirrorConnection(deletion.projectId, desiredCredentialStoreFiles);
|
|
3161
4201
|
deletion.mirrorUrl = connection.mirrorUrl;
|
|
3162
4202
|
deletion.credentialHelper = connection.credentialHelper;
|
|
3163
4203
|
deletion.credentialUsername = connection.mirrorCredentialUsername;
|
|
3164
4204
|
}
|
|
3165
|
-
|
|
3166
|
-
|
|
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;
|
|
3167
4229
|
visibleGitIdentity = message.gitIdentity;
|
|
3168
4230
|
workspaceGitIdentity = message.gitIdentity;
|
|
3169
4231
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
3170
4232
|
workspaceCredentialHelper = nextWorkspaceCredentialHelper;
|
|
3171
4233
|
workspaceCredentialUsername = workerCredentialUsername;
|
|
3172
|
-
(0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes,
|
|
4234
|
+
(0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, projectWorkspaceState.desiredProjects);
|
|
3173
4235
|
const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
|
|
3174
4236
|
...project,
|
|
3175
4237
|
branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
|
|
3176
4238
|
}));
|
|
3177
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)));
|
|
3178
4270
|
for (const existingProject of projectConfigById.values()) {
|
|
3179
4271
|
const nextProject = nextProjectConfigById.get(existingProject.projectId);
|
|
3180
4272
|
if (!nextProject) {
|
|
@@ -3184,6 +4276,7 @@ async function startWorker(options) {
|
|
|
3184
4276
|
const nextBranches = new Set(nextProject.branches.map(({ branchName }) => branchName));
|
|
3185
4277
|
for (const existingBranch of existingProject.branches) {
|
|
3186
4278
|
if (!nextBranches.has(existingBranch.branchName)) {
|
|
4279
|
+
if (preserveOnlyKeys.has(projectBranchKey(existingProject.projectId, existingBranch.branchName))) continue;
|
|
3187
4280
|
stageProjectBranchDeletion(existingProject, existingBranch.branchName, false);
|
|
3188
4281
|
}
|
|
3189
4282
|
}
|
|
@@ -3199,40 +4292,30 @@ async function startWorker(options) {
|
|
|
3199
4292
|
for (const projectId of [...readyProjectIds]) {
|
|
3200
4293
|
if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
|
|
3201
4294
|
}
|
|
3202
|
-
(0, import_workspace_git_sync.ensureWorkspaceGitClone)({
|
|
3203
|
-
workspacePath: workspaceShadowRoot,
|
|
3204
|
-
remoteUrl: message.workspaceRemoteUrl,
|
|
3205
|
-
credentialHelper: nextWorkspaceCredentialHelper,
|
|
3206
|
-
credentialUsername: workerCredentialUsername,
|
|
3207
|
-
gitIdentity: message.gitIdentity
|
|
3208
|
-
});
|
|
3209
|
-
pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
|
|
3210
4295
|
stageDurableWorkspaceDeletions();
|
|
3211
4296
|
const mountsBeforeGit = buildWorkspaceMounts();
|
|
3212
4297
|
(0, import_workspace_git_sync.hydrateWorkspaceGitMounts)(
|
|
3213
4298
|
workspaceShadowRoot,
|
|
3214
|
-
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 }
|
|
3215
4305
|
);
|
|
3216
4306
|
ensureConfiguredProjects();
|
|
3217
|
-
|
|
4307
|
+
const result = await performWorkspaceSync({
|
|
3218
4308
|
attemptId: crypto.randomUUID(),
|
|
3219
4309
|
trigger: { type: "connect" }
|
|
3220
4310
|
});
|
|
3221
|
-
if (["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
3222
|
-
const staleAfterInbound = projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot);
|
|
3223
|
-
if (staleAfterInbound.length > 0) {
|
|
3224
|
-
pruneStaleOuterWorkspaceEntries(staleAfterInbound);
|
|
3225
|
-
stageDurableWorkspaceDeletions();
|
|
3226
|
-
result = await performWorkspaceSync({
|
|
3227
|
-
attemptId: crypto.randomUUID(),
|
|
3228
|
-
trigger: { type: "connect" }
|
|
3229
|
-
});
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
4311
|
const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
|
|
3233
4312
|
if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
|
|
4313
|
+
const activeMountIds = new Set(result.activeMountIds ?? []);
|
|
3234
4314
|
const layoutCleanupIds = projectWorkspaceState.pendingTreeDeletions.flatMap(
|
|
3235
|
-
(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] : []
|
|
3236
4319
|
);
|
|
3237
4320
|
if (layoutCleanupIds.length > 0) {
|
|
3238
4321
|
projectWorkspaceState = projectWorkspaceStateStore.recordTreePublication({
|
|
@@ -3248,18 +4331,37 @@ async function startWorker(options) {
|
|
|
3248
4331
|
return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
|
|
3249
4332
|
});
|
|
3250
4333
|
} catch (error) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
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
|
+
);
|
|
3258
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);
|
|
3259
4359
|
}
|
|
3260
4360
|
return {
|
|
3261
4361
|
result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
|
|
3262
|
-
pending: [...pendingCheckouts.values()]
|
|
4362
|
+
pending: [...pendingCheckouts.values()].sort(
|
|
4363
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
4364
|
+
),
|
|
3263
4365
|
aheadOfOriginBranches: []
|
|
3264
4366
|
};
|
|
3265
4367
|
} finally {
|
|
@@ -3354,12 +4456,22 @@ async function startWorker(options) {
|
|
|
3354
4456
|
ws.addEventListener("message", (event) => {
|
|
3355
4457
|
void (async () => {
|
|
3356
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
|
+
});
|
|
3357
4468
|
if (message.type === "connected") {
|
|
3358
4469
|
return;
|
|
3359
4470
|
}
|
|
3360
4471
|
if (message.type === "workspace_config") {
|
|
4472
|
+
advanceWorkerAdmissionGeneration();
|
|
3361
4473
|
const configured = await configureWorkerWorkspace(message);
|
|
3362
|
-
|
|
4474
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3363
4475
|
type: "workspace_configured",
|
|
3364
4476
|
requestId: message.requestId,
|
|
3365
4477
|
result: configured.result,
|
|
@@ -3394,59 +4506,85 @@ async function startWorker(options) {
|
|
|
3394
4506
|
}
|
|
3395
4507
|
if (message.type === "create_project_branch") {
|
|
3396
4508
|
try {
|
|
3397
|
-
|
|
4509
|
+
const pendingBranch = {
|
|
4510
|
+
branchId: message.branchId,
|
|
3398
4511
|
projectId: message.projectId,
|
|
3399
4512
|
branchName: message.targetBranch
|
|
3400
|
-
}
|
|
4513
|
+
};
|
|
3401
4514
|
const created = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3402
4515
|
const project = projectConfigById.get(message.projectId);
|
|
3403
4516
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
if (!project.branches.some(({ branchName }) => branchName === message.sourceBranch)) {
|
|
3408
|
-
throw new Error(`Source branch ${message.sourceBranch} is missing from the worker workspace configuration`);
|
|
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`);
|
|
3409
4520
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
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`);
|
|
3426
4538
|
}
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
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
|
+
}
|
|
3431
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
|
+
);
|
|
3432
4578
|
sendWorkerMessage(ws, {
|
|
3433
4579
|
type: "operation_result",
|
|
3434
4580
|
requestId: message.requestId,
|
|
3435
4581
|
result: {
|
|
3436
4582
|
type: "create_project_branch",
|
|
4583
|
+
branchId: message.branchId,
|
|
3437
4584
|
branchName: message.targetBranch,
|
|
3438
4585
|
baseCommitHash: created.baseCommitHash
|
|
3439
4586
|
}
|
|
3440
4587
|
});
|
|
3441
|
-
markWorkspaceDirty(
|
|
3442
|
-
{
|
|
3443
|
-
type: "manual",
|
|
3444
|
-
projectId: message.projectId,
|
|
3445
|
-
branchName: message.targetBranch,
|
|
3446
|
-
detail: `created project branch from ${message.sourceBranch}`
|
|
3447
|
-
},
|
|
3448
|
-
true
|
|
3449
|
-
);
|
|
3450
4588
|
} catch (error) {
|
|
3451
4589
|
sendWorkerMessage(ws, {
|
|
3452
4590
|
type: "operation_result",
|
|
@@ -3459,42 +4597,66 @@ async function startWorker(options) {
|
|
|
3459
4597
|
if (message.type === "delete_project_branch") {
|
|
3460
4598
|
try {
|
|
3461
4599
|
const deletionKey = projectBranchKey(message.projectId, message.branchName);
|
|
3462
|
-
|
|
3463
|
-
projectId: message.projectId,
|
|
3464
|
-
branchName: message.branchName
|
|
3465
|
-
});
|
|
3466
|
-
await workspaceSyncSingleFlight.runMutation(() => {
|
|
4600
|
+
const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
|
|
3467
4601
|
const project = projectConfigById.get(message.projectId);
|
|
3468
4602
|
if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
|
|
3469
|
-
|
|
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;
|
|
3470
4628
|
stageProjectBranchDeletion(project, message.branchName, true);
|
|
3471
4629
|
projectConfigById.set(project.projectId, {
|
|
3472
4630
|
...project,
|
|
3473
4631
|
branches: project.branches.filter(({ branchName }) => branchName !== message.branchName)
|
|
3474
4632
|
});
|
|
4633
|
+
return true;
|
|
3475
4634
|
});
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
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
|
+
}
|
|
3492
4653
|
}
|
|
3493
4654
|
sendWorkerMessage(ws, {
|
|
3494
4655
|
type: "operation_result",
|
|
3495
4656
|
requestId: message.requestId,
|
|
3496
4657
|
result: {
|
|
3497
4658
|
type: "delete_project_branch",
|
|
4659
|
+
branchId: message.branchId,
|
|
3498
4660
|
branchName: message.branchName
|
|
3499
4661
|
}
|
|
3500
4662
|
});
|
|
@@ -3682,9 +4844,13 @@ async function startWorker(options) {
|
|
|
3682
4844
|
});
|
|
3683
4845
|
return;
|
|
3684
4846
|
}
|
|
3685
|
-
|
|
4847
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
4848
|
+
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
4849
|
+
});
|
|
4850
|
+
let releaseWorkspaceMutation;
|
|
3686
4851
|
let mutationLeaseTransferred = false;
|
|
3687
4852
|
try {
|
|
4853
|
+
releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
|
|
3688
4854
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3689
4855
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
|
|
3690
4856
|
`);
|
|
@@ -3693,6 +4859,7 @@ async function startWorker(options) {
|
|
|
3693
4859
|
message,
|
|
3694
4860
|
resolvedTarget,
|
|
3695
4861
|
planRoot,
|
|
4862
|
+
assertAdmission: assertMessageAdmission,
|
|
3696
4863
|
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
3697
4864
|
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
3698
4865
|
onTerminal: () => {
|
|
@@ -3702,13 +4869,14 @@ async function startWorker(options) {
|
|
|
3702
4869
|
});
|
|
3703
4870
|
mutationLeaseTransferred = releaseWorkspaceMutation !== void 0;
|
|
3704
4871
|
} catch (error) {
|
|
3705
|
-
|
|
4872
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3706
4873
|
type: "pty_error",
|
|
3707
4874
|
requestId: message.requestId,
|
|
3708
4875
|
ptyId: message.ptyId,
|
|
3709
4876
|
error: error instanceof Error ? error.message : String(error)
|
|
3710
4877
|
});
|
|
3711
4878
|
} finally {
|
|
4879
|
+
workspaceSyncPriorityPtyTargets.delete(message.ptyId);
|
|
3712
4880
|
if (!mutationLeaseTransferred) releaseWorkspaceMutation?.();
|
|
3713
4881
|
}
|
|
3714
4882
|
return;
|
|
@@ -3745,15 +4913,29 @@ async function startWorker(options) {
|
|
|
3745
4913
|
return;
|
|
3746
4914
|
}
|
|
3747
4915
|
let result;
|
|
4916
|
+
let targetReserved = false;
|
|
3748
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
|
+
});
|
|
3749
4922
|
const runCommand = async () => {
|
|
3750
4923
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
3751
4924
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
3752
4925
|
`);
|
|
3753
|
-
return await executeCommand({
|
|
4926
|
+
return await executeCommand({
|
|
4927
|
+
message,
|
|
4928
|
+
resolvedTarget,
|
|
4929
|
+
baseUrl,
|
|
4930
|
+
token,
|
|
4931
|
+
artifactRoot,
|
|
4932
|
+
planRoot,
|
|
4933
|
+
assertAdmission: assertMessageAdmission
|
|
4934
|
+
});
|
|
3754
4935
|
};
|
|
3755
4936
|
result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
|
|
3756
4937
|
} catch (error) {
|
|
4938
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
3757
4939
|
result = {
|
|
3758
4940
|
type: "exec_result",
|
|
3759
4941
|
requestId: message.requestId,
|
|
@@ -3762,6 +4944,8 @@ async function startWorker(options) {
|
|
|
3762
4944
|
exitCode: 1,
|
|
3763
4945
|
error: error instanceof Error ? error.message : String(error)
|
|
3764
4946
|
};
|
|
4947
|
+
} finally {
|
|
4948
|
+
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
3765
4949
|
}
|
|
3766
4950
|
ws.send(JSON.stringify(result));
|
|
3767
4951
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3799,7 +4983,8 @@ async function startWorker(options) {
|
|
|
3799
4983
|
baseUrl,
|
|
3800
4984
|
token,
|
|
3801
4985
|
artifactRoot,
|
|
3802
|
-
planRoot
|
|
4986
|
+
planRoot,
|
|
4987
|
+
assertAdmission: assertMessageAdmission
|
|
3803
4988
|
});
|
|
3804
4989
|
} finally {
|
|
3805
4990
|
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
@@ -3815,9 +5000,21 @@ async function startWorker(options) {
|
|
|
3815
5000
|
}
|
|
3816
5001
|
}
|
|
3817
5002
|
};
|
|
3818
|
-
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
|
+
})();
|
|
3819
5015
|
void execution.catch((error) => {
|
|
3820
|
-
|
|
5016
|
+
if (error instanceof StaleWorkerAdmissionError) return;
|
|
5017
|
+
sendWorkerMessageFromCurrentSource(ws, {
|
|
3821
5018
|
type: "exec_start_error",
|
|
3822
5019
|
requestId: message.requestId,
|
|
3823
5020
|
runId: message.runId,
|
|
@@ -3827,6 +5024,14 @@ async function startWorker(options) {
|
|
|
3827
5024
|
return;
|
|
3828
5025
|
}
|
|
3829
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;
|
|
3830
5035
|
try {
|
|
3831
5036
|
const result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, async () => {
|
|
3832
5037
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
@@ -3846,13 +5051,14 @@ async function startWorker(options) {
|
|
|
3846
5051
|
result
|
|
3847
5052
|
})
|
|
3848
5053
|
);
|
|
3849
|
-
if (
|
|
3850
|
-
|
|
5054
|
+
if (mutatesVisibleWorkspace) {
|
|
5055
|
+
dirtyTrigger = {
|
|
3851
5056
|
type: message.type,
|
|
3852
5057
|
sessionId: message.sessionId,
|
|
3853
5058
|
toolCallId: message.requestId,
|
|
3854
5059
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
3855
|
-
}
|
|
5060
|
+
};
|
|
5061
|
+
markWorkspaceDirty(dirtyTrigger);
|
|
3856
5062
|
}
|
|
3857
5063
|
} catch (error) {
|
|
3858
5064
|
ws.send(
|
|
@@ -3862,6 +5068,13 @@ async function startWorker(options) {
|
|
|
3862
5068
|
error: error instanceof Error ? error.message : String(error)
|
|
3863
5069
|
})
|
|
3864
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
|
+
}
|
|
3865
5078
|
}
|
|
3866
5079
|
return;
|
|
3867
5080
|
}
|
|
@@ -3899,6 +5112,7 @@ async function startWorker(options) {
|
|
|
3899
5112
|
terminalReplayTimer = void 0;
|
|
3900
5113
|
}
|
|
3901
5114
|
if (currentWorkerSocket === ws) {
|
|
5115
|
+
advanceWorkerAdmissionGeneration();
|
|
3902
5116
|
currentWorkerSocket = null;
|
|
3903
5117
|
}
|
|
3904
5118
|
closeAllPtys();
|
|
@@ -3911,16 +5125,16 @@ async function startWorker(options) {
|
|
|
3911
5125
|
if (shutdownAfterClose) {
|
|
3912
5126
|
process.exit(0);
|
|
3913
5127
|
}
|
|
3914
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5128
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3915
5129
|
process.stderr.write(
|
|
3916
|
-
`[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
|
|
3917
5131
|
`
|
|
3918
5132
|
);
|
|
3919
5133
|
const reconnect = () => {
|
|
3920
5134
|
void startWorker(options).catch((error) => {
|
|
3921
5135
|
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
3922
5136
|
`);
|
|
3923
|
-
if (activeProcesses.size > 0 || pendingProcessTerminals.size > 0) {
|
|
5137
|
+
if (activeProcesses.size > 0 || credentialBearingProcessGroups.size > 0 || activePtys.size > 0 || pendingProcessTerminals.size > 0) {
|
|
3924
5138
|
setTimeout(reconnect, import_supervisor.WORKER_RECONNECT_DELAY_MS);
|
|
3925
5139
|
return;
|
|
3926
5140
|
}
|
|
@@ -4003,6 +5217,7 @@ if (isCliEntrypoint()) {
|
|
|
4003
5217
|
resolveWorkerFilePath,
|
|
4004
5218
|
resolveWorkerSessionTarget,
|
|
4005
5219
|
syncSessionArtifacts,
|
|
5220
|
+
workerChildProcessEnvironment,
|
|
4006
5221
|
workerGitSecurityTestHarness,
|
|
4007
5222
|
writeWorkerTextFile
|
|
4008
5223
|
});
|