Files
oikos/plans/2026-08-16-dsh-as-agent-replace-nomos.md
dtoro 04aa1bd5e8 feat: Phase 3 — MCP header fallback, LIKE-based consent window, and auth token fix for dsh integration
- X-Oikos-Session-Id header → context fallback for _session_id
- sessionIDFromArgsOrContext() reads from args, then header
- AssentWindowActive falls back to session-ID-only LIKE lookup
- windowActiveLike() for LIKE-pattern autonomy_settings queries
- mcpBearerToken: package-level resolved token replaces os.Getenv() in decide_approval
- Bump 0.36.0 → 0.37.0
2026-08-16 16:35:34 +02:00

22 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)

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:
      • readonlyallow (no gate)
      • reversible_lowallow (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.