mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +02:00
fix(deepseek): review-driven hardening across the harness integration
Fifteen review findings on the dsh mode, the serious ones first: - Multi-user: DEEPSEEK_BASE_URL joins the owner-clamped env keys. _configureDeepSeek() forwards the SERVER's own DEEPSEEK_API_KEY into every dsh pane and applyEnvOverrides() lands after it, so a non-granted owner who could redirect the base URL would have the operator's key sent as a bearer credential to a host of their choosing. - Wait registry: until=stop/blocked is refused on docker and remote-SSH dsh sessions (new deepSeekBridgeUnreachable fact in sessionHookOptions). The HERDR triple is set via LOCAL tmux setenv, which crosses neither docker exec nor ssh, so such a session can never post a hook event and the wait burned its whole timeout on every turn. - Approvals: a dsh item is an ALERT, not an answerable card. The answer route refuses (the '1'/Esc keystrokes are Claude-dialog-shaped and the option parser cannot read a third-party TUI's frames, so an answer was a blind keystroke into a foreign composer), and the push notification carries no Approve/Deny actions for dsh sessions. - Status shim (v3): --seq is forwarded and the server drops stale retried reports inside a 60s window (the TUI retries with backoff, so a retried 'working' could land after 'blocked' and resolve an approval whose dialog was still on screen); 4xx responses exit 0 instead of retrying, so one misconfigured session cannot feed the auth rate-limit bucket until the hook endpoint 429s for the whole instance. - Web-UI server: concurrent starts are serialized through a lock (two racing POSTs used to pick the same port and orphan the winner), and the readiness poll / timeout paths only clear or stop the singleton while it is still theirs. First click actually opens the tab now (refreshWebviews, not the nonexistent loadWebviews). DELETE /api/deepseek/web requires the privileged grant in multi-user mode. - Cron: deepseek jobs run the same two-part launch gate as the HTTP create paths (impl moved into the resolver so all three share it) and no longer stamp a Claude default model on the session. - Parity sweeps: quick-start's docker branch rejects deepSeekConfig like the remote branch; the Ralph auto-enable list gained deepseek; HookEventType gained agent_working; the phone overview run menu filters managed webview records like the desktop menu. - install.sh: the dsh identity probe closes stdin (under curl|bash a child that reads stdin eats the rest of the script), bounds the exec with timeout where available, and is memoized to one scan per install. - Welcome screen: .welcome-btn-deepseek styled in the #4d6bfe brand identity (it rendered as an unstyled UA-grey button); stale markup comment about the web shortcut rewritten; clamp docs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -157,16 +157,37 @@ export async function stopDeepSeekWeb(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
type StartResult = { ok: true; port: number; url: string; reused: boolean } | { ok: false; error: string };
|
||||
|
||||
/**
|
||||
* Serializes concurrent starts. Two POSTs racing (two devices, or a double
|
||||
* click while the first boots) used to both see `current === null`, pick the
|
||||
* SAME free port, and spawn twice: the loser died on EADDRINUSE while its exit
|
||||
* handler nulled the singleton out from under the winner, leaving a live
|
||||
* `dsh web` nothing tracked or killed — the exact orphan this module exists to
|
||||
* prevent. The second caller now simply waits and reuses the first's server.
|
||||
*/
|
||||
let startLock: Promise<unknown> = Promise.resolve();
|
||||
|
||||
/**
|
||||
* Start (or reuse) the background `dsh web` for `authority`.
|
||||
*
|
||||
* @param dshDir directory holding the resolved `dsh` binary.
|
||||
* @param authority browser authority to pass as `--trusted-host`.
|
||||
*/
|
||||
export async function startDeepSeekWeb(
|
||||
dshDir: string,
|
||||
authority: string
|
||||
): Promise<{ ok: true; port: number; url: string; reused: boolean } | { ok: false; error: string }> {
|
||||
export function startDeepSeekWeb(dshDir: string, authority: string): Promise<StartResult> {
|
||||
const run = startLock.then(
|
||||
() => startDeepSeekWebLocked(dshDir, authority),
|
||||
() => startDeepSeekWebLocked(dshDir, authority)
|
||||
);
|
||||
startLock = run.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
);
|
||||
return run;
|
||||
}
|
||||
|
||||
async function startDeepSeekWebLocked(dshDir: string, authority: string): Promise<StartResult> {
|
||||
// Reuse only when the running server is BOTH healthy and fenced for the
|
||||
// authority now asking. A server trusting the other origin renders a page
|
||||
// whose every API call 403s, which looks like a broken dashboard rather than
|
||||
@@ -226,7 +247,10 @@ export async function startDeepSeekWeb(
|
||||
const deadline = Date.now() + READY_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
if (exited) {
|
||||
current = null;
|
||||
// Guarded like the exit/error handlers: a concurrent stop (DELETE route,
|
||||
// shutdown) may already have cleared or replaced the singleton, and an
|
||||
// unconditional null here would drop a server this call does not own.
|
||||
if (current === running) current = null;
|
||||
const tail = output.trim().slice(-800);
|
||||
return { ok: false, error: tail ? `dsh web exited during startup: ${tail}` : 'dsh web exited during startup' };
|
||||
}
|
||||
@@ -236,7 +260,14 @@ export async function startDeepSeekWeb(
|
||||
await new Promise((r) => setTimeout(r, READY_POLL_MS));
|
||||
}
|
||||
|
||||
await stopDeepSeekWeb();
|
||||
// Timeout: kill OUR child. Only route through stopDeepSeekWeb() while the
|
||||
// singleton is still ours — signalling `current` unconditionally here could
|
||||
// SIGTERM a healthy server a concurrent actor now owns.
|
||||
if (current === running) {
|
||||
await stopDeepSeekWeb();
|
||||
} else {
|
||||
killTree(running.child, 'SIGKILL');
|
||||
}
|
||||
const tail = output.trim().slice(-800);
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user