mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
feat(deepseek): add DeepSeek Harness (dsh) as a ninth CLI run mode
Adds `mode: 'deepseek'` alongside claude/shell/opencode/codex/gemini/ antigravity/pi/grok, plus a shortcut that opens the harness's own browser UI as a Codeman web tab. DeepSeek is wired unlike its siblings in three ways, each of which is the reason for a design decision rather than an accident: 1. The agent is a PROFILE, not the binary. `dsh` is a launcher over $DSH_HOME/profiles/<name>, and DeepSeek ships only `web`, `headless` and `base` -- the interactive terminal front door is always a third-party plugin. So availability is two questions: `isDeepSeekAvailable()` (binary) and `isDeepSeekRunnable()` (binary AND a pane-capable profile). The Run button gates on the latter, because reporting only the binary would spawn a pane that dies on arrival. When the binary is present but no profile is, the run menu offers to install one (POST /api/deepseek/install-profile). 2. The permission switch is an env var, not a flag. The harness has no command-line permission option; its sandbox/approval rows read DSH_PERMISSION_MODE (read-only / workspace-write / danger-full-access). Exported via `tmux setenv`, never on the spawn line. Absent = the harness's own workspace-write, which still asks, so the multi-user clamp is the only-if-sent branch and clamps to workspace-write, never read-only. 3. It is the only non-claude mode that passes hooksAvailableForMode(), and it earned that. The terminal front door reports idle/working/blocked to a supervising process over a generic env-gated contract; a generated shim (deepseek-status-shim.ts) makes Codeman that supervisor and forwards each report to /api/hook-event as stop / agent_working / permission_prompt. So a dsh session gets definitive respawn triggers, real wait-endpoint signals and real Approvals Inbox items instead of output-stabilization guesswork. `agent_working` is new (157th SSE constant) and joins APPROVAL_RESOLVING_EVENTS so a dialog answered in the terminal clears its alert at once. The resolver needs the strictest identity probe of the family: `dsh` is not merely a squattable npm name, Debian ships an unrelated `dsh` (dancer's shell), so `dsh --help` must print the harness's own banner before a candidate is handed a spawn line. Model is deliberately not a session field -- it is a composition entry in the profile's config tree. Env allowlist gains DSH_* and DEEPSEEK_* only; provider keys named by a settings-file `apiKeyEnv` stay out, which is pi's 34-provider-key problem in a new shape. Verified live against dsh 0.1.1-rc.2 and @deepseek-harness-tui/dsh-tui: the status endpoint's two-part answer, the no-profile refusal, the profile bootstrap, a real session whose pane runs `dsh --profile dsh-tui` with the permission mode injected via setenv, and the full status bridge -- a send-and-wait returned signal "stop" from a real turn, and blocked/working created and cleared an Approvals Inbox item. Docs: docs/deepseek-integration.md (guide), docs/deepseek-integration-plan.md (decisions + honest gaps). Tests: test/deepseek-mode.test.ts, test/deepseek-cli-resolver.test.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+71
-4
@@ -8,7 +8,7 @@
|
||||
* - SessionConfig — creation-time config (id, workingDir, createdAt)
|
||||
* - SessionOutput — captured stdout/stderr/exitCode
|
||||
* - SessionStatus — 'idle' | 'busy' | 'stopped' | 'error'
|
||||
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' (which CLI backend)
|
||||
* - SessionMode — 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' (which CLI backend)
|
||||
* - ClaudeMode — CLI permission mode ('dangerously-skip-permissions' | 'auto' | 'normal' | 'allowedTools')
|
||||
* - SessionColor — visual differentiation color
|
||||
* - OpenCodeConfig — OpenCode-specific settings (model, autoAllowTools, continueSession)
|
||||
@@ -17,6 +17,7 @@
|
||||
* - AntigravityConfig — Antigravity CLI (agy) settings (model, dangerouslySkipPermissions, resumeConversationId)
|
||||
* - PiConfig — Pi CLI (pi.dev) settings (model, provider, thinking, resume/continue, project trust)
|
||||
* - GrokConfig — Grok Build CLI (xAI `grok`) settings (model, alwaysApprove, resume/continue)
|
||||
* - DeepSeekConfig — DeepSeek Harness (`dsh`) settings (profile, permissionMode, resume, status bridge)
|
||||
*
|
||||
* Cross-domain relationships:
|
||||
* - SessionState.respawnConfig embeds RespawnConfig (respawn domain)
|
||||
@@ -45,11 +46,20 @@ export type SessionStatus = 'idle' | 'busy' | 'stopped' | 'error';
|
||||
export type ClaudeMode = 'dangerously-skip-permissions' | 'auto' | 'normal' | 'allowedTools';
|
||||
|
||||
/** Session mode: which CLI backend a session runs */
|
||||
export type SessionMode = 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok';
|
||||
export type SessionMode =
|
||||
| 'claude'
|
||||
| 'shell'
|
||||
| 'opencode'
|
||||
| 'codex'
|
||||
| 'gemini'
|
||||
| 'antigravity'
|
||||
| 'pi'
|
||||
| 'grok'
|
||||
| 'deepseek';
|
||||
|
||||
export type RemoteCommandMode = Extract<
|
||||
SessionMode,
|
||||
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok'
|
||||
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek'
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -158,7 +168,7 @@ export interface RemoteSessionInfo {
|
||||
/** Which CLI backends a Docker case can run (same set as remote). */
|
||||
export type DockerCommandMode = Extract<
|
||||
SessionMode,
|
||||
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok'
|
||||
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek'
|
||||
>;
|
||||
|
||||
/** Container engine. Docker and Podman differ in the uid/userns + host-gateway alias. */
|
||||
@@ -388,6 +398,61 @@ export interface GrokConfig {
|
||||
resumeSessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DeepSeek Harness (`dsh`) session configuration.
|
||||
*
|
||||
* Two things make this config shaped unlike every sibling above it.
|
||||
*
|
||||
* **1. The agent is a PROFILE, not the binary.** `dsh` is a launcher: it boots
|
||||
* `$DSH_HOME/profiles/<name>`, an ordered stack of plugin-bundle patch layers.
|
||||
* DeepSeek ships only `web`, `headless` and `base`, so the interactive terminal
|
||||
* agent is always a third-party profile the user installed. `profile` is
|
||||
* therefore the primary knob, and an absent one resolves to the first
|
||||
* pane-capable profile found (see resolveDefaultDeepSeekProfile).
|
||||
*
|
||||
* **2. Permissions are an ENV VAR, not a flag.** The harness has no
|
||||
* `--dangerously-skip-permissions` equivalent; its sandbox and approval rows are
|
||||
* config, driven by one documented input, `DSH_PERMISSION_MODE`, with three
|
||||
* presets (measured from `dsh --dump-default-config`):
|
||||
*
|
||||
* read-only sandbox read-only, approval ask
|
||||
* workspace-write sandbox workspace-write, approval ask <- default
|
||||
* danger-full-access sandbox danger-full-access, approval never
|
||||
*
|
||||
* This is the one place a Codeman env export is the RIGHT mechanism rather than
|
||||
* the forbidden one: unlike `CLAUDE_CODE_EFFORT_LEVEL` (which hard-locks
|
||||
* in-session `/effort`), `DSH_PERMISSION_MODE` is read with `??` as a boot-time
|
||||
* DEFAULT, so it stays a soft default the user can still change in-session. It
|
||||
* is exported via `tmux setenv`, never on the spawn command line.
|
||||
*/
|
||||
export interface DeepSeekConfig {
|
||||
/**
|
||||
* Profile under `$DSH_HOME/profiles` to boot (`dsh --profile <name>`). Absent
|
||||
* = the first pane-capable profile installed. A `web`/`headless` profile is
|
||||
* refused at spawn time: neither can drive an interactive pane.
|
||||
*/
|
||||
profile?: string;
|
||||
/**
|
||||
* Sandbox + approval preset, exported as `DSH_PERMISSION_MODE`. Absent = the
|
||||
* harness's own `workspace-write` default, which still ASKS — which is why the
|
||||
* multi-user clamp only needs the only-if-sent branch here, like
|
||||
* codex/antigravity/grok rather than pi.
|
||||
*/
|
||||
permissionMode?: 'read-only' | 'workspace-write' | 'danger-full-access';
|
||||
/** Resume the most recent session for this workspace (`--resume`). */
|
||||
resumeSession?: boolean;
|
||||
/** Resume a specific session by ID (`--resume <id>`). Wins over resumeSession. */
|
||||
resumeSessionId?: string;
|
||||
/**
|
||||
* Report idle/working/blocked back to Codeman through the Herdr-compatible
|
||||
* status shim (see `deepseek-status-shim.ts`). Default ON: it upgrades this
|
||||
* mode from output-stabilization guessing to definitive hook events. Only
|
||||
* TUIs that implement the contract report; for one that does not, this is
|
||||
* inert rather than harmful.
|
||||
*/
|
||||
statusReporting?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for creating a new session
|
||||
*/
|
||||
@@ -553,6 +618,8 @@ export interface SessionState {
|
||||
piConfig?: PiConfig;
|
||||
/** Grok-specific configuration (only for mode === 'grok') */
|
||||
grokConfig?: GrokConfig;
|
||||
/** DeepSeek Harness configuration (only for mode === 'deepseek') */
|
||||
deepSeekConfig?: DeepSeekConfig;
|
||||
/** Claude conversation session ID to resume after reboot (set by restore script) */
|
||||
resumeSessionId?: string;
|
||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||
|
||||
Reference in New Issue
Block a user