CB-592: shadow the admin GITEA_ACCESS_TOKEN in every member's herdr overlay
herdr spawns a pane from its own login-shell process env and layers our map on top, so any key baseEnv never mentions passes straight through — including the admin forge token. baseEnv now puts a non-blank sentinel for GITEA_ACCESS_TOKEN, applied after the profile's own env: so no profile can restore it. One place, every adapter, every profile including future ones. CB-302's GITEA_TOKEN grant (applyGitToken) is untouched.
This commit is contained in:
@@ -766,10 +766,28 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
/** A fresh mutable env map — the conventional starting point for {@link #buildLaunch}. */
|
||||
/**
|
||||
* CB-592: overlay value that shadows the admin {@code GITEA_ACCESS_TOKEN} a herdr pane
|
||||
* otherwise inherits from herdr's own login-shell process environment (gitea issue #77).
|
||||
* herdr spawns a pane from its <em>own</em> process environment and layers our map on top —
|
||||
* {@link dev.ltms.bridged.herdr.WorkspaceControl#createTab} and {@code #splitPane} send only
|
||||
* the keys we put in that map, so any key we never mention passes straight through from
|
||||
* herdr's own shell, admin token included.
|
||||
*
|
||||
* <p>Deliberately a non-blank sentinel, not {@code ""}. Whether an empty-string overlay value
|
||||
* overrides an inherited variable or is skipped as blank could not be settled by reading this
|
||||
* codebase — herdr's server-side merge is an external process, not something in this repo.
|
||||
* A non-blank replacement sidesteps that ambiguity entirely: {@link #baseEnv}'s own {@code
|
||||
* PATH} seeding already depends on the overlay reliably replacing an inherited value (see its
|
||||
* javadoc), and that is only demonstrated for a non-blank value, so this reuses the same,
|
||||
* proven-reliable shape rather than the unverified one.
|
||||
*/
|
||||
private static final String BLOCKED_GITEA_ACCESS_TOKEN =
|
||||
"blocked-by-bridged-cb592-see-gitea-issue-77";
|
||||
|
||||
/**
|
||||
* Seed a worker's environment (CB-511): the daemon's own {@code PATH}, then the profile's
|
||||
* {@code env:} entries.
|
||||
* {@code env:} entries, then the CB-592 admin-token shadow.
|
||||
*
|
||||
* <p>Why this exists: bridged passes herdr an explicit env map, and herdr merges it into
|
||||
* <em>its own</em> process environment. So before this, a worker inherited whatever PATH the
|
||||
@@ -783,6 +801,11 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
|
||||
* overriding {@code ANTHROPIC_BASE_URL} and slipping past {@link
|
||||
* dev.ltms.bridged.guard.SubscriptionGuard}, which is checked against the profile's
|
||||
* {@code baseUrl} and nothing else.
|
||||
*
|
||||
* <p>The CB-592 shadow is put in <em>last</em>, after the profile's own {@code env:}, so no
|
||||
* profile — present or future — can restore the admin token by naming it in config. This is
|
||||
* the one place the shadow is applied: every {@code buildLaunch} in every adapter calls this
|
||||
* first, so a new profile, and a peer kind not yet written, gets it for free.
|
||||
*/
|
||||
protected Map<String, String> baseEnv(BridgedConfig.Profile cfg) {
|
||||
Map<String, String> workerEnv = new LinkedHashMap<>();
|
||||
@@ -793,6 +816,7 @@ public abstract class HerdrPeerLauncher implements PeerLauncher {
|
||||
if (cfg != null && cfg.env() != null) {
|
||||
workerEnv.putAll(cfg.env());
|
||||
}
|
||||
workerEnv.put("GITEA_ACCESS_TOKEN", BLOCKED_GITEA_ACCESS_TOKEN);
|
||||
return workerEnv;
|
||||
}
|
||||
|
||||
|
||||
@@ -676,6 +676,46 @@ class ClaudeCodeLauncherTest {
|
||||
"the guard-checked baseUrl must win over any env: entry, or the boundary is bypassable");
|
||||
}
|
||||
|
||||
// --- CB-592: the admin GITEA_ACCESS_TOKEN never reaches a member -----------------------------
|
||||
|
||||
/**
|
||||
* herdr's env map is an overlay onto its own (login-shell) process environment, so a worker
|
||||
* inherits whatever the daemon's shell carries — including the admin GITEA_ACCESS_TOKEN — for
|
||||
* every key baseEnv does not explicitly shadow. This pins that the launcher DOES send an
|
||||
* explicit (non-blank) GITEA_ACCESS_TOKEN to herdr on every spawn, whatever the profile is, so
|
||||
* a future baseEnv refactor cannot silently drop it and reopen the leak. Asserted against what
|
||||
* tab.create's params actually carry, not an internal map built in the test (gitea #77).
|
||||
*/
|
||||
@Test
|
||||
void everySpawnShadowsTheAdminGiteaAccessToken() {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
service(herdr, List.of("claude"), null).spawn();
|
||||
|
||||
String shadowed = startEnv(herdr).get("GITEA_ACCESS_TOKEN");
|
||||
assertNotNull(shadowed, "GITEA_ACCESS_TOKEN must be explicitly overlaid, not left unmentioned");
|
||||
assertFalse(shadowed.isBlank(), "a blank overlay value's override behaviour is unverified — must be non-blank");
|
||||
}
|
||||
|
||||
/**
|
||||
* No profile — present or future — may restore the admin token by naming it in {@code env:}.
|
||||
* The shadow is applied after the profile's own env in {@link HerdrPeerLauncher#baseEnv}
|
||||
* precisely so this can never happen; this test pins that ordering.
|
||||
*/
|
||||
@Test
|
||||
void aProfileEnvEntryCannotRestoreTheAdminGiteaAccessToken() {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
BridgedConfig.Profile cfg = new BridgedConfig.Profile(
|
||||
"ltms-local", "http://gx00.gw:8000", "coder", null, "BRIDGED_WORKER_TOKEN",
|
||||
List.of("claude"), "tab", "bridged-workers", "w #{n}", null, null, null, null, null,
|
||||
null, Map.of("GITEA_ACCESS_TOKEN", "admin-secret-from-profile-config"), null, null);
|
||||
new ClaudeCodeLauncher(new AgentControl(herdr), new WorkspaceControl(herdr),
|
||||
new SubscriptionGuard(Set.of("gx00.gw")), Map.of(cfg.profile(), cfg), cfg.profile(),
|
||||
_ -> null).spawn();
|
||||
|
||||
assertNotEquals("admin-secret-from-profile-config", startEnv(herdr).get("GITEA_ACCESS_TOKEN"),
|
||||
"a profile's own env: must not be able to smuggle the admin token back in");
|
||||
}
|
||||
|
||||
// ── CB-533: the model is pinned on the command line, not only in the environment ────────────
|
||||
|
||||
/** A launcher for a profile identical but for its {@code model:} — the only variable here. */
|
||||
|
||||
@@ -208,6 +208,21 @@ class OpenCodeLauncherTest {
|
||||
"a git-token profile gets the peer-neutral GITEA_TOKEN grant, same as Claude");
|
||||
}
|
||||
|
||||
/**
|
||||
* CB-592: the shadow lives in {@link HerdrPeerLauncher#baseEnv}, shared by every adapter — this
|
||||
* pins that the opencode path gets it too, not just Claude's. See the matching test in
|
||||
* {@code ClaudeCodeLauncherTest} for the full rationale (gitea #77).
|
||||
*/
|
||||
@Test
|
||||
void everySpawnShadowsTheAdminGiteaAccessToken(@TempDir Path root) {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
service(herdr, root, opencodeCfg(null, null, null)).spawn();
|
||||
|
||||
String shadowed = startEnv(herdr).get("GITEA_ACCESS_TOKEN");
|
||||
assertNotNull(shadowed, "GITEA_ACCESS_TOKEN must be explicitly overlaid, not left unmentioned");
|
||||
assertFalse(shadowed.isBlank(), "a blank overlay value's override behaviour is unverified — must be non-blank");
|
||||
}
|
||||
|
||||
@Test
|
||||
void capabilitiesDeclareOrphanReapAndMcpAskAndConditionalSelfPr(@TempDir Path root) {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
|
||||
Reference in New Issue
Block a user