# dsh-as-agent: replace nomos with DeepSeek Harness **Date:** 2026-08-16 **Status:** Active **Scope:** `cmd/nomos/` → dsh sidecar; oikos stays as Go backend behind MCP ## 1. Summary Replace the nomos agent (`cmd/nomos/`, ~5,500 LOC) with [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh), a TypeScript/Cordis agent harness where everything is a plugin — model adapters, tool registry, agent loop, session log, Web UI. dsh runs as a Node.js sidecar alongside the oikos API, connecting via the existing MCP interface (67+ tools). Custom dsh plugins bridge oikos's Postgres-backed session model, policy engine, and approval gating. oikos-web (Svelte 5 SPA) is replaced by dsh's built-in Web UI. The Go backend remains untouched — dsh is purely an agent/UI replacement. ## 2. Session persistence: Postgres vs dsh SQLite dsh ships with `@deepseek-ai/dsh-session` backed by JSONL or SQLite (event-sourced log). oikos uses flat Postgres tables (`agent_sessions`, `agent_messages`, `agent_activity`, `session_plan_steps`, etc.) with ~2,200 lines of domain logic in `internal/nomos/session/store.go`. | | dsh SQLite (default) | oikos Postgres | |---|---|---| | **Model** | Event-sourced: every event appended, `deriveMessages()` projects model history | Flat: typed tables, aggregate columns (message_count, tool_call_count), precomputed views | | **Tightness with oikos data** | dsh owns session data in isolation; oikos backend cannot JOIN across sessions→entities | Session data lives in oikos Postgres alongside entities, executions, knowledge, events — one FK graph | | **Cross-cutting queries** | dsh would need its own API for "all sessions touching host X" | `SELECT ... FROM agent_messages JOIN entities ...` works directly — no bridge | | **Auto-upsert knowledge** | dsh would replicate nomos's `autoUpsertKnowledge` logic | Direct `INSERT INTO knowledge_entities` in the same DB — atomic, no network hop | | **Agent activity/audit** | dsh would replicate `agent_activity` table writes | Already exists: `agent_activity` with entity FK, tool_name, success, duration, tokens | | **Plan + execution linking** | dsh would replicate `nomos_plan_executions` join table | Already exists: `executions` ↔ `session_plan_steps` ↔ `agent_sessions` | | **dsh ecosystem compatibility** | Full — dsh's event-sourced model, built-in compaction, fork, replay, persistence seams all work out of the box | Partial — must write a custom persistence plugin implementing dsh's `session-persistence` seam against Postgres | | **Session events (chunks, boundaries)** | dsh stores raw `assistant/chunk` and `turn/start`/`turn/end` events for faithful replay | oikos stores only the rolled-up `assistant`/`tool` messages — loses per-chunk granularity | | **Migration cost** | None — dsh owns its storage | Medium — must write the Postgres persistence plugin (~1 week) | | **DB schema churn** | None | Adds migration files for dsh's event-sourced log format alongside existing flat tables | **Verdict:** Postgres is the right choice. The tight coupling with entities, knowledge, executions, and the event bus (`observability.Event` via PG `NOTIFY`) is too valuable to sever — it's what makes oikos oikos rather than a generic agent host. The cost is writing a custom `dsh-session-persistence-postgres` plugin that maps dsh's event-sourced `SessionEvent` log onto Postgres rows, while preserving enough granularity for dsh's `deriveMessages()` to reconstruct model history faithfully. The oikos flat tables (`agent_sessions`, `agent_messages`) become projections/views over the event log, maintained for backward compat with the REST API. ## 3. Architecture ``` ┌─────────────────────────────────────────┐ MCP (JSON-RPC over HTTP) │ dsh (Node.js) │ ◄────────────────────────────┐ │ │ │ │ ┌───────────────────────────────────┐ │ │ │ │ dsh-base bundle │ │ │ │ │ - dsh-agent-loop (turn/step) │ │ │ │ │ - dsh-llm-deepseek (model) │ │ │ │ │ - dsh-tools (tool pipeline) │ │ │ │ │ - dsh-session (event log) │ │ │ │ │ - dsh-interaction (approvals) │ │ │ │ │ - dsh-web-app (built-in UI) │ │ │ │ └───────────────────────────────────┘ │ │ │ │ │ │ ┌───────────────────────────────────┐ │ │ │ │ oikos dsh plugins │ │ │ │ │ │ │ │ │ │ @oikos/dsh-mcp-tools │──┤ tools/list + tools/call │ │ │ → discovers 67+ tools via MCP │ │ to oikos MCP server │ │ │ → ctx.tools.register() each │ │ │ │ │ │ │ │ │ │ @oikos/dsh-policy │──┤ classify_command / │ │ │ → tools/pre-execute listener │ │ preflight MCP tools │ │ │ → calls oikos classification │ │ │ │ │ → returns allow/deny/ask │ │ │ │ │ │ │ │ │ │ @oikos/dsh-session-pg │──┤ INSERT/UPDATE/SELECT │ │ │ → implements session- │ │ on oikos Postgres │ │ │ persistence seam vs Postgres │ │ │ │ │ │ │ │ │ │ @oikos/dsh-task-tools │ │ (same Postgres pool) │ │ │ → set_goal, propose_plan, etc. │──┤ │ │ │ → writes oikos tables directly │ │ │ │ │ → emits oikos observability │ │ │ │ │ Events for SSE fan-out │ │ │ │ │ │ │ │ │ │ @oikos/dsh-experiences │ │ │ │ │ → homelab-specific UI nodes │ │ │ │ │ and workflows │ │ │ │ └───────────────────────────────────┘ │ │ │ │ │ │ dsh Web UI (replaces oikos-web) │ │ │ → Serves at :3080 (dsh default) │ │ │ → oikos pages migrated as dsh │ │ │ ConversationNodes + custom views │ │ └─────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────────────────────┤ │ oikos (Go service — unchanged) │ │ │ │ ┌──────────────────────┐ ┌──────────────────────────┐ │ │ │ MCP server: 67+ tools │ │ REST API (chi) │ │ │ │ - Entity, Ops, │ │ - /api/v1/entities │ │ │ │ Knowledge, Analysis │ │ - /api/v1/executions │ │ │ └──────────────────────┘ │ - /api/v1/knowledge │ │ │ │ - health, metrics, etc. │ │ │ ┌──────────────────────┐ └──────────────────────────┘ │ │ │ Policy engine │ │ │ │ (internal/policy) │ ┌──────────────────────────┐ │ │ └──────────────────────┘ │ Postgres │ │ │ │ - entities, relationships │ │ │ ┌──────────────────────┐ │ - executions, signals │ │ │ │ Scheduler + probes │ │ - agent_sessions, messages│ │ │ └──────────────────────┘ │ - knowledge_entities │ │ │ │ - events (SSE NOTIFY) │ │ │ ┌──────────────────────┐ │ - agent_activity │ │ │ │ Secrets (Infisical) │ └──────────────────────────┘ │ │ └──────────────────────┘ │ └──────────────────────────────────────────────────────────────────────┘ ``` ## 4. Phases ### Phase 1: Scaffold + MCP tool bridge (2 weeks) **Goal:** dsh boots, connects to oikos MCP, agents run real tasks through oikos tools. 1. **Create `oikos-dsh/` monorepo** alongside `oikos-web/` - Workspace root with pnpm, `tsconfig`, `vitest` - `oikos-dsh/bundles/oikos-profile/` — profile YAML composing `dsh-base` + oikos plugins - `oikos-dsh/plugins/` — custom plugins directory 2. **`@oikos/dsh-mcp-tools` plugin** - On `apply(ctx)`: connect to oikos MCP (configured URL + token) - Call `tools/list`, for each tool: `ctx.tools.register(mcpToolDef)` - On execute: forward to oikos `tools/call`, stream result back - Handle `_session_id` scoping for concurrent task isolation - Cache tool list, invalidate on reconnect 3. **Boot dsh with the profile** - `dsh --profile oikos` - Validate connectivity at startup - Run `get_entity`, `list_entities` through dsh → confirm roundtrip 4. **Port the golden evals** - `evals/golden.yaml` → dsh vitest test suite - Assert tool calls, completion, plan structure same as nomos - Gate: 4/4 golden evals pass (trivial_readonly, plan_advances_on_proceed, ui_complaint_no_rerun, knowledge_preferred_over_rerun) **Check:** `dsh` agent answers "What is the state of lxc:dns?" through oikos MCP, returns the same answer nomos would. ### Phase 2: Postgres session persistence (1.5 weeks) **Goal:** dsh sessions write to oikos Postgres, not dsh SQLite. 1. **Design the bridging schema** - Add `session_event_log` table: `(session_id UUID, seq INT, event_type TEXT, payload JSONB, created_at TIMESTAMPTZ)` - This is the event-sourced log dsh needs for `deriveMessages()` - `agent_sessions`, `agent_messages` become materialized projections — updated via triggers or application-level write-through - `agent_activity` stays as-is for the audit/operations views 2. **`@oikos/dsh-session-pg` plugin** — implements dsh's session-persistence seam - `subscribe("session/event")` → append row to `session_event_log` - `subscribe("session/flush")` → commit/notify - On load: `SELECT * FROM session_event_log WHERE session_id = $1 ORDER BY seq` → rebuild `Session` - Session lifecycle: `session/created` → ensure row in `agent_sessions`; `session/disposed` → finalize outcome/summary - Use oikos `pgxpool` (via Node.js `pg` module) 3. **`@oikos/dsh-task-tools` plugin** — replaces nomos's task tools - Register `set_goal`, `propose_plan`, `update_plan_step`, `complete_task`, `ask_operator` - Each writes directly to oikos Postgres tables (`agent_sessions`, `session_plan_steps`, `session_questions`) - Emit oikos `observability.Event` via PG `NOTIFY` for SSE live-updates - Mirror nomos's business logic: auto-append writeback step, refuse complete without writeback, plan generation tracking, completion ordering, auto-complete-if-plan-done safety net 4. **Verify against evals** — all golden evals pass through dsh with Postgres persistence **Check:** Create a session in dsh, verify `agent_sessions` and `agent_messages` rows appear in oikos Postgres. Read them back from oikos REST API. ### Phase 3: Policy bridge (1 week) **Goal:** dsh respects oikos risk classification and approval gating. 1. **`@oikos/dsh-policy` plugin** - Listen on `tools/pre-execute` waterfall - For each tool call: call oikos `preflight` or `classify_command` MCP tool - Map risk class to dsh decision: - `readonly` → `allow` (no gate) - `reversible_low` → `allow` (auto-execute, same as nomos) - `config_mutation` → check oikos assent window; if active → `allow`, else → `ask` - `destructive` → check oikos destructive window; if active → `allow`, else → `ask` with typed-confirmation requirement - `ask` returns a dsh `Interaction` — the UI shows an approval dialog; operator decides → tool continues or is denied 2. **Backend:** no changes needed — oikos MCP `classify_command` and `preflight` already exist **Check:** A `run` call with `config_mutation` risk triggers an approval dialog in dsh UI. "Go ahead" in chat grants it. ### Phase 4: UI migration (3-4 weeks) **Goal:** dsh Web UI replaces oikos-web. 1. **dsh Web UI basics** - dsh ships its own Web UI: session list, chat window with tool cards, assistant chunks, turn/step boundaries - No changes needed for basic agent chat — it works out of the box 2. **Custom ConversationNodes for oikos pages** - **Entity Graph page** — reimplement sigma.js graph as a dsh Web Client plugin - ConversationNode listens for tool/call events, renders entity graph - Health/Type color mode toggle, filter presets (All, Problems, Infra) - Port `EntityGraph.svelte`'s logic to a dsh conversation node - **Operations page** — execution list, approval management - Use dsh's existing `interaction` UI for approvals - Custom node for execution history + systemctl status - **Knowledge page** — wiki browser, search, quick-open - dsh already has `search_knowledge` tool; add a Knowledge conversation node - Port `WikiTree`, `WikiReader`, `WikiOverview` from oikos-web - **Signals page** — signal list, ack/mute/resolve - Custom node reading from oikos REST API (via dsh `agent.inject` or API call) - **Overview/Dashboard** — fleet summary, health counts - dsh `get_health_summary` already exists; render as dashboard cards - **Config page** — API token, server URL, theme settings - dsh has `settings` and `credentials` seams; hook into them - **Desktop shell / mascot** — app launcher, dock, taskbar, Cluck mascot - dsh has no desktop paradigm — either skip the shell or implement as a ConversationNode - Mascot can be ported as a persistent UI element 3. **Route mapping** | oikos-web page | dsh equivalent | |---|---| | Overview.svelte | Custom dashboard ConversationNode | | EntityGraph.svelte | Custom entity-graph ConversationNode | | Ops.svelte | Custom operations ConversationNode | | Signals.svelte | Custom signals ConversationNode | | Knowledge.svelte / KnowledgeBase.svelte | Custom knowledge ConversationNode | | Config.svelte | dsh settings/credentials | | Chat session | Built-in dsh chat window | | Learning.svelte | Custom learning ConversationNode | | AppStore.svelte | Custom app-store ConversationNode | 4. **CSS theme migration** - oikos uses dark terminal aesthetic (cyberspace theme, amber/green, dithered images) - dsh has its own light/dark theme — customize via CSS overrides in the profile - Port the GlyphIndicator, MascotLayer, and other visual signatures **Check:** All major oikos-web pages have a functional equivalent in dsh UI. Entity graph renders with force layout and health coloring. ### Phase 5: Experiences as plugins (ongoing) With the bridge complete, "experiences" are standard dsh plugins registered in the profile: | Plugin | What it does | |---|---| | `@oikos/dsh-incident-response` | Guided workflow: detect signal → classify → `run` remediation → verify → document with `upsert_knowledge`. Uses dsh `plan-mode` for structured steps. | | `@oikos/dsh-infra-deploy` | Provision LXCs with blast radius visualization. Pre-flight check via `get_blast_radius`, then step-by-step `run` with approval gates. | | `@oikos/dsh-knowledge-autosync` | Background `ctx.jobs` that periodically audits knowledge gaps (orphan docs, stale entities) and suggests upserts. | | `@oikos/dsh-session-review` | Port the `session-review` skill from `.agents/skills/` to a dsh tool: given a session ID, analyze transcripts, compare objective to outcome, propose fixes. | | `@oikos/dsh-fleet-dashboard` | Real-time fleet health with drill-down. Uses oikos SSE event stream + dsh Web Client custom rendering. | Each plugin: - Registers tools on `ctx.tools` (model-visible capabilities) - Registers ConversationNodes on the Web Client (UI components) - Listens on `agent/*` or `session/event` for reactive behavior - Is independently versioned and hot-loadable via Cordis ## 5. Deleted code On completion of Phases 1-3, the following oikos code is decommissioned: - `cmd/nomos/` — entire directory (~5,500 LOC): agent.go, server.go, mcp.go, store.go (the old flat store), assent.go, continue.go, tasks.go, turngate.go, messagequeue.go, retrycap.go, plus tests - `nomos/` — SOUL.md, config.yaml, skills/ - `internal/nomos/session/` — moved to dsh plugin, but the domain types and some logic may be extracted into a shared `oikos-dsh` npm package - `internal/httpapi/` chat-related endpoints — replaced by dsh's own agent session endpoints - `compose/web/` — web service in docker-compose (served oikos-web SPA) - `desktop/` — Wails desktop wrapper (dsh Web UI is a PWA, no native wrapper needed) The following oikos code stays: - `internal/httpapi/` — REST API for entities, executions, knowledge, signals, health - `internal/mcp/` — the 67+ MCP tools (now serving dsh instead of nomos) - `internal/policy/` — risk classification engine - `internal/scheduler/` — health checks, metrics, probes - `internal/secrets/` — Infisical/SOPS integration - `internal/nomos/assent/`, `internal/nomos/session/` domain types (may be extracted to shared package) ## 6. Migration path The cutover is a rolling deployment: 1. **Deploy dsh alongside nomos** — both agent runtimes run in parallel during development. `compose/dsh/` joins the docker-compose stack. 2. **Port the UI incrementally** — dsh UI and oikos-web coexist on different ports: dsh on `:3080`, oikos-web on `:3000`. The Caddy reverse proxy routes `/chat/*` and `/` to dsh during testing. 3. **Switch the default route** — once dsh passes all golden evals and the UI covers the main pages, Caddy routes all traffic to dsh. oikos-web becomes available at `/legacy` during the transition. 4. **Cleanup** — remove `cmd/nomos/`, `compose/web/`, `oikos-web` repo (or archive). ## 7. Risks | Risk | Mitigation | |---|---| | **dsh breaking changes** — dev preview, no semver | Pin a specific git commit + `pnpm-lock.yaml`. Pin in VERSION file. Stretch: fork the core packages we depend on. | | **Session persistence bridge lag** — dsh expects event-sourced model, oikos has flat tables | Accept dual-write during migration. The `session_event_log` table feeds dsh's `deriveMessages()`; legacy `agent_messages` stays for REST API backward compat until all consumers migrate. | | **UI migration scope** — entity graph, desktop shell, mascot are non-trivial ports | Start with chat + operations (90% of daily use). Entity graph and mascot come after. The old oikos-web stays readable during transition. | | **Golden eval regressions** — subtle behavioral differences between nomos and dsh agent loops | Run evals in CI on every dsh change. Nomos stays deployed until evals pass at parity. | | **Team TS inexperience** — you said TS is OK, but ramp-up for Go developers | Start with small plugins (MCP bridge is ~200 LOC). The dsh extension cookbook is well-documented. | | **Performance** — every tool call crosses TS → HTTP → Go | Same architecture as nomos (which also crossed HTTP). Latency is the same. The MCP server is fast (no serialization overhead beyond JSON). | | **oikos-web features not supported by dsh UI** — desktop shell, window management, mascot | Assess during Phase 4. If the desktop paradigm is essential, implement it as a dsh conversation node (which can render any HTML/CSS) rather than maintaining two UIs. |