feat(runner): clone stage with ephemeral credential helper

Clone stage per tech spec §6, verbatim mechanics: every git invocation
carries a BLANK credential.helper first (suppresses OS keychain
helpers) then the inline veect helper; the PAT reaches git via child
env only — never argv, URLs, logs, or .git/config. GIT_TERMINAL_PROMPT
and GIT_ASKPASS are pinned so a headless clone can never hang on a TTY
or ambient IDE askpass. Staged status events (start/heartbeat/final) +
ready on success; named failures: invalid-git-url, auth-failed,
clone-failed (daemon's 'repository not exported' maps to bad-URL, not
auth). Integration suite clones the daemon-served vite-kitchen and an
authenticated dumb-HTTP remote that asserts the exact Basic credential
git presented, plus leak assertions over logs/argv/.git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 22:38:17 +02:00
parent b090e26701
commit 750ab44379
12 changed files with 1261 additions and 20 deletions

View File

@@ -115,6 +115,32 @@ export async function handshake(
return client.waitFor((m) => m.type === "ready", "ready handshake reply");
}
/**
* Send a command and resolve on the runner's NEXT `ready` — the
* protocol's success marker for a completed command. Counting readies
* (instead of matching any `ready`) keeps this correct on sessions that
* already completed earlier commands.
*/
export async function sendAndAwaitReady(
client: ProtocolClient,
message: RunnerMessage,
description: string,
timeoutMs?: number,
): Promise<void> {
const readiesBefore = client
.received()
.filter((m) => m.type === "ready").length;
client.send(message);
await client.waitFor(
(m) =>
m.type === "ready" &&
client.received().filter((r) => r.type === "ready").length >
readiesBefore,
description,
timeoutMs,
);
}
/** Wait for the next `error` event and assert its named cause. */
export async function expectNamedError(
client: ProtocolClient,