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:
Codeman maintainer
2026-08-25 19:01:29 +02:00
parent c30dfaf0e7
commit a628737d1f
18 changed files with 311 additions and 74 deletions
+2 -1
View File
@@ -657,7 +657,8 @@
</button>
<!-- The DeepSeek Harness browser UI is the vendor's OWN interactive
surface (the terminal one is third-party), so it gets a shortcut:
this starts `dsh web` in a shell session and opens it as a tab.
POST /api/deepseek/web starts a background `dsh web` fenced to
this origin, and the URL opens as a managed web tab.
Shown only when dsh is installed. -->
<button class="run-mode-option run-mode-option--web" id="runModeDeepSeekWeb" style="display: none;" onclick="app.runDeepSeekWeb()">
<span class="run-mode-dot deepseek"></span>DeepSeek web UI&hellip;
+5
View File
@@ -581,6 +581,11 @@ Object.assign(CodemanApp.prototype, {
menu.appendChild(header);
for (const webview of this.webviews ? this.webviews.values() : []) {
// Managed records are Codeman-owned shortcut state (the DeepSeek web UI
// writes one), not saved dashboards: same filter as the desktop run menu,
// or the phone picker lists a stale 127.0.0.1:<port> row that dies on the
// next server restart with no affordance here to restart it.
if (webview.managed) continue;
const option = document.createElement('button');
option.type = 'button';
option.className = 'mobile-overview-run-option';
+5 -1
View File
@@ -577,7 +577,11 @@ Object.assign(CodemanApp.prototype, {
if (!wvData.success) throw new Error(wvData.error || 'Failed to save the web tab');
webview = wvData.data.webview || wvData.data;
}
await this.loadWebviews?.();
// refreshWebviews, not a hopeful optional-chain: openWebview() reads
// this.webviews and silently no-ops on an id it has not loaded, so
// skipping the refresh made the FIRST click create the record but open
// nothing (the SSE round-trip had not landed yet).
await this.refreshWebviews?.();
this._appendSessionLaunchStatus(ownsLaunchTerminal, `Serving on ${url} - opening it as a tab.`);
if (webview?.id) await this.openWebview(webview.id);
+18
View File
@@ -3892,6 +3892,24 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
transform: translateY(-1px);
}
/* DeepSeek Harness: the #4d6bfe blue identity, matching
.btn-toolbar.btn-run.mode-deepseek and .run-mode-dot.deepseek so the welcome
action reads as the same backend. */
.welcome-btn-deepseek {
background: linear-gradient(135deg, #101a4d 0%, #2740c4 55%, #4d6bfe 100%);
border-color: rgba(124, 147, 255, 0.4);
color: #eef2ff;
box-shadow: 0 2px 8px rgba(77, 107, 254, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.welcome-btn-deepseek:hover {
background: linear-gradient(135deg, #16225f 0%, #3350e6 55%, #6b83ff 100%);
box-shadow: 0 4px 20px rgba(77, 107, 254, 0.3), 0 0 40px rgba(39, 64, 196, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.08);
border-color: rgba(150, 170, 255, 0.5);
color: #f8faff;
transform: translateY(-1px);
}
.welcome-btn-gemini {
background: linear-gradient(135deg, #10243f 0%, #174ea6 55%, #4f46e5 100%);
border-color: rgba(96, 165, 250, 0.4);