diff --git a/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java b/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java index 1a43c2f..bcf7c6d 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java +++ b/fleetd/src/main/java/dev/ltms/fleet/session/GitWorktrees.java @@ -126,6 +126,7 @@ public final class GitWorktrees implements Worktrees { afterWorktreeAdded.accept(wt); requireCredentialFreeHttpsOrigin(wt); configureEnvironmentCredentialHelper(repoRoot, wt); + configureHttpsUrlRewriteForSshOrigin(repoRoot, wt); isolateToolSurface(wt); return wt; } @@ -186,6 +187,63 @@ public final class GitWorktrees implements Worktrees { ENVIRONMENT_CREDENTIAL_HELPER); } + /** + * {@link #configureEnvironmentCredentialHelper} only ever fires for an HTTPS origin — Git never + * consults a {@code credential.helper} for an SSH transport. This repo's own origin is + * {@code ssh://git@git.ltms.dev:2224/fleet/fleetd.git}, and once {@code memberCredentials.policy: + * allow-list} blocks {@code SSH_AUTH_SOCK} (fleetd #157), a member sitting on an SSH origin has no + * way to authenticate at all — there is no private key file on this host, only an ssh-agent socket. + * + *
The fix is a worktree-scoped URL rewrite: {@code url. The host (and, for the rewrite's SSH-side match, the port) come from parsing the origin
+ * itself — never a hardcoded forge host, which is exactly what #177 removed. An origin that is
+ * already {@code https://} is left alone; the credential helper already covers it. An origin
+ * that is neither {@code ssh://} nor {@code https://} — including the scp-like shorthand
+ * ({@code git@host:path}, no scheme) — is left untouched deliberately: that shorthand's
+ * {@code host:path} split is defined by the user's ssh_config aliases, not by URI syntax, so
+ * guessing at it risks rewriting to the wrong place. A repo provisioned from that form keeps
+ * today's (broken, if the policy blocks the agent) SSH-only behaviour rather than a wrong rewrite.
+ */
+ private void configureHttpsUrlRewriteForSshOrigin(String repoRoot, String worktreePath) {
+ if (exitCode("git", "-C", repoRoot, "config", "--get", "remote.origin.url") != 0) {
+ return;
+ }
+ String origin = exec("git", "-C", repoRoot, "config", "--get", "remote.origin.url").trim();
+ URI uri;
+ try {
+ uri = new URI(origin);
+ } catch (URISyntaxException e) {
+ log.warn("origin URL {} is not a valid URI; skipping worktree HTTPS rewrite", origin);
+ return;
+ }
+ String scheme = uri.getScheme();
+ if (!"ssh".equalsIgnoreCase(scheme)) {
+ // Already https:// (the credential helper covers it), or a scheme-less/scp-like origin
+ // left alone on purpose — see the javadoc above.
+ log.debug("origin scheme is not ssh ({}) — no worktree HTTPS rewrite needed", origin);
+ return;
+ }
+ String host = uri.getHost();
+ String authority = uri.getRawAuthority();
+ if (host == null || host.isBlank() || authority == null || authority.isBlank()) {
+ log.warn("ssh origin {} has no resolvable host; skipping worktree HTTPS rewrite", origin);
+ return;
+ }
+ String sshBase = "ssh://" + authority + "/";
+ String httpsBase = "https://" + host + "/";
+ exec("git", "-C", worktreePath, "config", "--worktree", "--replace-all",
+ "url." + httpsBase + ".insteadOf", sshBase);
+ log.info("worktree {} rewrites {} to {} (worktree-scoped; parent checkout untouched)",
+ worktreePath, sshBase, httpsBase);
+ }
+
/**
* Neutralize the worktree's worktree-hostile project configs so a worker inherits only the tools
* and environment its launcher mounts (the bridge via {@code --mcp-config}, the opencode config
diff --git a/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java b/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java
index abb8069..1ddce93 100644
--- a/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java
+++ b/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java
@@ -269,6 +269,71 @@ class GitWorktreesTest {
assertFalse(credential.contains("operator-secret"), "Git used the inherited global helper");
}
+ /**
+ * fleetd #157 follow-up. An SSH origin never consults {@code credential.helper} — the environment
+ * credential helper set by {@link GitWorktrees#add} is therefore useless when a member's origin is
+ * SSH, which is exactly this repo's shape. The worktree must instead get a worktree-scoped
+ * {@code url.