# 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)~~ DONE **Status:** Complete. 8/8 golden evals passing. **What was built:** - `@deepseek-ai/dsh-mcp-client` discovery of all 67+ oikos MCP tools - `@deepseek-ai/dsh-llm-deepseek` model adapter via OpenRouter - Golden eval suite at `packages/oikos/evals/src/golden.test.ts` - Bundle at `packages/oikos/bundle/cordis.patch.yml` - Plugin packages inside deepseek-harness workspace at `packages/oikos/` **Known limitation:** `_session_id` cannot be injected into MCP tool call args — dsh deep-freezes args before pre-execute hooks fire, tool definitions have no interceptor mechanism, and MCP protocol has no per-call metadata. This is accepted as a permanent architectural constraint (see Phase 3 note below). ### Phase 2: Postgres session persistence ~~(1.5 weeks)~~ DONE **Status:** Complete. Hybrid approach — dsh SQLite owns the event-sourced log; thin Postgres mirror for cross-cutting queries. **What was built:** - `@deepseek-ai/dsh-oikos-session-summary` plugin (`packages/oikos/session-summary/`) - Mirrors `session/created` → `agent_sessions` INSERT, `session/disposed` → UPDATE status/outcome/closed_at - Tracks `session/title`, `turn/start`, `turn/end`, message counts for status transitions - UUID v5 deterministic mapping from dsh `session-` IDs to oikos UUID `agent_sessions.id` **What was deferred:** - Full `session-persistence` seam replacement (dsh SQLite → Postgres) — hybrid mirror is sufficient for oikos cross-cutting queries - `@oikos/dsh-task-tools` (set_goal, propose_plan, etc.) — nomos task model not yet ported ### Phase 3: Policy bridge ~~(1 week)~~ DONE **Status:** Complete. Consent window end-to-end flow working. **What was built:** - `@deepseek-ai/dsh-oikos-mcp-scope` plugin (`packages/oikos/mcp-scope/`) - `tools/pre-execute` waterfall: read-only → allow, mutation → check consent window → ask if no window - `tools/post-execute` auto-approve: detects oikos "requires approval" response → calls `decide_approval` HTTP API → replaces result text - Consent window: `approval/decided` event with `allowed-once` opens 30-min assent window in Postgres `autonomy_settings` - Auto-expiry cleanup (1-hour interval) for consent/destructive windows - Go-side fixes: `decide_approval` token mismatch, `X-Oikos-Session-Id` header fallback, LIKE-based assent query **Architecture note — two-layer consent:** Because `_session_id` cannot be passed to oikos MCP, the consent flow operates in two layers: 1. **dsh layer** (mcp-scope pre-execute): checks Postgres assent window by session UUID → allows without asking operator again 2. **oikos layer** (Go classifyAndGate): always sees empty `_session_id` → queues execution → post-execute auto-approve detects the queued result and calls the oikos `decide_approval` HTTP API This is the permanent solution — not a workaround. The dsh layer provides the operator consent UX; the post-execute layer bridges the gap to oikos's execution pipeline. ### Phase 4: UI migration (3-4 weeks) — IN PROGRESS **Goal:** dsh Web UI replaces oikos-web. **Prerequisites:** - [x] Phase 1-3 complete - [x] dsh running at http://127.0.0.1:3080 with all oikos MCP tools - [x] Consent/approval flow working end-to-end - [x] dsh-gate: oikos auto-runs non-destructive when no session - [x] dsh-harness plugin changes committed - [x] Post-execute auto-approve removed (dead code) #### 4.0 Architecture mapping oikos-web is a Svelte 5 desktop-windowing SPA (wmkit) with 10 apps in a floating window manager. dsh's Web Client is a React three-column layout (sidebar | conversation | details) with a slot-based extension system — no router, no windowing paradigm. **Key dsh extension surfaces:** | dsh surface | Type | Scope | Use for | |---|---|---|---| | `conversation.view` | list | session | View tabs replacing chat (like Trajectory) | | `settings.section` | list | root | Full settings pages | | `conversation.chat.node` | keyed | session | Inline chat rows (ConversationNodes) | | `sidebar.footer.action` | list | root | Sidebar footer actions | | `shell.overlay` | list | root | Floating overlay badges | | `conversation.composer` | chain | session | Composer takeover (approvals) | | `conversation.details.tool` | single | session | Right panel tool details | | `conversation.session.header.actions` | list | session | Per-session header action buttons | **What dsh provides out of the box (no migration needed):** - Chat window with tool cards, streaming, turn/step boundaries - Session list (workspace browser in sidebar) - Approval dialog (`tools/pre-execute` `ask` → built-in approval UI) - Settings panel (theme, credentials, model selection) - Dark/light theme with `--dsw-*` CSS token overrides #### 4.1 Page migration plan (priority order) **Tier 1 — Daily operations (week 1-2):** | oikos-web page | Complexity | dsh approach | Notes | |---|---|---|---| | **Ops.svelte** | Medium | `settings.section` → "Operations" page | Approvals list + recent activity. Call `/api/v1/approvals`, `/api/v1/activity/recent` via `fetch()`. Approve/deny via `decide_approval` MCP tool or direct HTTP. This is the most-used page after chat. | | **Signals.svelte** | Medium | `settings.section` → "Signals" page | Signal list with ack/mute/resolve. Call `/api/v1/signals`. Direct HTTP POSTs for actions. | | **Config.svelte** | None | dsh built-in | Already handled by dsh settings/credentials. Token stored in dsh credentials seam. | **Tier 2 — Navigation & fleet awareness (week 2-3):** | oikos-web page | Complexity | dsh approach | Notes | |---|---|---|---| | **Overview.svelte** (Tasks) | Low | dsh built-in + `sidebar.footer.action` badge | dsh already has session list in sidebar. Add a pending-approvals count badge to `shell.overlay` via polling `/api/v1/dashboard/summary`. | | **KnowledgeBase.svelte** (Fleet) | High | `conversation.view` → "Fleet" tab | Entity table + health status. Call `/api/v1/entities?limit=200`, `/api/v1/ontology`. Live updates via SSE `/api/v1/events/stream`. | **Tier 3 — Complex visualizations (week 3-4):** | oikos-web page | Complexity | dsh approach | Notes | |---|---|---|---| | **EntityGraph.svelte** | Very High | `conversation.view` → "Graph" tab | sigma.js + graphology force layout. Port the graph rendering to a React component registered as a view tab. Health/Type color modes, filter presets, blast radius on click. This is the hardest port (~711 LOC of Svelte → React). | | **Knowledge.svelte** (Wiki) | High | `conversation.view` → "Knowledge" tab | Three-pane split (tree + reader + context rail). Full CRUD via `/api/v1/knowledge/*`. Markdown rendering via dsh's built-in `MarkdownText`. Wiki tree and search are the main lift. | | **EntityDetailContent.svelte** | Very High | `conversation.details.tool` or modal | ~1188 LOC. Dynamic sections per entity type (health, checks, metrics, relations, events, signals, executions, knowledge). Consider deferring to Phase 5 or implementing incrementally (health + relations first). | **Tier 4 — Nice to have (deferred):** | oikos-web page | Complexity | dsh approach | Notes | |---|---|---|---| | **Learning.svelte** | Medium | `conversation.view` → "Learning" tab | uPlot trend chart + patterns + skills. Lower priority. | | **AppStore.svelte** | Low | Skip | No real catalog — just "Notes" app. Not needed in dsh. | | **Desktop shell** (wmkit) | N/A | Skip entirely | dsh uses a standard web layout, not a windowing desktop. The window manager paradigm doesn't map. | | **Mascot (Cluck)** | Medium | `shell.overlay` or skip | Persistent animated mascot. Low priority — pure visual flair. | | **GlyphIndicator** | Low | `shell.overlay` or sidebar footer | Canvas-rendered procedural glyph. Low priority. | #### 4.2 CSS theme oikos-web uses a **Gruvbox-inspired theme** (amber primary `#d79921`, dark bg `#1d2021`, JetBrains Mono + VT323 fonts). dsh uses `--dsw-*` CSS tokens with light/dark palettes. Migration approach: 1. Register a custom dsh theme via `ctx.theme.register()` that overrides alias-layer tokens to match Gruvbox 2. Key token mappings: - `--dsw-alias-brand-primary` → `#d79921` (amber) - `--dsw-alias-bg-base` → `#1d2021` (dark bg) - `--dsw-alias-label-primary` → `#ebdbb2` (warm white) 3. Fonts: dsh uses its own font system. Override via CSS `font-family` on body if JetBrains Mono/VT323 are desired. Optional — dsh's default fonts are fine. #### 4.3 Plugin structure New package: `packages/oikos/ui-plugin/` ``` packages/oikos/ui-plugin/ src/ index.ts — apply(): registers all slots + theme theme.ts — Gruvbox token overrides ops-page.tsx — Operations settings section signals-page.tsx — Signals settings section fleet-view.tsx — Fleet conversation view tab graph-view.tsx — Entity graph conversation view tab knowledge-view.tsx — Knowledge conversation view tab api.ts — fetch wrapper for oikos REST endpoints package.json tsconfig.json ``` The ui-plugin is composed into the oikos bundle (cordis.patch.yml) alongside mcp-client, scope, and session-summary. It only runs in the Web Client bundle (browser-side), not in the Node.js host. #### 4.4 REST API access dsh has no generic HTTP client for external APIs. The oikos ui-plugin will: 1. Use native `fetch()` with the oikos API base URL (from plugin config or dsh credentials seam) 2. Wrap in a typed `OikosApi` class (`api.ts`) with methods for each endpoint 3. Handle auth via the same bearer token stored in dsh credentials The oikos REST API remains unchanged — all existing `/api/v1/*` endpoints continue to serve the dsh Web Client the same data they served oikos-web. #### 4.5 SSE live updates oikos-web uses SSE (`/api/v1/events/stream`) for real-time updates across all pages. The ui-plugin will: 1. Open a single `EventSource` connection to `/api/v1/events/stream` on plugin init 2. Dispatch events to registered listeners (signals, approvals, entity health) 3. Auto-reconnect on disconnect (same pattern as oikos-web's `events.ts` store) **Check:** All Tier 1 and Tier 2 pages have a functional equivalent in dsh UI. Operator can manage approvals, signals, and view fleet health without oikos-web. ### 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~~ DONE (2026-08-16) **Deleted:** - `cmd/nomos/` — entire directory: agent.go, server.go (the :8092 chat gateway with `/query`, `/chat`, `/sessions` routes), mcp.go, tasks.go, continue.go, workers.go, eval/ runner, plus tests - `nomos/` — SOUL.md, config.yaml, skills/ - `internal/nomos/session/` — the flat store (mirrored by the dsh session-summary plugin writing straight to Postgres) - `internal/nomos/messagequeue/`, `internal/nomos/retrycap/`, `internal/nomos/turngate/` — nomos-only machinery, no remaining importers - `compose/nomos/` Dockerfile + the `nomos` service in docker-compose.yml (profiles now: dev = postgres+api+scheduler, full adds worker+Infisical) - httpapi's `/agent` reverse-proxy mount (`NOMOS_PROXY_URL`) — the only chat-related surface in `internal/httpapi/`; the generated REST API was already chat-free - `evals/*.yaml` — nomos golden-conversation manifests (their only runner was `cmd/nomos/eval`; dsh evals live at `packages/oikos/evals` in the harness workspace) - Script/doc cleanup: deploy.sh image list (+ one-time oikos-nomos image prune), verify-phase6.sh gateway checks, seed-secrets.sh OpenRouter key source (host env now), README/CONTRIBUTING/AGENTS.md/operator-facing comments, compose/caddy/Caddyfile.oikos (`nomos.hubris.network` block and `/agent/*` path removed — mirror in dtoro/caddy-conf), `.golangci.yml` nomos-isolation rules, go.mod (openai-go dropped via `go mod tidy`) **Already gone before this pass:** `compose/web/` (SPA extracted to dtoro/oikos-web), `desktop/` (Wails wrapper, deleted with the SPA split). **Kept (per the stays list):** - ~~`internal/nomos/assent/`~~ — deleted after review: zero importers remained once cmd/nomos was gone (the assent *window* logic lives in `internal/adapters/postgres` — governance.go/approvals.go — behind the governance port and is shared by the dsh consent flow; the orphaned chat-text parser added nothing). `.golangci.yml` nomos rules removed with it, and `go mod tidy` dropped the nomos-only openai-go dependency. - `internal/httpapi/` REST API, `internal/mcp/` (67+ tools), `internal/policy/`, `internal/scheduler/`, `internal/secrets/` - `OIKOS_NOMOS_AGENT_SLUG` config + compose env — resolves the seeded `agent:nomos` entity the MCP handler attributes activity to (dsh sends no agent identity of its own) **Open data item:** `seeds/inventory.yaml` still carries the `agent:nomos` entity and the `nomos_gateway: 8092` port mapping. Left as-is — the DB is the source of truth; retire or rename the entity at runtime (set_entity_state → retired) when dsh gets its own agent entity. ## 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/`~~ done (section 5). The `oikos-web` repo stays until Phase 4 Tier 1-2 land in the dsh UI, then 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. |