Four cross-linked documents under docs/mbse/, structured after Jon Holt's Systems Engineering Demystified (2nd ed.): Framework = Ontology + Viewpoints, producing a Model made of Views. - framework.md — the Ontology (SE meta-concepts + Oikos's domain ontology) and an 11-entry Viewpoint catalog (two repeating: Component, Ontology). - README.md — the Model's 9 concern-based Views (mission, requirements, functional/physical architecture, interfaces, behavior, V&V, risk, roadmap). - components.md — 8 per-component Views going one layer deeper into each running part of the system's own internal structure. - ontology.md — 4 Views on the domain ontology itself: entity type hierarchy (split into 9 digestible per-domain diagrams), full relationship catalog, lifecycle state machines with their requires: gates, and concrete population. Grounded in direct verification against source (grep/read), not just existing docs — every finding is graded verified vs. per-research-pass. Surfaced several real, previously undocumented findings along the way: the policy kill-switch (global.auto_act/never_auto_act) is checked only by dead code and an unstarted actuator package, so it doesn't gate the live run path; internal/actuator and internal/learning are compiled but never started by any process; the relationship catalog grew from 34 to 47 types since ADR-0014; and task has no registered lifecycle_defs entry despite having a documented, code-enforced state machine. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
27 KiB
Oikos — Component Views
Companion to the Model and the Framework. Where README.md's nine Views cut across the whole system by concern (requirements, behavior, risk...), this document cuts across it by component — one View per running part of the System, going one layer deeper into its own internal structure than the whole-system Views do. Per framework.md §4, each section below is still a View and must still answer Holt's three questions; they're stated once per section rather than as a separate table, since here the Stakeholder is almost always the same ("an engineer about to change this component") and the Notation is the same (prose + Mermaid) throughout.
How to use this alongside the other two documents: if you're deciding whether something belongs in the Model, read framework.md. If you're asking what does the system do and why, read README.md. If you're about to change code in a specific package and want to know its internal shape, its own state, and what's already known to be broken or dormant inside it before you touch it, read the relevant section here.
Contents
| Component | Path | Status |
|---|---|---|
| 1. oikos api | internal/httpapi, internal/mcp, internal/policy |
✅ live — the decision/execution gate |
| 2. oikos scheduler | internal/scheduler, internal/checkdefaults |
✅ live — the observe loop |
| 3. oikos notifier | internal/notifier |
✅ live — approval delivery |
| 4. nomos | cmd/nomos |
✅ live — the agent, unauthenticated gateway |
| 5. web control room | web/src |
✅ live — standalone SPA |
| 6. PostgreSQL/TimescaleDB | migrations/, seeds/ |
✅ live — the System's own source of truth |
| 7. Dormant components | internal/actuator, internal/learning |
🔴 compiled, never started |
| 8. Auxiliary components | cmd/webhook, cmd/desktop |
✅ live — deploy + packaging, not decision logic |
1. oikos api
Stakeholders: engineers extending the MCP tool surface, the run gate,
or REST endpoints; anyone debugging why a specific command was or wasn't
classified the way they expected. Why this View earns its place: this
is the single component where the highest-consequence findings in
Risk & Safety live — extending it without
knowing its internal shape is how the kill-switch gap and the
two-classifiers problem happened in the first place.
oikos api — Internal structure
| File | Lines | Role |
|---|---|---|
internal/httpapi/server.go |
842 | NewHandler (routing entry, L75), combinedAuth (L229), OIDC JWKS discovery/fetch/validate (L337-524), GetActor (L526), OIDC config/token/callback handlers (L575-712), ListenAndServe (L811) |
internal/httpapi/impl.go |
1,639 | Entity CRUD, lifecycle transitions + preconditions (per ADR-0014) |
internal/httpapi/phase3.go |
2,627 | Executions, approvals (DecideApproval), sshExec, executeApprovedAction, autonomy-settings read/write endpoints — the largest single file in the component |
internal/httpapi/sse.go |
366 | LISTEN/NOTIFY fan-out, ring-buffer replay |
internal/httpapi/activity.go |
215 | agent_activity read endpoints |
internal/httpapi/knowledge.go |
286 | Knowledge search/content endpoints |
internal/httpapi/dashboard.go |
172 | dashboard/summary |
internal/httpapi/learning_view.go |
129 | learning/timeline, learning/trend |
internal/httpapi/problem.go |
71 | RFC 9457 problem+json error envelope |
internal/httpapi/default_checks.go |
13 | Thin wrapper calling internal/checkdefaults on entity creation |
internal/mcp/server.go |
1,691 | All 33 MCP tool registrations (get_entity at L76 through list_my_secrets at L753), sshExec (L1031), resolveExecTarget (L1223), classifyAndGate (L1264-1417) |
internal/policy/command.go |
174 | ClassifyCommand (L108) — the live classifier, computeCommandRisk (L127), allSegmentsReadOnly (L157), riskRank (L26) |
internal/policy/classify.go |
157 | ClassifySignal (L46) — dead code, zero callers (see Roadmap §9.2) |
oikos api — internal call structure: the run gate, by file
README.md's §3.3 shows the decision
logic of the run gate. This shows the code path — which file hands off
to which — because they're not the same question: the decision flowchart
tells you what happens, this tells you where to go fix it.
flowchart LR
MCP["mcp/server.go\nrun tool handler, L366"] --> GATE["mcp/server.go\nclassifyAndGate, L1264"]
GATE --> RESOLVE["mcp/server.go\nresolveExecTarget, L1223"]
GATE --> CLASSIFY["policy/command.go\nClassifyCommand, L108"]
CLASSIFY --> RISK["policy/command.go\ncomputeCommandRisk, L127\nallSegmentsReadOnly, L157"]
GATE -->|read_only or window open| EXEC["mcp/server.go\nsshExec, L1031"]
GATE -->|otherwise| APPROVAL["phase3.go\ncreateApproval"]
APPROVAL -->|operator decides| DECIDE["phase3.go\nDecideApproval"]
DECIDE --> EXEC2["phase3.go\nsshExec\n(separate implementation)"]
style CLASSIFY fill:#e8f5e9,stroke:#2e7d32
style EXEC fill:#fff3e0,stroke:#e65100
style EXEC2 fill:#fff3e0,stroke:#e65100
The two orange boxes are the same finding stated visually: mcp/server.go
and phase3.go each have their own sshExec, independently written,
not sharing an implementation. Fix one path's SSH handling and the other is
untouched — verified during the Roadmap audit, not assumed.
oikos api — Interfaces this component owns
Full catalogs live in README.md §5 (33 MCP
tools, REST groups, SSE event types) — not repeated here. What's specific
to this component's internal ownership: internal/mcp/server.go owns
every MCP tool; internal/httpapi/{impl,phase3,sse,activity,knowledge, dashboard,learning_view}.go own the REST surface between them, split by
resource area rather than by file size; internal/policy/command.go is a
pure function library with no HTTP surface of its own, called only from
classifyAndGate.
oikos api — Status and known issues
All of the following are detailed with evidence in Roadmap & Traceability and Risk & Safety — cross-referenced here so an engineer opening this specific package sees them before making a change, not after:
policy.ClassifySignal(in this component) is dead code; the schema it reads (autonomy_settings.global.auto_act,never_auto_act.*) is therefore not enforced by anything in the live request path — §8.1.notifier.VerifyApprovalToken(a different component, §3 below) is dead;phase3.go:DecideApprovalreimplements token verification inline instead of calling it.domain.Execution's state constants are descriptive only —phase3.gowrites ad-hoc SQL string statuses that don't map 1:1 onto them.- SSH host key verification is disabled (
InsecureIgnoreHostKey) on the actuation path — open gap B4.
2. oikos scheduler
Stakeholders: engineers adding a new probe kind or debugging why a signal did or didn't fire. Why this View earns its place: the scheduler is the only component that runs unattended on a fixed interval with no operator or agent triggering it — its failure modes look different from every request-driven component above.
oikos scheduler — Internal structure
| File | Lines | Role |
|---|---|---|
internal/scheduler/scheduler.go |
761 | Everything — no sub-packages |
internal/scheduler/init.go |
13 | RunnerForMain() — the only thing cmd/oikos's scheduler role calls |
internal/checkdefaults/defaults.go |
— | ForEntityType (L60-123), Ensure (L144-204), DefaultInterval (L133-142) — default check provisioning on entity creation |
Key functions inside scheduler.go: Run (L36-64, the tick loop, default
30s), runCheckPass (L67-94, loads check_defs, dispatches with a
10-worker errgroup limit), runCheck (L104-186), executeCheck (L237-254,
the kind dispatcher), resolveSignal (L206-225), evaluateSeverity
(L737-760), staleSweep (L286-333, 3× fastest interval / 5 min floor).
oikos scheduler — behavior specific to this component: the probe dispatch
flowchart TB
TICK["Run tick, every 30s"] --> LOAD["ListEnabledCheckDefs"]
LOAD --> DISPATCH["executeCheck: dispatch by kind"]
DISPATCH --> HTTP["checkHTTP, L336"]
DISPATCH --> TCP["checkTCP, L393"]
DISPATCH --> DISK["checkDisk, L424"]
DISPATCH --> CERT["checkCertExpiry, L474"]
DISPATCH --> PING["checkPing, L545"]
DISPATCH --> SSH["checkSSHScript, L613"]
HTTP & TCP & DISK & CERT & PING & SSH --> RESULT["checkResult struct\nhealth, signalKind, evidence, metrics"]
RESULT -->|healthy| RESOLVE["resolveSignal\nraw SQL, bypasses Signal.CanTransition"]
RESULT -->|unhealthy| UPSERT["UpsertSignal\ndedup by target+kind"]
RESULT --> METRICS["INSERT metric_samples"]
RESULT --> STATUS["UpsertEntityStatus"]
checkSSHScript (L613-724) is the odd one out: it shells out to the system
ssh binary directly (BatchMode=yes, StrictHostKeyChecking=no) rather
than using a Go SSH library, restricted to scripts matching
^[a-z][a-z0-9_-]+\.sh$ at a fixed path /opt/oikos/checks/<script>. The
18 scripts it can run (cpu_check.sh, disk_usage_check.sh,
docker_health_check.sh, zfs_check.sh, …) live in checks/ in this repo
and are auto-deployed to every enrolled client by tools/setup-checks.sh
(per AGENTS.md §8) — this component's actual probe logic is split between
Go code here and shell scripts version-controlled elsewhere in the repo.
oikos scheduler — Interfaces this component owns
No external API — this is the one component with no inbound interface at
all, only outbound: SSH to the fleet (probes), and writes to
metric_samples/signals/entity_status/events that every other
component reads. It is a pure producer.
oikos scheduler — Status and known issues
- Never calls
policy.ClassifySignal— signals it raises sit asstate='raised'with no automatic classification; whatever consumes them downstream (the agent, the console) does its own interpretation. resolveSignalupdatesraised → resolvedvia raw SQL, bypassing the one enforced state machine in the domain layer (domain.Signal.CanTransition) — the specific transition happens to be legal today, but nothing would stop a future change from making it not.
3. oikos notifier
Stakeholders: engineers debugging a missed or duplicate Matrix alert, or extending the approval-delivery mechanism to a new channel. Why this View earns its place: this is the one component whose entire job is bridging an asynchronous human decision into the same-shaped synchronous decision every other component expects — worth understanding in isolation before assuming "approval" means one simple thing.
oikos notifier — Internal structure
All in internal/notifier/notifier.go (305 lines, one file, no
sub-packages): Run (L25-47, two tickers — 15s for pending approvals, 30s
for reaction polling), processPendingApprovals (L65-115, generates the
token/hash lazily on first pass), generateApprovalToken (L275-283,
HMAC-SHA256 over approval ID + nanosecond timestamp), hashToken
(L302-305, only the hash is stored), sendMatrixAlert (L231-272),
pollReactions/checkReaction (L118-201), callDecideApproval
(L204-228), VerifyApprovalToken (L286-300, dead code).
oikos notifier — Behavior specific to this component
The full sequence (Matrix + console paths converging on one decision
endpoint) is in README.md §6.4.
Specific to this component in isolation: it never calls into
internal/httpapi directly except through one HTTP call
(callDecideApproval, an ordinary client request to
POST /api/v1/approvals/{id}/decision) — the notifier and the API process
communicate only through the database and one HTTP endpoint, never
through shared Go state, which is why the header comment in notifier.go
calls this a "DB rendezvous pattern."
oikos notifier — Interfaces this component owns
Outbound only: the Matrix client-server API
(PUT /rooms/.../send/m.room.message, GET /relations/.../m.annotation)
and one outbound call to the API's own approval-decision endpoint. No
inbound interface — nothing calls into the notifier process.
oikos notifier — Status and known issues
VerifyApprovalTokenis dead code;phase3.go:DecideApproval(a different component, §1 above) reimplements the same hash-compare logic inline rather than calling it — a single source of truth for token verification does not currently exist.- Open gap A2:
alert_sent_atis written after the send attempt, so a failed UPDATE re-sends the alert on the next poll; no dedup beyond that, and reaction-polling API calls are unbounded.
4. nomos (agent gateway)
Stakeholders: engineers changing agent behavior, adding a task tool, or
investigating a stuck/duplicated task. Why this View earns its place:
this is the largest component by line count (4,681 lines across six files)
and the one with the most active recent bug-fix history
(plans/2026-07-11-nomos-agent-code-review.md,
plans/done/2026-07-14-post-fix-session-remainders.md) — its internal
shape is not obvious from outside.
nomos — Internal structure
| File | Lines | Role |
|---|---|---|
cmd/nomos/main.go |
914 | Gateway HTTP server (:8092), /query//chat//sessions routes, the hand-rolled Streamable-HTTP MCP client (mcpClient, per-session pooled) |
cmd/nomos/store.go |
1,472 | Persistence — sessions, messages, logActivity |
cmd/nomos/agent.go |
861 | The agentic loop itself; model config (L62-120); maxIterations = 40 (L24, a hard-coded constant, not read from nomos/config.yaml's max_iterations: 15 — the two disagree, see status below) |
cmd/nomos/tasks.go |
416 | The five nomos-local task tools: set_goal, propose_plan, update_plan_step, ask_operator, complete_task — handled in-process, never forwarded to internal/mcp |
cmd/nomos/continue.go |
347 | The auto-continuation worker — polls nomos_plan_executions |
cmd/nomos/assent.go |
183 | isAssent/isTypedConfirmation — regex word-boundary matching (fixed 2026-07-11 after a false-positive bug where "yesterday" matched "yes") |
nomos — Behavior specific to this component
The Task lifecycle and auto-continuation sequences are in
README.md §3.4
and §6.5.
Specific to this component: the LLM sees a union of two tool sources —
the 33 tools fetched live from api's /mcp endpoint via tools/list,
plus the 5 local task tools in tasks.go — and agent.go's per-call
routing decides in-process versus forwarded with no visible seam to the
model itself. A hidden _session_id is injected into forwarded calls on
the wire (never in the model-visible arguments) so internal/mcp/server.go
can scope assent/destructive windows per task.
nomos — Interfaces this component owns
| Route | Auth |
|---|---|
GET /healthz |
none |
POST /query (structured tool call or a pointer to /chat) |
none |
POST /chat (SSE, the real agentic loop) |
none |
GET/POST /sessions, /sessions/{id} |
none |
This entire interface is unauthenticated — full detail in
README.md §5.4.
Outbound: a pooled MCP client to api, and chat completions to OpenRouter
(data_collection: deny pinned, default model deepseek/deepseek-v4-pro).
nomos — Status and known issues
- C1, the most consequential open gap involving this component: zero authentication on the entire gateway, including the ability to grant chat-assent approvals with no credential check. Explicitly deferred by operator instruction, not an oversight — see README.md §8.4.
nomos/config.yaml'smax_iterations: 15does not match the enforced Go constant (40) — one of the two is stale.- Dual
agent_activitylogging: both this component'sstore.logActivityandinternal/mcp/server.go'swithActivityLogging(a different component) log the same forwarded tool call. Not confirmed whether this is an intentional two-sided audit trail or accidental duplication.
5. web control room
Stakeholders: the operator, directly; engineers changing the UI's data model or adding a page. Why this View earns its place: this is the only component with no server-side logic of its own — understanding it means understanding what it doesn't do (it is not the system of record for anything) as much as what it does.
web control room — Internal structure
Nine pages under web/src/pages/ (Svelte 5, hash-based routing, no router
library):
| Page | Lines | Shows |
|---|---|---|
Overview.svelte |
262 | Task dashboard — fleet/health/signal summary cards, the task list, entry point for launching a new chat |
Chat.svelte |
410 | The conversation UI — streaming, TaskContextPanel, tool-call rendering, inline approvals |
Ops.svelte |
240 | Approvals queue, recent activity/execution feed, approve/deny/cancel |
Signals.svelte |
171 | Alert/signal triage — severity filter, ack/resolve/mute |
KnowledgeBase.svelte |
263 | Entity browser — force-directed graph view and table view |
Knowledge.svelte |
186 | Free-text knowledge search |
Learning.svelte |
174 | Pattern/skill telemetry — the one page whose backing data source |
(internal/learning) is dormant per §7 below, so this page currently shows |
||
| whatever accumulated before the engine stopped being called, not a live | ||
| feed | ||
Config.svelte |
202 | Auth/connection screen — static bearer token or OIDC login |
EntityDetail.svelte |
7 | Thin wrapper, deep-link target |
Shared logic under web/src/lib/: config.ts (fetchWithAuth, the single
wrapper every API call goes through), oidc.ts, api.ts, tasks.ts,
stores/events.ts (the always-on SSE connection), plus task-specific
components (TaskContextPanel.svelte, GoalHeader.svelte,
PlanProgress.svelte, OperatorQuestion.svelte, SessionGraph.svelte).
web control room — Behavior specific to this component
Two data-flow patterns, not one: most pages fetch REST on mount and
re-fetch on a relevant SSE event; Chat.svelte's TaskContextPanel is
driven by the always-on global event stream
(web/src/lib/stores/events.ts), not the per-turn chat SSE connection —
deliberately, so the live context panel stays populated during
server-side auto-continuation (§4 above) when no chat turn is actually
open, and survives a tab reload.
web control room — Interfaces this component owns
None inbound — it is a pure consumer of internal/httpapi's REST and SSE
interfaces (full catalog: README.md §5).
fetchWithAuth resolves config per request rather than at import time, so
the same build works same-origin (production, Vite dev proxy) or
cross-origin (the Wails desktop webview, §8).
web control room — Status and known issues
Standalone deploy, versioned and released independently of the oikos
binary — see README.md §4.5 for
why "deployed" means two different release cadences depending on whether
you mean the container or the desktop app.
6. PostgreSQL/TimescaleDB
Stakeholders: anyone writing a migration, or reasoning about what "the system's source of truth" actually means (see framework.md §2 for why that phrase needs disambiguating from the engineering Model). Why this View earns its place: every other component in this document either reads from or writes to this one; it is the only component every other component has in common.
PostgreSQL/TimescaleDB — Internal structure
20 forward-only, idempotent migrations
(001_ontology.up.sql … 020_session_reliability.up.sql,
ADR-0008). Four TimescaleDB
hypertables, all created in 006_observability.up.sql:
metric_samples, audit_log, events, agent_activity — each with
continuous aggregates and retention policies.
The recurring structural pattern across this schema, per
ADR-0014 §7: dual entities —
check_defs, signals, classifications, executions, feedback,
patterns, skills, approvals, knowledge_entities, and (as of
migration 018) agent_sessions-as-task all have an
entity_id UUID PK REFERENCES entities(id), meaning every specialized row
is simultaneously a node in the general entity graph — this is what lets
get_relations/get_blast_radius work uniformly over signals, tasks, and
infrastructure alike without a special case for each.
Partial unique indexes provide snapshot semantics without an application-
level lock: relationships (current edges only), signals (one open
signal per entity+kind), patterns (one per type+action).
PostgreSQL/TimescaleDB — Behavior specific to this component
008_event_notify.up.sql's pg_notify trigger on events INSERT is the
entire mechanism behind README.md's SSE interface
— the database, not the API process, is what decides an event happened;
the API process is just a fan-out listener.
PostgreSQL/TimescaleDB — Interfaces this component owns
Every Go component in this document connects directly (via sqlc-generated
queries, internal/db) — there is no ORM abstraction layer and no
component-specific access restriction beyond what each service's own
Postgres role grants (notably: the learning engine's DB role has no grants
on governance/autonomy tables, per
ADR-0006 — a structural guarantee
that would matter the moment §7's dormant learning engine is wired back in).
seeds/*.yaml + oikos seed/oikos export form the bootstrap/DR
interface — the database can be regenerated from seeds, and seeds can be
regenerated from the database.
PostgreSQL/TimescaleDB — Status and known issues
A shared-Postgres single point of failure across every service is a documented residual risk in ADR-0007, not a newly discovered one.
7. Dormant components
Stakeholders: anyone deciding whether to revive auto-act, or tempted to
extend internal/actuator/internal/learning believing them to be the
live implementation. Why this View earns its place: these are the two
components most likely to mislead an engineer navigating by package name —
both are substantial, well-written, and compile cleanly into the oikos
binary, and neither runs.
Dormant components — Internal structure
| File | Lines | Role, and why it's dormant |
|---|---|---|
internal/actuator/actuator.go |
505 | Run (L24-41, 10s ticker), processAutoActSignals, kill-switch checks (L47, L69 — getAutonomySetting for global.auto_act and never_auto_act.<slug>), a real circuit breaker (L156-202, threshold + cooldown), advisory locking (L87-99) — but the executor itself is a hardcoded stub, {"success": true, "message": "stub execution"} (L124-137), and Run() is never called by cmd/oikos/main.go or any docker-compose.yml service |
internal/actuator/ssh.go |
291 | ExecuteProcedure (L126-230) — a fully-built step-by-step SSH runner with classifySSHError (L77-108: network/auth/timeout/remote), per-step timeouts, verify-step semantics. Zero callers anywhere in the codebase. |
internal/learning/learning.go |
183 | Run (L20-41, hourly ticker), extractPatterns (L45-81), processGroup (L83-169, Wilson lower-bound confidence, evidence≥5 ∧ confidence≥0.7 → validated, anomaly quarantine at >10 same-key events per pass) — algorithmically faithful to ADR-0006, never started by any process |
Why this matters more than "unused code"
internal/actuator/actuator.go is where the policy kill-switch
(global.auto_act, never_auto_act.*) is actually checked in Go — the
only other place is the dead policy.ClassifySignal. Reviving auto-act
and fixing the kill-switch gap
(README.md §8.1)
are, structurally, the same piece of work — whoever picks up
internal/actuator/actuator.go:47-69 is the person who also resolves
REQ-DEC-5. This is stated explicitly here because it is not obvious from
reading the Risk & Safety or Roadmap views in isolation; it only becomes
visible once you've read this component's code.
Dormant components — Status and known issues
This entire section is the known issue — see
README.md §9.1
("revive auto-act," the one item the general-gated-execution plan's own
header still marks open) and
§9.4
item 1 and 3 for the two decisions this leaves open: wire these back in, or
delete them and correct the "Phase 3 — DONE" claim in
OIKOS.md/ADR-0014 that currently overstates what's running.
8. Auxiliary components
Two small, single-purpose components that support deployment and packaging rather than decision logic — given lighter treatment here deliberately, since no Stakeholder identified in this document's other sections needs their internals to make a change elsewhere.
cmd/webhook (96 lines, one file) — the Gitea push-to-deploy receiver.
Verifies X-Hub-Signature-256 HMAC-SHA256 against WEBHOOK_HMAC_SECRET
using hmac.Equal, responds 202 immediately, then runs
scripts/deploy.sh asynchronously. Full deploy sequence:
README.md §4.2
and §7.2.
cmd/desktop (784 lines) — the Wails-wrapped desktop shell. Bundles
web/dist into a native binary, adds an OIDC login flow through a local
HTTP server + system browser, and a SaveConfig bridge the web bundle
calls when running inside the desktop webview (web/src/lib/config.ts's
saveToDesktop()). Contains no decision logic of its own — it is
packaging for component 5 (§5 above), not a new component in the
functional sense.
Keeping this document current
The same discipline as README.md's closing note applies here, scoped to components: when a file listed in a "Internal structure" table is renamed, split, or gains a new responsibility, update that row. When a "Status and known issues" bullet is resolved, remove it — and check whether removing it also resolves an entry in README.md §9, since most of the findings here were first surfaced there and are repeated in this document for proximity to the code, not because they're independently tracked in two places.