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:
@@ -8,7 +8,10 @@ import {
|
||||
type RunnerMessage,
|
||||
type RunnerStage,
|
||||
} from "@repo/core-runner-protocol";
|
||||
import { runCommand } from "./exec";
|
||||
import type { Logger } from "./log";
|
||||
import { cloneRepository } from "./stages/clone";
|
||||
import { runStage, type StageEmitter } from "./stages/stage-runner";
|
||||
|
||||
/**
|
||||
* The runner's WS protocol server (ADR-027, protocol version "0").
|
||||
@@ -137,38 +140,59 @@ export type CommandMessage = Extract<
|
||||
{ type: "clone" | "install" | "scan" | "adapter-start" | "render-frame" }
|
||||
>;
|
||||
|
||||
export interface StageEmitter {
|
||||
status: (stage: RunnerStage, elapsedMs: number) => void;
|
||||
error: (
|
||||
cause: RunnerErrorCause,
|
||||
message: string,
|
||||
stage?: RunnerStage,
|
||||
) => void;
|
||||
}
|
||||
|
||||
/** Stages that later walking-skeleton stories wire up (05+). */
|
||||
const NOT_IMPLEMENTED: Record<
|
||||
CommandMessage["type"],
|
||||
Exclude<CommandMessage["type"], "clone">,
|
||||
{ cause: RunnerErrorCause; stage?: RunnerStage }
|
||||
> = {
|
||||
clone: { cause: "clone-failed", stage: "cloning" },
|
||||
install: { cause: "install-failed", stage: "installing" },
|
||||
scan: { cause: "scan-failed", stage: "scanning" },
|
||||
"adapter-start": { cause: "adapter-start-failed", stage: "starting-preview" },
|
||||
"render-frame": { cause: "render-failed" },
|
||||
};
|
||||
|
||||
interface CommandContext {
|
||||
emit: StageEmitter;
|
||||
/** Signal successful command completion — the session's idle marker. */
|
||||
ready: () => void;
|
||||
options: RunnerServerOptions;
|
||||
}
|
||||
|
||||
async function handleCommand(
|
||||
command: CommandMessage,
|
||||
emit: StageEmitter,
|
||||
_options: RunnerServerOptions,
|
||||
ctx: CommandContext,
|
||||
): Promise<void> {
|
||||
const notImplemented = NOT_IMPLEMENTED[command.type];
|
||||
emit.error(
|
||||
notImplemented.cause,
|
||||
`the "${command.type}" command is not implemented by this runner build yet`,
|
||||
notImplemented.stage,
|
||||
);
|
||||
const { emit, options } = ctx;
|
||||
const stageOptions = {
|
||||
emit,
|
||||
heartbeatMs: options.heartbeatMs,
|
||||
log: options.log,
|
||||
};
|
||||
switch (command.type) {
|
||||
case "clone": {
|
||||
const result = await runStage(
|
||||
"cloning",
|
||||
"clone-failed",
|
||||
stageOptions,
|
||||
() =>
|
||||
cloneRepository({
|
||||
workspaceDir: options.workspaceDir,
|
||||
runCommand,
|
||||
log: options.log,
|
||||
})(command),
|
||||
);
|
||||
if (result.ok) ctx.ready();
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
const notImplemented = NOT_IMPLEMENTED[command.type];
|
||||
emit.error(
|
||||
notImplemented.cause,
|
||||
`the "${command.type}" command is not implemented by this runner build yet`,
|
||||
notImplemented.stage,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rawToString(data: RawData): string {
|
||||
@@ -257,7 +281,11 @@ export async function startRunnerServer(
|
||||
log.info("command-received", { command: command.type });
|
||||
commandChain = commandChain.then(async () => {
|
||||
try {
|
||||
await handleCommand(command, emit, options);
|
||||
await handleCommand(command, {
|
||||
emit,
|
||||
ready: () => send({ type: "ready" }),
|
||||
options,
|
||||
});
|
||||
} catch (error) {
|
||||
// Stages map their own failures; this is the last-resort net.
|
||||
log.error("command-crashed", {
|
||||
|
||||
Reference in New Issue
Block a user