mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Point any Codeman-supported harness (Claude, opencode, Codex, Gemini, Pi, Grok, DeepSeek, OMP) at a custom OpenAI-compatible endpoint instead of its native cloud backend, for a given session. Covers local hardware (llama.cpp, Ollama, vLLM, DGX Spark, Strix Halo) and cloud (Azure AI Foundry, OpenRouter). Off by default (customModelEndpointsEnabled, synced, default OFF). - Registry: capabilities.customModelInjection per CLI entry (env / configContentEnv / configDir / unsupported kinds) - Pure injection builder (custom-model-injection.ts) turning an endpoint + model id into the real env vars / config content per CLI - Endpoint store + CRUD routes (custom-model-hosts.ts, custom-model-routes.ts), discovery via GET /v1/models, SSRF-guarded - Session integration: Session.setCustomModel()/restartCli() (POST /api/sessions/:id/custom-model), reusing the existing respawn-pane -k primitive to restart the CLI process with new env - Multi-user hardening: every new redirect-capable env var added to its CLI's privilegedEnvKeys, closing a pre-existing gap where several were already reachable via the generic envOverrides field's prefix allowlist - Standalone scripts/test-local-llm-harnesses.mjs: spawns real CLI binaries against a real endpoint outside the web UI, independent of tmux/sessions - Mock-server contract tests (test/fixtures/mock-openai-server.ts) replaying every CLI's injected values through a real HTTP shape Real end-to-end validation against a live llama-swap server (inside a codeman/agent:llm-test Docker image with all 9 CLI binaries) found and fixed three real bugs before they shipped: - Codex's config.toml schema was wrong ([model].default table instead of a top-level model string + [model_providers.custom]); fixing it then surfaced a genuine, documented protocol incompatibility (Codex only speaks the Responses API since Feb 2026, which llama.cpp/llama-swap don't implement) - Claude Code's async session-title-generation call validates ANTHROPIC_DEFAULT_HAIKU_MODEL against its own internal model list and hangs the whole -p invocation on an unrecognized name; documented for chunk 6, worked around in the standalone script only (--bare is NOT safe for a real interactive session, which needs hooks) - The discovery route's authStyle: 'both' option (send both Authorization and api-key headers) reliably hung a real server; removed the option entirely rather than just changing the default Status: draft. Chunk 6 (frontend toolbar/settings UI) not yet built — see PR.md and deployment_plan.md for the full chunk breakdown and confidence table. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3
717 lines
30 KiB
TypeScript
717 lines
30 KiB
TypeScript
/**
|
|
* @fileoverview Session type definitions.
|
|
*
|
|
* Core domain type — SessionState is the primary entity in the system.
|
|
*
|
|
* Key exports:
|
|
* - SessionState — full session state (status, tokens, respawn, ralph, CLI metadata)
|
|
* - 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' | 'deepseek' | 'omp' (which CLI backend)
|
|
* - ClaudeMode — CLI permission mode ('dangerously-skip-permissions' | 'auto' | 'normal' | 'allowedTools')
|
|
* - SessionColor — visual differentiation color
|
|
* - OpenCodeConfig — OpenCode-specific settings (model, autoAllowTools, continueSession)
|
|
* - CodexConfig — Codex (OpenAI CLI)-specific settings (model, resumeSessionId)
|
|
* - GeminiConfig — Gemini CLI-specific settings (model, approvalMode, resumeSession)
|
|
* - 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)
|
|
* - SessionState.id is referenced by: RalphSessionState.sessionId (ralph),
|
|
* RunSummary.sessionId (run-summary), ActiveBashTool.sessionId (tools),
|
|
* TeamConfig.leadSessionId (teams), RespawnCycleMetrics.sessionId (respawn),
|
|
* TaskState.assignedSessionId (task)
|
|
*
|
|
* Persisted to `~/.codeman/state.json`. Served at `GET /api/sessions` and
|
|
* `GET /api/sessions/:id`.
|
|
*/
|
|
|
|
import type { RespawnConfig } from './respawn.js';
|
|
import type { AttachmentDetectedType } from './tools.js';
|
|
|
|
/** Status of a Claude session */
|
|
export type SessionStatus = 'idle' | 'busy' | 'stopped' | 'error';
|
|
|
|
/**
|
|
* Claude CLI startup permission mode.
|
|
* - `'dangerously-skip-permissions'`: Bypass all permission prompts (default)
|
|
* - `'auto'`: Anthropic's classifier-guarded low-prompt mode (`--permission-mode auto`)
|
|
* - `'normal'`: Standard mode with permission prompts
|
|
* - `'allowedTools'`: Only allow specific tools (requires allowedTools list)
|
|
*/
|
|
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'
|
|
| 'deepseek'
|
|
| 'omp';
|
|
|
|
export type RemoteCommandMode = Extract<
|
|
SessionMode,
|
|
'shell' | 'claude' | 'opencode' | 'codex' | 'gemini' | 'antigravity' | 'pi' | 'grok' | 'deepseek' | 'omp'
|
|
>;
|
|
|
|
/**
|
|
* Advanced SSH connection options shared by RemoteHost and SessionRemote.
|
|
*
|
|
* COD-107 — all fields are optional; every field absent reproduces today's
|
|
* behavior (port-22, default-identity, directly-SSH-able hosts). These describe
|
|
* HOW Codeman reaches the host (identity, proxy, jump host, arbitrary `-o`),
|
|
* letting it connect to e.g. a host fronted by a cloudflared SOCKS5 proxy on a
|
|
* custom port — the same connection `ssh-aa-desktop` makes — without a wrapper.
|
|
*/
|
|
export interface RemoteSshOptions {
|
|
/**
|
|
* Path to an SSH identity (private key) file — path ONLY, never key bytes.
|
|
* A leading `~`/`$HOME` is expanded to an absolute path at command-build time
|
|
* (ssh does not expand `~` in `-i`).
|
|
*/
|
|
identityFile?: string;
|
|
/**
|
|
* SOCKS5 proxy as `host:port` (e.g. `127.0.0.1:1080`). Expands to
|
|
* `-o ProxyCommand=nc -X 5 -x <host:port> %h %p` (the cloudflared/SOCKS5 case).
|
|
*/
|
|
socksProxy?: string;
|
|
/** SSH jump host (`[user@]host[:port]`) emitted as `-J <jumpHost>`. */
|
|
jumpHost?: string;
|
|
/** Arbitrary additional `-o KEY=VALUE` options (escape hatch). Each `KEY=VALUE`. */
|
|
extraSshOptions?: string[];
|
|
}
|
|
|
|
export interface RemoteHost extends RemoteSshOptions {
|
|
id: string;
|
|
label: string;
|
|
host: string;
|
|
username: string;
|
|
port?: number;
|
|
commands?: Partial<Record<RemoteCommandMode, string>>;
|
|
}
|
|
|
|
export interface RemoteCase {
|
|
name: string;
|
|
type: 'remote';
|
|
/** Owning username in multi-user mode; absent = legacy/unassigned (admin-only). */
|
|
owner?: string;
|
|
hostId: string;
|
|
remotePath: string;
|
|
}
|
|
|
|
export interface SessionRemote extends RemoteSshOptions {
|
|
hostId: string;
|
|
label: string;
|
|
host: string;
|
|
username: string;
|
|
port?: number;
|
|
remotePath: string;
|
|
commands?: Partial<Record<RemoteCommandMode, string>>;
|
|
/**
|
|
* COD-105 — whether THIS Codeman created the remote tmux session.
|
|
*
|
|
* - `true` (default for COD-104 launched sessions): we own the remote session;
|
|
* an explicit "kill" may propagate a remote `tmux kill-session`.
|
|
* - `false` (discovered + attached an existing remote session another Codeman
|
|
* created): closing the local tab must DETACH only — we must NEVER issue a
|
|
* remote `kill-session`, or we'd nuke work the remote's own Codeman (or
|
|
* another instance) still relies on. See `killSession()` gate.
|
|
*
|
|
* Absent is treated as owned (legacy/COD-104 sessions persisted before this
|
|
* field existed were all launched by us).
|
|
*/
|
|
owned?: boolean;
|
|
/**
|
|
* COD-105 — for a NON-owned (discovered + attached) session, the EXISTING
|
|
* remote tmux session name to `attach -t` (e.g. `codeman-disco1`). It differs
|
|
* from this Codeman's deterministic `codeman-<id>` name because the remote
|
|
* session was created elsewhere. Only meaningful when `owned === false`.
|
|
*/
|
|
remoteSessionName?: string;
|
|
}
|
|
|
|
/**
|
|
* COD-105 — a `codeman-*` tmux session discovered on a remote host's
|
|
* `tmux -L codeman` socket (may have been created by the remote's own Codeman,
|
|
* another instance, or this one). Returned by `listRemoteCodemanSessions`.
|
|
*/
|
|
export interface RemoteSessionInfo {
|
|
/** tmux session name (always starts `codeman-`). */
|
|
name: string;
|
|
/** Whether at least one client is currently attached to the remote session. */
|
|
attached: boolean;
|
|
/** COD-106 — number of clients attached (tmux `session_attached`); >1 = shared. */
|
|
attachedClients: number;
|
|
/** tmux `session_created` epoch seconds. */
|
|
created: number;
|
|
/** Number of windows in the remote session. */
|
|
windows: number;
|
|
}
|
|
|
|
// ========== Docker cases (COD-Docker) ==========
|
|
//
|
|
// Docker mode is a LOCATION OVERLAY on cases (never a 6th SessionMode), the exact
|
|
// analog of the remote-SSH feature above: instead of a local tmux pane running
|
|
// `ssh host` into a durable remote tmux server, a local tmux pane runs
|
|
// `docker exec -it` into a durable in-container tmux server. The container is
|
|
// scoped to the CASE (not the session), so multiple sessions can `docker exec`
|
|
// into the same long-lived container. See `docs/docker-cases-plan.md`.
|
|
|
|
/** 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' | 'deepseek' | 'omp'
|
|
>;
|
|
|
|
/** Container engine. Docker and Podman differ in the uid/userns + host-gateway alias. */
|
|
export type DockerEngine = 'docker' | 'podman';
|
|
|
|
/**
|
|
* Container network mode. `host` and any inbound `-p` publish are deliberately
|
|
* unrepresentable (never in this union, never emitted by the flag builder).
|
|
* - `bridge`: own netns, NAT egress, no inbound (default — every API CLI needs egress)
|
|
* - `none`: fully offline sandbox (breaks API CLIs; reserved for `shell`)
|
|
* - `custom`: a user-defined bridge `codeman-net-<slug>` (future egress-allowlist chokepoint)
|
|
*/
|
|
export type DockerNetworkMode = 'bridge' | 'none' | 'custom';
|
|
|
|
/** Per-container resource caps. Advisory under non-delegated rootless (see `capsEnforced`). */
|
|
export interface DockerResourceLimits {
|
|
/** e.g. '4g' -> --memory 4g --memory-swap 4g (swap==memory: a real OOM cap) */
|
|
memory?: string;
|
|
/** e.g. '2' -> --cpus 2 */
|
|
cpus?: string;
|
|
/** e.g. 512 -> --pids-limit 512 (fork-bomb guard) */
|
|
pidsLimit?: number;
|
|
/** e.g. '4096:8192' -> --ulimit nofile=4096:8192 */
|
|
nofile?: string;
|
|
/** e.g. '256m' -> --shm-size (only when a tool needs /dev/shm) */
|
|
shmSize?: string;
|
|
}
|
|
|
|
/** A reusable Docker engine/image/network/resource profile (mirror of RemoteHost). */
|
|
export interface DockerHost {
|
|
id: string;
|
|
label: string;
|
|
/** Engine; when absent the availability probe resolves it (docker, else podman). */
|
|
engine?: DockerEngine;
|
|
/** Base image ref (built locally by scripts/build-agent-image.mjs, e.g. codeman/agent:base). */
|
|
image: string;
|
|
/** Advanced: remote daemon (-H ssh://user@host or a DOCKER_HOST value). */
|
|
daemonHost?: string;
|
|
/** Advanced: docker `--context` name. */
|
|
context?: string;
|
|
/** Network mode (default 'bridge'). */
|
|
network?: DockerNetworkMode;
|
|
/** Custom bridge name when network === 'custom'. */
|
|
networkName?: string;
|
|
resources?: DockerResourceLimits;
|
|
/** GPU allocation, e.g. 'all' / '1' / 'device=0,1' -> `--gpus <value>` (needs the NVIDIA container toolkit). */
|
|
gpus?: string;
|
|
/** true (default) = convenient: bind-mount host cred dirs RW. false = sealed (blocks full-image export). */
|
|
mountCredentials?: boolean;
|
|
/** true (default) = wire in-container hooks (host-gateway callback + workspace scaffold). */
|
|
hooksEnabled?: boolean;
|
|
/** true (default) = a relaunch resumes the last conversation from the bind-mounted transcript. */
|
|
resumeOnStart?: boolean;
|
|
/** Per-mode command overrides (mirror RemoteHost.commands). */
|
|
commands?: Partial<Record<DockerCommandMode, string>>;
|
|
/** Escape hatch: extra `docker create` args (validated like extraSshOptions). */
|
|
extraCreateArgs?: string[];
|
|
/** Escape hatch: extra `docker exec` args. */
|
|
extraExecArgs?: string[];
|
|
}
|
|
|
|
/** A case linked to a Docker container (mirror of RemoteCase). */
|
|
export interface DockerCase {
|
|
name: string;
|
|
type: 'docker';
|
|
/** Owning username in multi-user mode; absent = legacy/unassigned (admin-only). */
|
|
owner?: string;
|
|
hostId: string;
|
|
/** Absolute HOST directory: the bind-mount source AND Session.workingDir (real host bytes). */
|
|
hostWorkspacePath: string;
|
|
/** Container path (default = hostWorkspacePath: mirror -> transcript projHash correlates). */
|
|
containerWorkdir?: string;
|
|
/** Container name (default codeman-case-<slug>). */
|
|
container?: string;
|
|
/**
|
|
* Whether THIS Codeman created the container (mirror of `SessionRemote.owned`).
|
|
*
|
|
* - `true` (default for cases Codeman linked/quick-created): we own the
|
|
* container; drift may recreate it, case-delete may `docker rm -f` it, and
|
|
* the launch chain may create + start it.
|
|
* - `false` (ADOPTED: an already-running container the user built and runs
|
|
* themselves): Codeman must never create, start, stop, restart or remove it.
|
|
* The launch chain fails closed when the container is missing or not running
|
|
* instead of touching its lifecycle, drift is not evaluated (there is no
|
|
* `codeman.confighash` label to compare), and no credential seed is copied
|
|
* into its HOME. Only the in-container tmux session is ever created or
|
|
* killed — exactly the `owned:false` remote-SSH contract.
|
|
*
|
|
* Absent is treated as owned (cases persisted before this field existed were
|
|
* all created by us).
|
|
*/
|
|
owned?: boolean;
|
|
/**
|
|
* CLIs found INSIDE the container by the adoption preflight. A container case
|
|
* runs its agents in the container, so host CLI availability says nothing about
|
|
* what this case can run — the base image ships every CLI, and an adopted
|
|
* container ships whatever its owner installed. Absent = unknown (owned cases,
|
|
* or a case linked before this field existed), which callers read as "do not
|
|
* gate".
|
|
*/
|
|
availableModes?: SessionMode[];
|
|
/** Last captured Claude conversation id, replayed via --resume on a fresh launch. */
|
|
lastClaudeSessionId?: string;
|
|
}
|
|
|
|
/**
|
|
* Flattened Docker execution metadata carried on a live session (mirror of
|
|
* SessionRemote). Round-trips through MuxSession/SessionState/mux-sessions.json.
|
|
*/
|
|
export interface SessionDocker {
|
|
hostId: string;
|
|
label: string;
|
|
engine: DockerEngine;
|
|
image: string;
|
|
/** Per-CASE container name (shared by all sessions of the case). */
|
|
containerName: string;
|
|
hostWorkspacePath: string;
|
|
containerWorkdir: string;
|
|
network: DockerNetworkMode;
|
|
networkName?: string;
|
|
resources?: DockerResourceLimits;
|
|
/** GPU allocation ('all' / '1' / 'device=0,1'). */
|
|
gpus?: string;
|
|
mountCredentials: boolean;
|
|
hooksEnabled: boolean;
|
|
resumeOnStart: boolean;
|
|
daemonHost?: string;
|
|
context?: string;
|
|
commands?: Partial<Record<DockerCommandMode, string>>;
|
|
extraCreateArgs?: string[];
|
|
extraExecArgs?: string[];
|
|
/** Stable hash of the drift-relevant create args (recreate-on-drift detection). */
|
|
configHash?: string;
|
|
/**
|
|
* Whether the container's exec user is root. Claude Code REFUSES
|
|
* `--dangerously-skip-permissions` as root, and an adopted container's user
|
|
* belongs to its owner, so the flag is omitted rather than letting the pane
|
|
* die with a message only visible inside the container.
|
|
*/
|
|
runsAsRoot?: boolean;
|
|
/**
|
|
* Mirror of `DockerCase.owned`, flattened onto the live session so every
|
|
* lifecycle decision (launch chain, drift, stop, remove) can see it without
|
|
* re-reading docker-cases.json. Absent = owned. See `DockerCase.owned`.
|
|
*/
|
|
owned?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Valid Claude CLI effort levels (claude >= 2.1.154).
|
|
* `ultracode` = xhigh effort + standing dynamic-workflow orchestration; it is a
|
|
* separate `ultracode` settings key rather than an `effortLevel` value.
|
|
*/
|
|
export const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'] as const;
|
|
|
|
/** Claude CLI effort level for new sessions (soft default, switchable via /effort in-session) */
|
|
export type EffortLevel = (typeof EFFORT_LEVELS)[number];
|
|
|
|
/** Type guard: is the string a valid EffortLevel? */
|
|
export function isEffortLevel(value: string | undefined): value is EffortLevel {
|
|
return value !== undefined && (EFFORT_LEVELS as readonly string[]).includes(value);
|
|
}
|
|
|
|
/** OpenCode session configuration */
|
|
export interface OpenCodeConfig {
|
|
/** Model identifier (e.g., "anthropic/claude-sonnet-4-5", "openai/gpt-5.2", "ollama/codellama") */
|
|
model?: string;
|
|
/** Whether to auto-allow all tool executions (sets permission.* = allow) */
|
|
autoAllowTools?: boolean;
|
|
/** Session ID to continue from */
|
|
continueSession?: string;
|
|
/** Whether to fork when continuing (branch the conversation) */
|
|
forkSession?: boolean;
|
|
/** Custom inline config JSON (passed via OPENCODE_CONFIG_CONTENT) */
|
|
configContent?: string;
|
|
}
|
|
|
|
/** Codex (OpenAI CLI) browser rendering strategy. Hybrid TUI is the only supported mode. */
|
|
export type CodexRenderMode = 'hybrid';
|
|
|
|
/** Codex (OpenAI CLI) session configuration */
|
|
export interface CodexConfig {
|
|
/** Model identifier (e.g., "gpt-5", "o4-mini"). Passed via --model. */
|
|
model?: string;
|
|
/** Resume a previous codex conversation by session id (passed via --resume) */
|
|
resumeSessionId?: string;
|
|
/** Bypass approval prompts (passes --dangerously-bypass-approvals-and-sandbox) */
|
|
dangerouslyBypassApprovals?: boolean;
|
|
/** Enable Codex's decorative TUI animations. Disable to reduce remote terminal redraws. */
|
|
animations?: boolean;
|
|
/** Browser rendering strategy for Codex sessions. Hybrid TUI is the only supported mode. */
|
|
renderMode?: CodexRenderMode;
|
|
}
|
|
|
|
/** Gemini CLI session configuration */
|
|
export interface GeminiConfig {
|
|
/** Model identifier (e.g., "gemini-2.5-pro"). Passed via --model. */
|
|
model?: string;
|
|
/** Gemini approval mode for tool calls. */
|
|
approvalMode?: 'default' | 'auto_edit' | 'yolo' | 'plan';
|
|
/** Resume a previous Gemini session ("latest", index, or session id). */
|
|
resumeSession?: string;
|
|
}
|
|
|
|
/** Antigravity CLI (agy) session configuration */
|
|
export interface AntigravityConfig {
|
|
/** Model identifier. Passed via --model. */
|
|
model?: string;
|
|
/** Auto-approve all tool permission requests (passes --dangerously-skip-permissions). Absent = agy's default prompting. */
|
|
dangerouslySkipPermissions?: boolean;
|
|
/** Resume a previous conversation by ID (passed via --conversation). */
|
|
resumeConversationId?: string;
|
|
}
|
|
|
|
/** OMP CLI session configuration */
|
|
export interface OmpConfig {
|
|
/** Model identifier (e.g., "crof/glm-5.2"). Passed via --model. */
|
|
model?: string;
|
|
/** Resume a previous conversation (passed via --resume). */
|
|
resumeSessionId?: string;
|
|
/** Continue the most recent session in this directory (passed via --continue). */
|
|
continueSession?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Pi CLI (pi.dev) session configuration.
|
|
*
|
|
* Pi has NO permission prompts and no `--dangerously-skip-permissions` analog,
|
|
* so there is deliberately no bypass field here. The one privilege-shaped knob is
|
|
* `approveProjectTrust`, which controls whether pi loads and EXECUTES repo-local
|
|
* `.pi/` extensions (and installs missing project packages).
|
|
*/
|
|
export interface PiConfig {
|
|
/** Model pattern or ID. Supports `provider/id` and a `:<thinking>` suffix (e.g. `sonnet:high`). Passed via --model. */
|
|
model?: string;
|
|
/** Provider name (anthropic, openai, google, ...). Passed via --provider. */
|
|
provider?: string;
|
|
/** Reasoning level. Passed via --thinking. */
|
|
thinking?: 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
/** Continue the most recent session (-c). Skipped when resumeSessionId is set (the two conflict). */
|
|
continueSession?: boolean;
|
|
/** Resume a specific session by ID or partial UUID (--session). Ids only, never paths. */
|
|
resumeSessionId?: string;
|
|
/**
|
|
* Tri-state project trust (repo-local `.pi/` settings/extensions/skills, plus
|
|
* installing missing project packages):
|
|
* true -> --approve (trust for this run; loads and EXECUTES repository TypeScript)
|
|
* false -> --no-approve (force-deny; the trust prompt never appears)
|
|
* absent -> pi's own defaultProjectTrust (ask).
|
|
* Multi-user: MATERIALIZED to false for non-granted owners, because pi's
|
|
* absent-config default is a prompt the session user could answer themselves.
|
|
*/
|
|
approveProjectTrust?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Grok Build CLI (xAI `grok`) session configuration.
|
|
*
|
|
* Grok has Claude-style permission modes; the bypass switch is `--always-approve`
|
|
* ("auto-approve all tool executions", the CLI's `bypassPermissions` mode). Deny
|
|
* rules from `~/.grok/config.toml` / project `.grok/config.toml` still apply on
|
|
* top of it. Verified against grok 1.0.5.
|
|
*/
|
|
export interface GrokConfig {
|
|
/** Model ID (e.g. "grok-4.5", or a custom `[model.<name>]` from config.toml). Passed via --model. */
|
|
model?: string;
|
|
/**
|
|
* Auto-approve all tool executions (passes --always-approve). Absent = grok's
|
|
* own default permission mode (ask). Multi-user: forced off for non-granted
|
|
* owners by the only-if-sent clamp branch, like codex/antigravity — the
|
|
* absent-config spawn already defaults safe.
|
|
*/
|
|
alwaysApprove?: boolean;
|
|
/** Continue the most recent session for the working directory (-c). Skipped when resumeSessionId is set. */
|
|
continueSession?: boolean;
|
|
/** Resume a specific session by ID (--resume). Ids only, never titles or paths. */
|
|
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
|
|
*/
|
|
export interface SessionConfig {
|
|
/** Unique session identifier */
|
|
id: string;
|
|
/** Working directory for the session */
|
|
workingDir: string;
|
|
/** Timestamp when session was created */
|
|
createdAt: number;
|
|
}
|
|
|
|
/**
|
|
* Available session colors for visual differentiation
|
|
*/
|
|
export type SessionColor = 'default' | 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple' | 'pink';
|
|
|
|
export type SessionAttachmentHistorySource = 'detected' | 'external';
|
|
|
|
/**
|
|
* Session-scoped attachment history entry.
|
|
*
|
|
* `externalPath` is server-private. It may be present in the internal persisted
|
|
* history copy, but API-bound session state must sanitize it before returning
|
|
* to the browser.
|
|
*/
|
|
export interface SessionAttachmentHistoryItem {
|
|
/** Stable history identity used for dedupe and list rendering */
|
|
id: string;
|
|
/** Codeman session ID this item belongs to */
|
|
sessionId: string;
|
|
/** Display filename */
|
|
fileName: string;
|
|
/** Lowercase extension without a leading dot */
|
|
extension: string;
|
|
/** Viewer category used by the web UI */
|
|
attachmentType: AttachmentDetectedType;
|
|
/** File size in bytes */
|
|
size: number;
|
|
/** Last modified timestamp in milliseconds, if known */
|
|
mtimeMs: number;
|
|
/** Last time this attachment was seen or explicitly published */
|
|
timestamp: number;
|
|
/** How the attachment entered the session */
|
|
source: SessionAttachmentHistorySource;
|
|
/** Workspace-relative path for detected session files */
|
|
relativePath?: string;
|
|
/** Server-private absolute path for explicitly published external files */
|
|
externalPath?: string;
|
|
}
|
|
|
|
/**
|
|
* Current state of a session
|
|
*/
|
|
export interface SessionState {
|
|
/** Unique session identifier */
|
|
id: string;
|
|
/** Process ID of the PTY process, null if not running */
|
|
pid: number | null;
|
|
/** Current session status */
|
|
status: SessionStatus;
|
|
/** Working directory path */
|
|
workingDir: string;
|
|
/** Remote execution metadata, present when this session runs over SSH through local tmux */
|
|
remote?: SessionRemote;
|
|
/** Docker execution metadata, present when this session runs inside a container via local tmux + docker exec */
|
|
docker?: SessionDocker;
|
|
/** Owning username in multi-user mode; undefined in single-user (ignored when the flag is off) */
|
|
owner?: string;
|
|
/**
|
|
* The Codeman session that spawned this one, supplied by the caller at create time
|
|
* (`parentSessionId` body field or the `X-Codeman-Parent-Session` header) and resolved
|
|
* against live sessions before being stored.
|
|
*
|
|
* ⚠️ UI DECORATION ONLY — it draws the lineage lines between tabs. It is never an
|
|
* ownership, permission, or lifecycle signal: a child outlives its parent, and an
|
|
* unresolvable value is dropped rather than failing the spawn.
|
|
*/
|
|
parentSessionId?: string;
|
|
/** ID of currently assigned task, null if none */
|
|
currentTaskId: string | null;
|
|
/** Timestamp when session was created */
|
|
createdAt: number;
|
|
/** Timestamp of last activity */
|
|
lastActivityAt: number;
|
|
/** Session display name */
|
|
name?: string;
|
|
/** Session mode */
|
|
mode?: SessionMode;
|
|
/** Auto-clear enabled */
|
|
autoClearEnabled?: boolean;
|
|
/** Auto-clear token threshold */
|
|
autoClearThreshold?: number;
|
|
/** Auto-compact enabled */
|
|
autoCompactEnabled?: boolean;
|
|
/** Auto-compact token threshold */
|
|
autoCompactThreshold?: number;
|
|
/** Auto-compact prompt */
|
|
autoCompactPrompt?: string;
|
|
/** Auto-resume on usage limit enabled */
|
|
autoResumeEnabled?: boolean;
|
|
/** Pending usage-limit auto-resume fire time (epoch ms), if armed */
|
|
autoResumeAt?: number;
|
|
/** Pinned to the top of the session manager list (COD-139) */
|
|
pinned?: boolean;
|
|
/** When the session was pinned (epoch ms) — orders the pinned group, most-recent-first */
|
|
pinnedAt?: number;
|
|
/** Image watcher enabled for this session */
|
|
imageWatcherEnabled?: boolean;
|
|
/** Total cost in USD */
|
|
totalCost?: number;
|
|
/** Input tokens used */
|
|
inputTokens?: number;
|
|
/** Output tokens used */
|
|
outputTokens?: number;
|
|
/** Whether respawn controller is currently enabled/running */
|
|
respawnEnabled?: boolean;
|
|
/** Respawn controller config (if enabled) */
|
|
respawnConfig?: RespawnConfig & { durationMinutes?: number };
|
|
/** Ralph / Todo tracker enabled */
|
|
ralphEnabled?: boolean;
|
|
/** Ralph auto-enable disabled (user explicitly turned off Ralph) */
|
|
ralphAutoEnableDisabled?: boolean;
|
|
/** Ralph completion phrase (if set) */
|
|
ralphCompletionPhrase?: string;
|
|
/** Parent agent ID if this session is a spawned agent */
|
|
parentAgentId?: string;
|
|
/** Child agent IDs spawned by this session */
|
|
childAgentIds?: string[];
|
|
/** Nice priority enabled */
|
|
niceEnabled?: boolean;
|
|
/** Nice value (-20 to 19) */
|
|
niceValue?: number;
|
|
/** User-assigned color for visual differentiation */
|
|
color?: SessionColor;
|
|
/** Flicker filter enabled (buffers output after screen clears) */
|
|
flickerFilterEnabled?: boolean;
|
|
/**
|
|
* True while the CLI in the pane has a mouse-tracking DECSET on, as observed
|
|
* by the server on its way out of the stream (those sequences are stripped for
|
|
* claude/codex/gemini, so the browser can never see them itself). The browser
|
|
* hand-encodes a click report ONLY when this is true; without it, every click
|
|
* sent mouse reports to a CLI that never asked for them.
|
|
*/
|
|
cliMouseTracking?: boolean;
|
|
/** Claude Code CLI version (parsed from terminal, e.g., "2.1.27") */
|
|
cliVersion?: string;
|
|
/** Claude model in use (parsed from terminal, e.g., "Opus 4.5") */
|
|
cliModel?: string;
|
|
/** Account type (parsed from terminal, e.g., "Claude Max", "API") */
|
|
cliAccountType?: string;
|
|
/** Latest CLI version available (parsed from version check) */
|
|
cliLatestVersion?: string;
|
|
/** OpenCode-specific configuration (only for mode === 'opencode') */
|
|
openCodeConfig?: OpenCodeConfig;
|
|
/** Codex-specific configuration (only for mode === 'codex') */
|
|
codexConfig?: CodexConfig;
|
|
/** Gemini-specific configuration (only for mode === 'gemini') */
|
|
geminiConfig?: GeminiConfig;
|
|
/** Antigravity-specific configuration (only for mode === 'antigravity') */
|
|
antigravityConfig?: AntigravityConfig;
|
|
/** Pi-specific configuration (only for mode === 'pi') */
|
|
piConfig?: PiConfig;
|
|
/** Grok-specific configuration (only for mode === 'grok') */
|
|
grokConfig?: GrokConfig;
|
|
/** DeepSeek Harness configuration (only for mode === 'deepseek') */
|
|
deepSeekConfig?: DeepSeekConfig;
|
|
/** OMP-specific configuration (only for mode === 'omp') */
|
|
ompConfig?: OmpConfig;
|
|
/** 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) */
|
|
effort?: EffortLevel;
|
|
/**
|
|
* Custom Model Endpoint Profiles (deployment_plan.md): the custom OpenAI-compatible
|
|
* endpoint (local or cloud) this session's CLI is currently pointed at, if any.
|
|
* Undefined = the harness's native cloud default. No secrets here — the endpoint's
|
|
* base URL/api key live only in Session._envOverrides, never in this public state.
|
|
*/
|
|
customModel?: { endpointId: string; modelId: string; label?: string };
|
|
/** Sanitized per-session attachment history. */
|
|
attachmentHistory?: SessionAttachmentHistoryItem[];
|
|
/**
|
|
* Wall-clock ms of this pane's last Enter (Session.lastSubmitAt). Persisted
|
|
* because it is the response-viewer's only anchor for re-deriving the pane's
|
|
* live conversation after a Codeman restart: `start()` resets
|
|
* `claudeSessionId` to the launch id even when re-attaching to a mux session
|
|
* whose CLI has since moved on via `/clear`, and the correlation cannot run
|
|
* again until the pane's own Enter is known.
|
|
*/
|
|
lastSubmitAt?: number;
|
|
/**
|
|
* Claude conversations this pane has been on, oldest first, current last.
|
|
* Written ONLY from a first-hand `UserPromptSubmit`/`Stop` hook payload —
|
|
* never from the history correlation — so it cannot record a sibling pane's
|
|
* conversation. Persisted because `/clear` is otherwise unrecoverable: once
|
|
* the pane moves on, the predecessor id exists nowhere else, and the last
|
|
* entry is what re-pins `claudeSessionId` past `start()`'s three resets.
|
|
*/
|
|
claudeSessionChain?: string[];
|
|
/**
|
|
* PTY-exit circuit breaker tripped — respawn blocked until an explicit restart
|
|
* (COD-118). Runtime-only: never restored on boot (fresh server = fresh breaker).
|
|
*/
|
|
respawnBlocked?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Output captured from a session
|
|
*/
|