Replace nomos with dsh (DeepSeek Harness) as the agent runtime per
plans/2026-08-16-dsh-as-agent-replace-nomos.md section 5.
Deleted:
- cmd/nomos/ (agent loop, :8092 chat gateway, task tools, eval runner)
- nomos/ (SOUL.md, config.yaml, skills/)
- internal/nomos/{session,messagequeue,retrycap,turngate,assent}/
- compose/nomos/ Dockerfile + docker-compose nomos service
- httpapi /agent reverse-proxy mount (NOMOS_PROXY_URL)
- evals/*.yaml (nomos golden-conversation manifests)
- compose/caddy/Caddyfile.oikos nomos.hubris.network block + /agent path
- .golangci.yml nomos-isolation rules
- go.mod openai-go dependency (go mod tidy)
Kept:
- internal/adapters/postgres assent-window logic (governance port)
- OIKOS_NOMOS_AGENT_SLUG config (MCP agent entity attribution)
- Full REST API, MCP tools, policy, scheduler, secrets
Cleaned: deploy.sh image prune, verify-phase6.sh checks,
seed-secrets.sh key source, README/CONTRIBUTING/AGENTS.md docs.
Build, vet, test, compose config all pass.
27 KiB
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 (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-clientdiscovery of all 67+ oikos MCP tools@deepseek-ai/dsh-llm-deepseekmodel 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-summaryplugin (packages/oikos/session-summary/)- Mirrors
session/created→agent_sessionsINSERT,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-<n>IDs to oikos UUIDagent_sessions.id
What was deferred:
- Full
session-persistenceseam 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-scopeplugin (packages/oikos/mcp-scope/)tools/pre-executewaterfall: read-only → allow, mutation → check consent window → ask if no windowtools/post-executeauto-approve: detects oikos "requires approval" response → callsdecide_approvalHTTP API → replaces result text- Consent window:
approval/decidedevent withallowed-onceopens 30-min assent window in Postgresautonomy_settings - Auto-expiry cleanup (1-hour interval) for consent/destructive windows
- Go-side fixes:
decide_approvaltoken mismatch,X-Oikos-Session-Idheader 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:
- dsh layer (mcp-scope pre-execute): checks Postgres assent window by session UUID → allows without asking operator again
- oikos layer (Go classifyAndGate): always sees empty
_session_id→ queues execution → post-execute auto-approve detects the queued result and calls the oikosdecide_approvalHTTP 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:
- Phase 1-3 complete
- dsh running at http://127.0.0.1:3080 with all oikos MCP tools
- Consent/approval flow working end-to-end
- dsh-gate: oikos auto-runs non-destructive when no session
- dsh-harness plugin changes committed
- 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-executeask→ 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:
- Register a custom dsh theme via
ctx.theme.register()that overrides alias-layer tokens to match Gruvbox - Key token mappings:
--dsw-alias-brand-primary→#d79921(amber)--dsw-alias-bg-base→#1d2021(dark bg)--dsw-alias-label-primary→#ebdbb2(warm white)
- Fonts: dsh uses its own font system. Override via CSS
font-familyon 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:
- Use native
fetch()with the oikos API base URL (from plugin config or dsh credentials seam) - Wrap in a typed
OikosApiclass (api.ts) with methods for each endpoint - 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:
- Open a single
EventSourceconnection to/api/v1/events/streamon plugin init - Dispatch events to registered listeners (signals, approvals, entity health)
- Auto-reconnect on disconnect (same pattern as oikos-web's
events.tsstore)
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/*orsession/eventfor 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,/sessionsroutes), mcp.go, tasks.go, continue.go, workers.go, eval/ runner, plus testsnomos/— 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 importerscompose/nomos/Dockerfile + thenomosservice in docker-compose.yml (profiles now: dev = postgres+api+scheduler, full adds worker+Infisical)- httpapi's
/agentreverse-proxy mount (NOMOS_PROXY_URL) — the only chat-related surface ininternal/httpapi/; the generated REST API was already chat-free evals/*.yaml— nomos golden-conversation manifests (their only runner wascmd/nomos/eval; dsh evals live atpackages/oikos/evalsin 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.networkblock and/agent/*path removed — mirror in dtoro/caddy-conf),.golangci.ymlnomos-isolation rules, go.mod (openai-go dropped viago 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):
— deleted after review: zero importers remained once cmd/nomos was gone (the assent window logic lives ininternal/nomos/assent/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.ymlnomos rules removed with it, andgo mod tidydropped the nomos-only openai-go dependency.internal/httpapi/REST API,internal/mcp/(67+ tools),internal/policy/,internal/scheduler/,internal/secrets/OIKOS_NOMOS_AGENT_SLUGconfig + compose env — resolves the seededagent:nomosentity 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:
- Deploy dsh alongside nomos — both agent runtimes run in parallel during development.
compose/dsh/joins the docker-compose stack. - 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. - 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
/legacyduring the transition. - Cleanup —
removedone (section 5). Thecmd/nomos/,compose/web/oikos-webrepo 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. |