Files
oikos/plans/2026-08-16-dsh-as-agent-replace-nomos.md
dtoro 22fe1526ca
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
feat: add /deploy-plugins webhook route + deploy-plugins script
2026-08-17 00:05:55 +02:00

26 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: executionssession_plan_stepsagent_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/createdagent_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-<n> 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:

  • 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-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, verify-phase6.sh gateway checks, seed-secrets.sh OpenRouter key source (host env now), README/CONTRIBUTING/ AGENTS.md/operator-facing comments

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/ — chat-assent/typed-confirmation parsing; 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
  • 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. Cleanupremove 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.