@ricsam/r5d-worker 0.0.76 → 0.0.78

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/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
+ workerGitSecurityTestHarness: () => workerGitSecurityTestHarness,
51
52
  writeWorkerTextFile: () => writeWorkerTextFile
52
53
  });
53
54
  module.exports = __toCommonJS(main_exports);
@@ -57,6 +58,7 @@ var import_node_crypto = require("node:crypto");
57
58
  var import_node_os = __toESM(require("node:os"), 1);
58
59
  var import_node_child_process = require("node:child_process");
59
60
  var import_cli_update = require("./cli-update.cjs");
61
+ var import_git_process_environment = require("./git-process-environment.cjs");
60
62
  var import_heartbeat = require("./heartbeat.cjs");
61
63
  var import_process_tree = require("./process-tree.cjs");
62
64
  var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
@@ -659,29 +661,16 @@ const NON_RECURSIVE_GIT_CONFIG_ARGS = [
659
661
  "-c",
660
662
  "push.recurseSubmodules=false"
661
663
  ];
662
- function gitExtraHeaderConfigKey(extraHeaderUrl) {
663
- return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
664
- }
665
- function gitAuthArgs(auth) {
666
- if (!auth) {
667
- return [];
668
- }
669
- const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
670
- return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
671
- }
672
664
  const workerGitCommand = {
673
- commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args]
665
+ commandArgs: (args) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...args]
674
666
  };
675
667
  function runGit(args, options = {}) {
676
- const command = workerGitCommand.commandArgs(args, options.auth);
668
+ const command = workerGitCommand.commandArgs(args);
677
669
  const result = Bun.spawnSync(command, {
678
670
  cwd: options.cwd,
679
671
  stdout: "pipe",
680
672
  stderr: "pipe",
681
- env: {
682
- ...process.env,
683
- GIT_TERMINAL_PROMPT: "0"
684
- }
673
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
685
674
  });
686
675
  if (result.exitCode !== 0) {
687
676
  throw new Error(
@@ -814,28 +803,92 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
814
803
  function canonicalProjectRemoteUrl(baseUrl, projectId) {
815
804
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
816
805
  }
817
- function gitExtraHeaderUrlForRemote(remoteUrl) {
818
- try {
819
- return new URL(remoteUrl).origin;
820
- } catch {
821
- return remoteUrl;
822
- }
823
- }
824
- function gitAuthForRemote(remoteUrl, authHeader) {
825
- if (!authHeader) {
826
- return void 0;
827
- }
828
- return {
829
- extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
830
- header: authHeader
831
- };
832
- }
833
806
  function shellQuote(value) {
834
807
  return `'${value.replace(/'/g, "'\\''")}'`;
835
808
  }
836
- function githubCredentialsPath() {
809
+ function readOnlyCredentialStoreHelper(storePath) {
810
+ return `!f() { if [ "$1" = get ]; then git credential-store --file=${shellQuote(storePath)} get; else return 0; fi; }; f`;
811
+ }
812
+ function credentialStoreDirectory() {
813
+ return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "git-credentials");
814
+ }
815
+ function legacySharedCredentialStorePath() {
837
816
  return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "github-credentials");
838
817
  }
818
+ function credentialStorePathForProject(projectId) {
819
+ validateProjectId(projectId);
820
+ const digest = (0, import_node_crypto.createHash)("sha256").update(`project\0${projectId}`).digest("hex");
821
+ return import_node_path.default.join(credentialStoreDirectory(), `project-${digest}.store`);
822
+ }
823
+ function credentialStorePathForWorkspace(remoteUrl) {
824
+ const digest = (0, import_node_crypto.createHash)("sha256").update(`workspace\0${remoteUrl}`).digest("hex");
825
+ return import_node_path.default.join(credentialStoreDirectory(), `workspace-${digest}.store`);
826
+ }
827
+ function lstatIfExists(filePath) {
828
+ try {
829
+ return import_node_fs.default.lstatSync(filePath);
830
+ } catch (error) {
831
+ if (error.code === "ENOENT") return null;
832
+ throw error;
833
+ }
834
+ }
835
+ function ensurePrivateCredentialStoreDirectory(directoryPath) {
836
+ const stat = lstatIfExists(directoryPath);
837
+ if (stat) {
838
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("Git credential-store path must be a private directory");
839
+ } else {
840
+ import_node_fs.default.mkdirSync(directoryPath, { recursive: true, mode: 448 });
841
+ }
842
+ import_node_fs.default.chmodSync(directoryPath, 448);
843
+ }
844
+ function removeLegacySharedCredentialStore(storePath = legacySharedCredentialStorePath()) {
845
+ import_node_fs.default.rmSync(storePath, { force: true });
846
+ }
847
+ let credentialStoreDirectoryInitialized = false;
848
+ function resetCredentialStoreDirectory(directoryPath = credentialStoreDirectory()) {
849
+ const stat = lstatIfExists(directoryPath);
850
+ if (stat) {
851
+ if (stat.isSymbolicLink() || !stat.isDirectory()) import_node_fs.default.unlinkSync(directoryPath);
852
+ else import_node_fs.default.rmSync(directoryPath, { recursive: true, force: true });
853
+ }
854
+ ensurePrivateCredentialStoreDirectory(directoryPath);
855
+ }
856
+ function initializeCredentialStoreDirectoryOnce() {
857
+ if (credentialStoreDirectoryInitialized) return;
858
+ removeLegacySharedCredentialStore();
859
+ resetCredentialStoreDirectory();
860
+ credentialStoreDirectoryInitialized = true;
861
+ }
862
+ const APP_CREDENTIAL_STORE_FILE = /^(?:project|workspace)-[0-9a-f]{64}\.store$/;
863
+ function pruneCredentialStoreFiles(requiredStorePaths, directoryPath = credentialStoreDirectory()) {
864
+ ensurePrivateCredentialStoreDirectory(directoryPath);
865
+ const resolvedDirectory = import_node_path.default.resolve(directoryPath);
866
+ const requiredNames = /* @__PURE__ */ new Set();
867
+ for (const storePath of requiredStorePaths) {
868
+ const resolvedStorePath = import_node_path.default.resolve(storePath);
869
+ if (import_node_path.default.dirname(resolvedStorePath) !== resolvedDirectory || !APP_CREDENTIAL_STORE_FILE.test(import_node_path.default.basename(resolvedStorePath))) {
870
+ throw new Error("Invalid app-owned Git credential-store path");
871
+ }
872
+ requiredNames.add(import_node_path.default.basename(resolvedStorePath));
873
+ }
874
+ const removed = [];
875
+ for (const name of import_node_fs.default.readdirSync(directoryPath)) {
876
+ if (!APP_CREDENTIAL_STORE_FILE.test(name) || requiredNames.has(name)) continue;
877
+ const storePath = import_node_path.default.join(directoryPath, name);
878
+ const stat = import_node_fs.default.lstatSync(storePath);
879
+ if (!stat.isFile() && !stat.isSymbolicLink()) continue;
880
+ import_node_fs.default.unlinkSync(storePath);
881
+ removed.push(storePath);
882
+ }
883
+ return removed;
884
+ }
885
+ function removeProjectCredentialStore(projectId) {
886
+ const storePath = credentialStorePathForProject(projectId);
887
+ const stat = lstatIfExists(storePath);
888
+ if (!stat) return;
889
+ if (!stat.isFile() && !stat.isSymbolicLink()) throw new Error("Git credential store must be a regular file");
890
+ import_node_fs.default.unlinkSync(storePath);
891
+ }
839
892
  function decodeGitAuthHeader(authHeader) {
840
893
  const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
841
894
  if (match) {
@@ -847,45 +900,84 @@ function decodeGitAuthHeader(authHeader) {
847
900
  const bearer = /^Authorization:\s*Bearer\s+(.+)$/i.exec(authHeader.trim());
848
901
  return bearer?.[1] ? { username: "r5d-worker", password: bearer[1] } : null;
849
902
  }
850
- function writeCredentialStoreEntry(remoteUrl, authHeader) {
903
+ function credentialUsernameForAuthHeader(authHeader) {
851
904
  const credentials = decodeGitAuthHeader(authHeader);
852
- if (!credentials) {
853
- return null;
854
- }
855
- let remote;
905
+ if (!credentials) throw new Error("Unsupported Git authorization header");
906
+ if (/[\0\r\n]/.test(credentials.username)) throw new Error("Invalid Git credential username");
907
+ return credentials.username;
908
+ }
909
+ function replaceCredentialStoreFile(storePath, content) {
910
+ const temporaryPath = import_node_path.default.join(import_node_path.default.dirname(storePath), `.${import_node_path.default.basename(storePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
856
911
  try {
857
- remote = new URL(remoteUrl);
858
- } catch {
859
- return null;
860
- }
861
- if (remote.protocol !== "https:") {
862
- return null;
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 });
863
917
  }
864
- const storePath = githubCredentialsPath();
865
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(storePath), { recursive: true });
866
- const hostKey = `${remote.protocol}//${remote.host}`;
867
- const existing = import_node_fs.default.existsSync(storePath) ? import_node_fs.default.readFileSync(storePath, "utf8").split(/\r?\n/).filter(Boolean) : [];
868
- const next = existing.filter((line) => {
918
+ }
919
+ function writeCredentialStoreEntries(remotes, storePath) {
920
+ const authoritativeEntries = /* @__PURE__ */ new Map();
921
+ for (const input of remotes) {
922
+ let remote;
869
923
  try {
870
- const parsed = new URL(line);
871
- return `${parsed.protocol}//${parsed.host}` !== hostKey;
924
+ remote = new URL(input.remoteUrl);
872
925
  } catch {
873
- return true;
926
+ if (input.authHeader != null) throw new Error("Invalid authenticated Git remote URL");
927
+ continue;
874
928
  }
875
- });
876
- const credentialUrl = new URL(hostKey);
877
- credentialUrl.username = credentials.username;
878
- credentialUrl.password = credentials.password;
879
- next.push(credentialUrl.toString());
880
- import_node_fs.default.writeFileSync(storePath, `${next.join("\n")}
881
- `, { mode: 384 });
882
- import_node_fs.default.chmodSync(storePath, 384);
883
- return storePath;
884
- }
885
- function credentialHelperForRemote(remoteUrl, authHeader) {
886
- const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
887
- return credentialStore ? `store --file=${shellQuote(credentialStore)}` : null;
888
- }
929
+ if ((remote.protocol === "https:" || remote.protocol === "http:") && (remote.username || remote.password)) {
930
+ throw new Error("Git credentials must not be embedded in a remote URL");
931
+ }
932
+ if (remote.protocol !== "https:" && remote.protocol !== "http:") {
933
+ if (input.authHeader != null) throw new Error("Unsupported authenticated Git remote protocol");
934
+ continue;
935
+ }
936
+ const hostKey = `${remote.protocol}//${remote.host}`;
937
+ let replacement = null;
938
+ if (input.authHeader != null) {
939
+ const credentials = decodeGitAuthHeader(input.authHeader);
940
+ if (!credentials) throw new Error("Unsupported Git authorization header");
941
+ const credentialUrl = new URL(hostKey);
942
+ credentialUrl.username = credentials.username;
943
+ credentialUrl.password = credentials.password;
944
+ replacement = credentialUrl.toString();
945
+ }
946
+ if (authoritativeEntries.has(hostKey) && authoritativeEntries.get(hostKey) !== replacement) {
947
+ throw new Error("Conflicting Git credentials for one transport host");
948
+ }
949
+ authoritativeEntries.set(hostKey, replacement);
950
+ }
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
+ const replacements = [...authoritativeEntries.values()].filter((entry) => entry !== null);
956
+ if (replacements.length === 0) {
957
+ if (storeStat) import_node_fs.default.unlinkSync(storePath);
958
+ return null;
959
+ }
960
+ ensurePrivateCredentialStoreDirectory(import_node_path.default.dirname(storePath));
961
+ replaceCredentialStoreFile(storePath, `${replacements.join("\n")}
962
+ `);
963
+ return { hasCredentials: true, storePath };
964
+ }
965
+ function credentialHelperForRemotes(remotes, storePath) {
966
+ const credentialStore = writeCredentialStoreEntries(remotes, storePath);
967
+ return credentialStore?.hasCredentials ? readOnlyCredentialStoreHelper(credentialStore.storePath) : null;
968
+ }
969
+ const workerGitSecurityTestHarness = {
970
+ commandArgs: workerGitCommand.commandArgs,
971
+ credentialHelperForRemotes,
972
+ credentialStorePathForProject,
973
+ credentialStorePathForWorkspace,
974
+ legacySharedCredentialStorePath,
975
+ removeLegacySharedCredentialStore,
976
+ pruneCredentialStoreFiles,
977
+ readOnlyCredentialStoreHelper,
978
+ resetCredentialStoreDirectory,
979
+ replaceCredentialStoreFile
980
+ };
889
981
  function dockerConfigPath() {
890
982
  return import_node_path.default.join(import_node_os.default.homedir(), ".docker", "config.json");
891
983
  }
@@ -2271,6 +2363,7 @@ async function startWorker(options) {
2271
2363
  });
2272
2364
  const config = readConfig(options.configPath);
2273
2365
  const { baseUrl, token } = resolveCredentials(options, config);
2366
+ initializeCredentialStoreDirectoryOnce();
2274
2367
  const label = requireValue(options.label, "Missing required --label <label>");
2275
2368
  validateLabel(label);
2276
2369
  const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
@@ -2317,6 +2410,8 @@ async function startWorker(options) {
2317
2410
  const lastObservedProjectHeads = /* @__PURE__ */ new Map();
2318
2411
  const pendingMirrorDeletes = /* @__PURE__ */ new Map();
2319
2412
  let workspaceRemoteUrl = null;
2413
+ let workspaceCredentialHelper = null;
2414
+ let workspaceCredentialUsername = null;
2320
2415
  let workspaceGitIdentity = null;
2321
2416
  let workspaceConfigured = false;
2322
2417
  let activeWorkspaceIncidentId = null;
@@ -2331,6 +2426,7 @@ async function startWorker(options) {
2331
2426
  let reloadAfterClose = false;
2332
2427
  let shutdownAfterClose = false;
2333
2428
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
2429
+ const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
2334
2430
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2335
2431
  const projectMountId = (projectId, branchName) => `project:${projectId}:${encodeURIComponent(branchName)}`;
2336
2432
  const projectPlanMountId = (projectId, branchName) => `plan:${projectId}:${encodeURIComponent(branchName)}`;
@@ -2338,12 +2434,32 @@ async function startWorker(options) {
2338
2434
  const workspacePlanRelativePath = (projectId, branchName) => import_node_path.default.posix.join("plans", projectId, encodeURIComponent(branchName));
2339
2435
  const projectConnection = (project) => {
2340
2436
  const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, project.projectId);
2341
- const mirrorAuth = gitAuthForRemote(mirrorUrl, bearerAuthHeader);
2342
2437
  const originUrl = project.repoHttpUrl ?? mirrorUrl;
2343
2438
  const originHeader = project.repoHttpUrl ? project.repoAuthHeader : bearerAuthHeader;
2344
- const originAuth = gitAuthForRemote(originUrl, originHeader);
2345
- const credentialHelper = originHeader ? credentialHelperForRemote(originUrl, originHeader) : null;
2346
- return { mirrorUrl, mirrorAuth, originUrl, originAuth, credentialHelper };
2439
+ const credentialHelper = credentialHelperForRemotes(
2440
+ [
2441
+ { remoteUrl: mirrorUrl, authHeader: bearerAuthHeader },
2442
+ { remoteUrl: originUrl, authHeader: originHeader }
2443
+ ],
2444
+ credentialStorePathForProject(project.projectId)
2445
+ );
2446
+ if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2447
+ return {
2448
+ mirrorUrl,
2449
+ mirrorCredentialUsername: workerCredentialUsername,
2450
+ originUrl,
2451
+ originCredentialUsername: originHeader ? credentialUsernameForAuthHeader(originHeader) : null,
2452
+ credentialHelper
2453
+ };
2454
+ };
2455
+ const projectMirrorConnection = (projectId) => {
2456
+ const mirrorUrl = canonicalProjectRemoteUrl(baseUrl, projectId);
2457
+ const credentialHelper = credentialHelperForRemotes(
2458
+ [{ remoteUrl: mirrorUrl, authHeader: bearerAuthHeader }],
2459
+ credentialStorePathForProject(projectId)
2460
+ );
2461
+ if (!credentialHelper) throw new Error("Project mirror credentials are unavailable");
2462
+ return { mirrorUrl, credentialHelper, mirrorCredentialUsername: workerCredentialUsername };
2347
2463
  };
2348
2464
  const durableDeletionForBranch = (projectId, checkoutPathSegments, branchName) => projectWorkspaceState.tombstones.find(
2349
2465
  (tombstone) => tombstone.projectId === projectId && tombstone.checkoutPathSegments[0] === checkoutPathSegments[0] && tombstone.checkoutPathSegments[1] === checkoutPathSegments[1] && (tombstone.kind === "project" || tombstone.branchName === branchName)
@@ -2367,7 +2483,8 @@ async function startWorker(options) {
2367
2483
  branchName,
2368
2484
  gitDirectory: configuredProjectBranchPath(projectsRoot, project, primaryBranchName),
2369
2485
  mirrorUrl: connection.mirrorUrl,
2370
- mirrorAuth: connection.mirrorAuth,
2486
+ credentialHelper: connection.credentialHelper,
2487
+ credentialUsername: connection.mirrorCredentialUsername,
2371
2488
  sourcePath,
2372
2489
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branchName),
2373
2490
  planSourcePath,
@@ -2378,7 +2495,7 @@ async function startWorker(options) {
2378
2495
  };
2379
2496
  const stageProjectDeletion = (project) => {
2380
2497
  const projectRoot = configuredProjectRoot(projectsRoot, project);
2381
- const connection = projectConnection(project);
2498
+ const connection = projectMirrorConnection(project.projectId);
2382
2499
  const branches = project.branches.map(({ branchName }) => ({
2383
2500
  branchName,
2384
2501
  sourcePath: configuredProjectBranchPath(projectsRoot, project, branchName),
@@ -2399,7 +2516,8 @@ async function startWorker(options) {
2399
2516
  // hidden ref only after that publication succeeds.
2400
2517
  gitDirectory: workspaceShadowRoot,
2401
2518
  mirrorUrl: connection.mirrorUrl,
2402
- mirrorAuth: connection.mirrorAuth,
2519
+ credentialHelper: connection.credentialHelper,
2520
+ credentialUsername: connection.mirrorCredentialUsername,
2403
2521
  sourcePath: branch.sourcePath,
2404
2522
  workspaceRelativePath: workspaceProjectRelativePath(project.projectId, branch.branchName),
2405
2523
  planSourcePath: branch.planSourcePath,
@@ -2457,14 +2575,17 @@ async function startWorker(options) {
2457
2575
  for (const deletion of projectWorkspaceState.pendingMirrorRefDeletions) {
2458
2576
  const key = projectBranchKey(deletion.projectId, deletion.branchName);
2459
2577
  const planSourcePath = import_node_path.default.join(planRoot, deletion.projectId, ...deletion.branchName.split("/"));
2578
+ const liveProject = projectConfigById.get(deletion.projectId);
2579
+ const connection = liveProject ? projectConnection(liveProject) : projectMirrorConnection(deletion.projectId);
2460
2580
  import_node_fs.default.rmSync(planSourcePath, { recursive: true, force: true });
2461
2581
  pendingMirrorDeletes.set(key, {
2462
2582
  id: projectMountId(deletion.projectId, deletion.branchName),
2463
2583
  projectId: deletion.projectId,
2464
2584
  branchName: deletion.branchName,
2465
2585
  gitDirectory: workspaceShadowRoot,
2466
- mirrorUrl: canonicalProjectRemoteUrl(baseUrl, deletion.projectId),
2467
- mirrorAuth: gitAuthForRemote(canonicalProjectRemoteUrl(baseUrl, deletion.projectId), bearerAuthHeader),
2586
+ mirrorUrl: connection.mirrorUrl,
2587
+ credentialHelper: connection.credentialHelper,
2588
+ credentialUsername: connection.mirrorCredentialUsername,
2468
2589
  sourcePath: deletion.managedPath,
2469
2590
  workspaceRelativePath: workspaceProjectRelativePath(deletion.projectId, deletion.branchName),
2470
2591
  planSourcePath,
@@ -2612,9 +2733,9 @@ async function startWorker(options) {
2612
2733
  primaryBranchName: primaryProjectBranch(project),
2613
2734
  branches: project.branches,
2614
2735
  originUrl: connection.originUrl,
2615
- originAuth: connection.originAuth,
2736
+ originCredentialUsername: connection.originCredentialUsername,
2616
2737
  mirrorUrl: connection.mirrorUrl,
2617
- mirrorAuth: connection.mirrorAuth,
2738
+ mirrorCredentialUsername: connection.mirrorCredentialUsername,
2618
2739
  credentialHelper: connection.credentialHelper,
2619
2740
  gitIdentity: workspaceGitIdentity
2620
2741
  });
@@ -2653,10 +2774,17 @@ async function startWorker(options) {
2653
2774
  if (!readyProjectIds.has(project.projectId) || project.branches.length === 0) continue;
2654
2775
  const connection = projectConnection(project);
2655
2776
  const primaryPath = configuredProjectBranchPath(projectsRoot, project, primaryProjectBranch(project));
2656
- if (!tryGit(["fetch", "--no-recurse-submodules", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*"], {
2657
- cwd: primaryPath,
2658
- auth: connection.originAuth
2659
- })) {
2777
+ if (!tryGit(
2778
+ [
2779
+ ...(0, import_git_process_environment.gitTransportSecurityArgs)(connection.originUrl, connection.credentialHelper, connection.originCredentialUsername),
2780
+ "fetch",
2781
+ "--no-recurse-submodules",
2782
+ "--prune",
2783
+ "origin",
2784
+ "+refs/heads/*:refs/remotes/origin/*"
2785
+ ],
2786
+ { cwd: primaryPath }
2787
+ )) {
2660
2788
  process.stderr.write(`[r5d-worker] could not refresh origin divergence for ${project.projectPath}
2661
2789
  `);
2662
2790
  }
@@ -2706,7 +2834,8 @@ async function startWorker(options) {
2706
2834
  projectRoot: configuredProjectRoot(projectsRoot, project),
2707
2835
  branchNames: project.branches.map(({ branchName }) => branchName),
2708
2836
  mirrorUrl: connection.mirrorUrl,
2709
- mirrorAuth: connection.mirrorAuth,
2837
+ credentialHelper: connection.credentialHelper,
2838
+ credentialUsername: connection.mirrorCredentialUsername,
2710
2839
  onlyBranches: changed
2711
2840
  });
2712
2841
  for (const result of results) {
@@ -2733,7 +2862,8 @@ async function startWorker(options) {
2733
2862
  gitDirectory: deletion.gitDirectory,
2734
2863
  branchName: deletion.branchName,
2735
2864
  mirrorUrl: deletion.mirrorUrl,
2736
- mirrorAuth: deletion.mirrorAuth
2865
+ credentialHelper: deletion.credentialHelper,
2866
+ credentialUsername: deletion.credentialUsername
2737
2867
  });
2738
2868
  }
2739
2869
  if (deletion.tombstoneId && !deletion.projectDeleted) {
@@ -2743,6 +2873,9 @@ async function startWorker(options) {
2743
2873
  });
2744
2874
  }
2745
2875
  pendingMirrorDeletes.delete(key);
2876
+ if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
2877
+ removeProjectCredentialStore(deletion.projectId);
2878
+ }
2746
2879
  lastObservedProjectHeads.delete(key);
2747
2880
  }
2748
2881
  };
@@ -2756,7 +2889,8 @@ async function startWorker(options) {
2756
2889
  primaryBranchName: primaryProjectBranch(project),
2757
2890
  branchNames: project.branches.map(({ branchName }) => branchName),
2758
2891
  mirrorUrl: connection.mirrorUrl,
2759
- mirrorAuth: connection.mirrorAuth
2892
+ credentialHelper: connection.credentialHelper,
2893
+ credentialUsername: connection.mirrorCredentialUsername
2760
2894
  });
2761
2895
  for (const state of refreshed) {
2762
2896
  lastObservedProjectHeads.set(
@@ -2781,9 +2915,9 @@ async function startWorker(options) {
2781
2915
  primaryBranchName: primaryProjectBranch(project),
2782
2916
  branches: project.branches,
2783
2917
  originUrl: connection.originUrl,
2784
- originAuth: connection.originAuth,
2918
+ originCredentialUsername: connection.originCredentialUsername,
2785
2919
  mirrorUrl: connection.mirrorUrl,
2786
- mirrorAuth: connection.mirrorAuth,
2920
+ mirrorCredentialUsername: connection.mirrorCredentialUsername,
2787
2921
  credentialHelper: connection.credentialHelper,
2788
2922
  gitIdentity: workspaceGitIdentity
2789
2923
  });
@@ -2830,16 +2964,16 @@ async function startWorker(options) {
2830
2964
  error: error instanceof Error ? error.message : String(error)
2831
2965
  });
2832
2966
  const performWorkspaceSync = async (input) => {
2833
- if (!workspaceRemoteUrl || !workspaceGitIdentity) throw new Error("Worker workspace configuration has not been received");
2967
+ if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
2968
+ throw new Error("Worker workspace configuration has not been received");
2969
+ }
2834
2970
  const mounts = buildWorkspaceMounts();
2835
- const remoteAuth = gitAuthForRemote(workspaceRemoteUrl, bearerAuthHeader);
2836
- const credentialHelper = credentialHelperForRemote(workspaceRemoteUrl, bearerAuthHeader);
2837
2971
  if (input.resetToCanonical) {
2838
2972
  const reset = (0, import_workspace_git_sync.resetWorkspaceGit)({
2839
2973
  workspacePath: workspaceShadowRoot,
2840
2974
  remoteUrl: workspaceRemoteUrl,
2841
- remoteAuth,
2842
- credentialHelper,
2975
+ credentialHelper: workspaceCredentialHelper,
2976
+ credentialUsername: workspaceCredentialUsername,
2843
2977
  gitIdentity: workspaceGitIdentity,
2844
2978
  mounts
2845
2979
  });
@@ -2868,8 +3002,8 @@ async function startWorker(options) {
2868
3002
  workerLabel: label,
2869
3003
  workspacePath: workspaceShadowRoot,
2870
3004
  remoteUrl: workspaceRemoteUrl,
2871
- remoteAuth,
2872
- credentialHelper,
3005
+ credentialHelper: workspaceCredentialHelper,
3006
+ credentialUsername: workspaceCredentialUsername,
2873
3007
  gitIdentity: workspaceGitIdentity,
2874
3008
  mounts,
2875
3009
  commitDetail: input.confirmationReason ?? input.trigger.detail,
@@ -2902,6 +3036,12 @@ async function startWorker(options) {
2902
3036
  const runWorkspaceSync = async (input) => {
2903
3037
  const attemptId = input.attemptId ?? crypto.randomUUID();
2904
3038
  const requestedAt = Date.now();
3039
+ if (!input.requestId && input.sendResult !== false) {
3040
+ if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
3041
+ throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
3042
+ }
3043
+ ws.send(JSON.stringify({ type: "workspace_sync_started", attemptId }));
3044
+ }
2905
3045
  let queueEnteredAt = requestedAt;
2906
3046
  let syncStartedAt = requestedAt;
2907
3047
  workspaceSyncRequestsInFlight += 1;
@@ -2955,6 +3095,12 @@ async function startWorker(options) {
2955
3095
  if (result.outcome === "failed") {
2956
3096
  pendingAutomaticTrigger = scheduledTrigger;
2957
3097
  }
3098
+ }).catch((error) => {
3099
+ pendingAutomaticTrigger = scheduledTrigger;
3100
+ process.stderr.write(
3101
+ `[r5d-worker] failed to start automatic workspace sync: ${error instanceof Error ? error.message : String(error)}
3102
+ `
3103
+ );
2958
3104
  }).finally(() => {
2959
3105
  automaticSyncInFlight = false;
2960
3106
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
@@ -2982,13 +3128,47 @@ async function startWorker(options) {
2982
3128
  try {
2983
3129
  return await workspaceSyncSingleFlight.runExclusive(async () => {
2984
3130
  workspaceConfigured = false;
3131
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
3132
+ desiredProjects: message.projects
3133
+ });
3134
+ const requiredCredentialStorePaths = /* @__PURE__ */ new Set();
3135
+ const workspaceStorePath = credentialStorePathForWorkspace(message.workspaceRemoteUrl);
3136
+ requiredCredentialStorePaths.add(workspaceStorePath);
3137
+ const nextWorkspaceCredentialHelper = credentialHelperForRemotes(
3138
+ [{ remoteUrl: message.workspaceRemoteUrl, authHeader: bearerAuthHeader }],
3139
+ workspaceStorePath
3140
+ );
3141
+ if (!nextWorkspaceCredentialHelper) throw new Error("Workspace credentials are unavailable");
3142
+ const desiredProjectConfigById = new Map(message.projects.map((project) => [project.projectId, project]));
3143
+ for (const project of message.projects) {
3144
+ requiredCredentialStorePaths.add(credentialStorePathForProject(project.projectId));
3145
+ projectConnection(project);
3146
+ }
3147
+ const pendingCredentialProjectIds = /* @__PURE__ */ new Set([
3148
+ ...[...pendingMirrorDeletes.values()].map(({ projectId }) => projectId),
3149
+ ...projectWorkspaceState.pendingTreeDeletions.map(({ projectId }) => projectId),
3150
+ ...projectWorkspaceState.pendingMirrorRefDeletions.map(({ projectId }) => projectId)
3151
+ ]);
3152
+ for (const projectId of pendingCredentialProjectIds) {
3153
+ const desiredProject = desiredProjectConfigById.get(projectId);
3154
+ if (desiredProject) projectConnection(desiredProject);
3155
+ else projectMirrorConnection(projectId);
3156
+ requiredCredentialStorePaths.add(credentialStorePathForProject(projectId));
3157
+ }
3158
+ for (const deletion of pendingMirrorDeletes.values()) {
3159
+ const desiredProject = desiredProjectConfigById.get(deletion.projectId);
3160
+ const connection = desiredProject ? projectConnection(desiredProject) : projectMirrorConnection(deletion.projectId);
3161
+ deletion.mirrorUrl = connection.mirrorUrl;
3162
+ deletion.credentialHelper = connection.credentialHelper;
3163
+ deletion.credentialUsername = connection.mirrorCredentialUsername;
3164
+ }
3165
+ pruneCredentialStoreFiles(requiredCredentialStorePaths);
2985
3166
  configureGitHubAuth(message.githubCredential);
2986
3167
  visibleGitIdentity = message.gitIdentity;
2987
3168
  workspaceGitIdentity = message.gitIdentity;
2988
3169
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2989
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
2990
- desiredProjects: message.projects
2991
- });
3170
+ workspaceCredentialHelper = nextWorkspaceCredentialHelper;
3171
+ workspaceCredentialUsername = workerCredentialUsername;
2992
3172
  (0, import_project_workspace_state.pruneAuthoritativelyDesiredBranchDeletions)(pendingMirrorDeletes, message.projects);
2993
3173
  const effectiveProjects = effectiveWorkerProjects(message.projects).map((project) => ({
2994
3174
  ...project,
@@ -3019,13 +3199,11 @@ async function startWorker(options) {
3019
3199
  for (const projectId of [...readyProjectIds]) {
3020
3200
  if (!projectConfigById.has(projectId)) readyProjectIds.delete(projectId);
3021
3201
  }
3022
- const remoteAuth = gitAuthForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
3023
- const credentialHelper = credentialHelperForRemote(message.workspaceRemoteUrl, bearerAuthHeader);
3024
3202
  (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
3025
3203
  workspacePath: workspaceShadowRoot,
3026
3204
  remoteUrl: message.workspaceRemoteUrl,
3027
- remoteAuth,
3028
- credentialHelper,
3205
+ credentialHelper: nextWorkspaceCredentialHelper,
3206
+ credentialUsername: workerCredentialUsername,
3029
3207
  gitIdentity: message.gitIdentity
3030
3208
  });
3031
3209
  pruneStaleOuterWorkspaceEntries(projectWorkspaceStateStore.discoverStaleOuterEntries(workspaceShadowRoot));
@@ -3825,5 +4003,6 @@ if (isCliEntrypoint()) {
3825
4003
  resolveWorkerFilePath,
3826
4004
  resolveWorkerSessionTarget,
3827
4005
  syncSessionArtifacts,
4006
+ workerGitSecurityTestHarness,
3828
4007
  writeWorkerTextFile
3829
4008
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.76",
3
+ "version": "0.0.78",
4
4
  "type": "commonjs"
5
5
  }