CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m22s
docs/Development-Plan.md: where the code stands against each section of docs/Architecture.md, the environment today, how we work (plan-first per milestone, tests, the Quint model, a bring-up run on this box ending each phase), and seven phases of milestones from housekeeping to nisse-ops, each marked pre-approved or needs discussion. Every open issue is mapped to a milestone or marked superseded; an old-to-new section table serves the reference update in M0.1. docs/Architecture.md: status now agreed; D7 regains the browser tool for the internal test network, left out of the rewrite by mistake. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
1006 lines
53 KiB
Markdown
1006 lines
53 KiB
Markdown
# Nisse — Architecture
|
||
|
||
**Status: agreed (PR #83).** How and in what order it gets built is in
|
||
`docs/Development-Plan.md`. Until that plan's first milestone retires
|
||
`docs/Architecture-Build-Spec.md`, code comments still cite the old spec's section
|
||
numbers (A1–A18, B, C, F). This document uses different letters (P, L, D, O, X, Q)
|
||
so the two can't be confused meanwhile.
|
||
|
||
This document says what Nisse is, how its parts fit together, and why. It records
|
||
decisions and the reasons for them. It doesn't record status or sequencing.
|
||
|
||
---
|
||
|
||
## Naming
|
||
|
||
The project is **Nisse**, after the Nordic farm spirit that keeps the place running
|
||
overnight and expects to be treated well in return.
|
||
|
||
| Artifact | Name | Role |
|
||
|---|---|---|
|
||
| Shared library | `nisse-core` | What every agent is built on (Part L) |
|
||
| Dev Agent | `nisse-dev` | Designs and builds software projects (Part D) |
|
||
| Sysadmin Agent | `nisse-ops` | Operates infrastructure (Part O) |
|
||
|
||
More agents are planned (for example 3D design work and video-file management).
|
||
Each is another application over `nisse-core`.
|
||
|
||
**Dev Agent** and **Sysadmin Agent** name the *roles* in discussion; `nisse-dev` and
|
||
`nisse-ops` name the *artifacts*: packages, container images, VM hostnames, Matrix
|
||
identities, service accounts. One string is used for all of an agent's artifacts,
|
||
so grepping the infrastructure for `nisse-ops` finds everything.
|
||
|
||
**Sites** are identified by their domain stem: `garrett-hall` (hosts under
|
||
`garrett-hall.com`) and `hartman-hut` (hosts under `hartman-hut.com`). As with
|
||
agents, one string is used everywhere a site appears, including MCP server
|
||
instances (X3).
|
||
|
||
All artifacts live as packages in **one repository, `nisse`**. The repository
|
||
boundary was never a trust boundary in this design (the VM is, X1); a monorepo
|
||
avoids tripled admin for a solo operator and keeps `nisse-core` easy to change
|
||
while its seams are still settling.
|
||
|
||
---
|
||
|
||
## Part P — Principles
|
||
|
||
### P1. What Nisse is
|
||
|
||
Nisse is a set of self-hosted agents that **do real work and talk about it**. Each
|
||
agent is a collaborator the operator converses with in Matrix: it explains what it
|
||
is doing and why, answers questions about its plans and results, asks when it is
|
||
unsure, and summarises what it did. It is not a command interpreter behind a menu
|
||
of keywords.
|
||
|
||
The agents run **local-first** (P7): a model on the operator's own inference
|
||
hardware does the work, and frontier models are called in when it needs help.
|
||
|
||
The agents share one library, `nisse-core` (Part L). Each agent adds what is
|
||
specific to its job: `nisse-dev` works on software projects in a sealed VM
|
||
(Part D); `nisse-ops` operates live infrastructure across two sites (Part O).
|
||
|
||
### P2. Threat model
|
||
|
||
The checks in this design exist to stop **mistakes and manipulation** from doing
|
||
damage. They are not built around a model that schemes against the operator.
|
||
In order of how often they matter:
|
||
|
||
1. **Honest mistakes.** A wrong command, a misread instruction, deleting the wrong
|
||
thing, confidently wrong code, an action taken on a stale belief. By far the
|
||
most common.
|
||
2. **Prompt injection.** An agent reads untrusted content: issue and comment text,
|
||
repository files, dependencies, web pages, alert payloads, log lines. That
|
||
content can steer a well-meaning model into acting as if it were malicious. No
|
||
model reliably tells instructions from data (L11).
|
||
3. **Crashes and environment surprises.** A process dies mid-action, a host
|
||
reboots, a workspace is stale or corrupt, a tool is missing.
|
||
|
||
Each calls for something different:
|
||
- **Mistakes** call for reversibility, visibility and a second look before
|
||
anything irreversible (P3, P5).
|
||
- **Injection** calls for a small number of hard boundaries exactly where
|
||
untrusted input meets real power (P4): a steered model can't be trusted to ask
|
||
first.
|
||
- **Crashes** call for durable state and idempotent effects (L8).
|
||
|
||
An agent's *intentions* are never the thing a check relies on, in either
|
||
direction: a check that assumes the model is honest fails under injection, and a
|
||
workflow built around containing a hostile model makes the agent useless.
|
||
|
||
### P3. Actions by their effects
|
||
|
||
Every action any agent can take falls into one of three classes:
|
||
|
||
| Class | What it is | Examples | Rule |
|
||
|---|---|---|---|
|
||
| **1. No side effects** | Observes; changes nothing outside a disposable sandbox | Reading files, querying APIs, searching, viewing logs and metrics, running a build or tests in a throwaway environment | Always allowed |
|
||
| **2. Contained side effects** | Changes something, but the change stays within the agent's own area or has a known undo | Editing the workspace, committing to the agent's branch, opening a PR, writing the agent's own notes, starting a test container; for ops, a step with a known undo inside an approved plan | Allowed when in scope; logged and reported |
|
||
| **3. Dangerous** | Irreversible, visible beyond the agent's own area, or outside what was agreed | Merging to a default branch, deleting data, changing a firewall, installing software on a host, contacting third parties, spending beyond a budget | Needs discussion, unless an approved plan names the exact action |
|
||
|
||
**"In scope"** means within what the operator agreed to: the work item's task,
|
||
an approved plan, a pre-approved milestone (D2), or an approved step sequence
|
||
(O2). An action can be class 2 in one work item and class 3 in another.
|
||
|
||
**Move actions down the classes wherever it is cheap.** Reversibility is designed
|
||
in, not hoped for (P5).
|
||
|
||
### P4. Three layers of control
|
||
|
||
1. **Hard walls, outside any model.** Things no model conclusion can change:
|
||
credentials the agent doesn't hold, network destinations it can't reach
|
||
(X2), branches it can't push to or merge into, budget caps, the kill switch,
|
||
and durable, idempotent external effects (L8). These hold even if every model
|
||
in the system is fooled.
|
||
2. **A deterministic fast path.** Class-1 actions, and class-2 actions a static
|
||
rule already recognises as in scope, proceed without any model being asked.
|
||
Most actions take this path.
|
||
3. **The action classifier** (L4) for everything else: an independent model call
|
||
that sorts an action into *safe*, *dangerous but in scope*, or *dangerous,
|
||
needs discussion*, judging the action against the agreed scope.
|
||
|
||
**Conversation sits above all three.** When anything needs discussion, the agent
|
||
asks, in plain language, and waits.
|
||
|
||
The classifier is a filter for mistakes and injection, not a guarantee. Anything
|
||
truly irreversible also sits behind a hard wall.
|
||
|
||
### P5. Reversibility first
|
||
|
||
- Work happens on **branches**, never directly on what others depend on.
|
||
- A task starts from a **fresh, clean workspace** (D3), so a stale or broken
|
||
environment is replaced rather than diagnosed.
|
||
- Durable state is **snapshotted** (X4), and snapshots are taken before risky
|
||
changes where the platform allows it.
|
||
- Where a tool offers a **dry run** or a preview, the agent uses it before the
|
||
real action.
|
||
- Build artifacts and caches are **disposable**; nothing depends on them.
|
||
|
||
### P6. Structural, not model judgment
|
||
|
||
Anything that safety or recovery depends on is enforced in code, never left to a
|
||
model noticing. A good outcome that only happened because a model behaved well is
|
||
a bug to fix. Examples from building `nisse-dev`: adopting an already-open PR after
|
||
a crash (L8), redelivering an interrupted review comment (L8), binding approvals to
|
||
the plan they were about (L3), never publishing build artifacts (D4).
|
||
|
||
Models are trusted outright only where a wrong choice costs time rather than
|
||
safety, such as choosing which ready work item to switch to (L1).
|
||
|
||
### P7. Local first, frontier on call
|
||
|
||
- **A local model drives every step:** conversation, planning, implementation and
|
||
review. It runs on the operator's own inference host, behind LiteLLM (L9).
|
||
- **Frontier models are helpers the driver calls:** a quick question
|
||
(`ask_expert`), or taking over a scoped sub-task (delegation). Both are
|
||
described in L9.
|
||
- **The driver is prompted to ask for help when it isn't confident,** and told
|
||
that doing so is expected, not a failure. Because small models are poorly
|
||
calibrated about their own competence, **observable triggers** escalate too:
|
||
repeated failures and hitting caps (L9). Escalation never depends on the model's
|
||
self-assessment alone.
|
||
- **This is an experiment with a cheap exit.** The driver is a role in
|
||
configuration. If local models fall short at some kind of work, a frontier model
|
||
drives that work instead: a configuration change and prompt tuning, not a
|
||
redesign.
|
||
|
||
### P8. Carried principles
|
||
|
||
- **Isolation matched to blast radius.** `nisse-dev` is disposable and sealed;
|
||
`nisse-ops` holds live-infrastructure reach. They never share a running
|
||
instance, and every agent gets its own VM (X1).
|
||
- **Stateless images, stateful volumes.** Containers are replaceable; durable
|
||
state lives on volumes that survive redeploys and unexpected reboots.
|
||
- **Structured channels for machines, prose channels for humans.** Machine intake
|
||
(Gitea, Vikunja, alerts) is structured JSON, never routed through a chat bridge.
|
||
- **Own the stack.** Everything self-hostable stays inside the network where
|
||
practical.
|
||
- **Nothing remotely held is load-bearing.** No provider holds state the system
|
||
needs to resume, and Matrix room history is never a source of truth.
|
||
|
||
---
|
||
|
||
## Part L — The shared library (`nisse-core`)
|
||
|
||
Everything in this part is used by every agent.
|
||
|
||
### L1. The work model
|
||
|
||
Every agent's work is organised into three kinds of unit.
|
||
|
||
| Concept | What it is | Lifetime | `nisse-dev` | `nisse-ops` |
|
||
|---|---|---|---|---|
|
||
| **Duty** | A standing responsibility that produces work items | Recurring | None yet; the operator is the only source | Scheduled checks, log monitoring, alert intake |
|
||
| **Work item** | One effort, opened and eventually closed | Opened → closed | Building a feature in a repo | Diagnosing a failure; making one change |
|
||
| **Scope** | What a work item operates on, and the context that persists across work items there | Indefinite | A repo | A site and service, e.g. `garrett-hall` Proxmox |
|
||
|
||
- **A duty produces work items; it is not one.** A duty run is read-only and
|
||
yields findings; a finding that needs attention becomes a work item. Duties
|
||
deduplicate: a later finding for the same problem attaches to the open work item
|
||
instead of opening a second (a stable fingerprint per finding).
|
||
- **The operator is always a source** of work items.
|
||
- **A work item is the unit of state:** its conversation, plans and approvals,
|
||
pending questions, pinned role mapping (L9) and transcript (L7) all belong to it.
|
||
Its ID is minted locally when it opens and doubles as its correlation ID in logs.
|
||
- **An external record is optional and linked:** a Gitea issue for dev work, a
|
||
Vikunja ticket for ops. When one is linked, external truth wins on divergence
|
||
(L8).
|
||
- **Work items don't nest.** A milestone is a phase within a dev work item; a
|
||
delegation is an episode within one.
|
||
- **Scopes hold context shared across work items** (L6): for a repo, its documents
|
||
and conventions; for infrastructure, the world model of that site or service.
|
||
|
||
**Park-and-switch: one active work item per agent.**
|
||
- Other work items are *parked*, with their full position held durably.
|
||
- **Switching happens only at safe points,** when the active item is waiting on
|
||
the operator or has closed, and never while intents are unresolved (L8).
|
||
- **The operator can always switch.** The agent may switch on its own to a *ready*
|
||
item (one it can act on without the operator) when the active item is waiting on
|
||
the operator, after a grace period (default 10 minutes, so a watching operator
|
||
can answer first), or has closed.
|
||
- **The driver chooses which ready item,** from code-rendered summaries (source,
|
||
age, severity, state, title), never from other items' transcripts. A failed or
|
||
invalid choice falls back to the oldest ready item. Choosing grants nothing: every
|
||
item still meets its own rules.
|
||
- **Parking gives up authority.** Elevated credentials an item's plan mounted
|
||
(O2, X3) are unmounted when it parks, and remounted only if its approval hasn't
|
||
expired.
|
||
- **Nothing is lost while parked.** Intake for a parked item is read from a
|
||
durable cursor when it becomes active again (L8).
|
||
- **The state schema doesn't assume one item.** "Active" is a pointer, so allowing
|
||
more than one later (Q) is a loop change, not a migration.
|
||
|
||
### L2. The agent loop
|
||
|
||
Each agent runs **one conversational loop**, driven by the local model (P7). The
|
||
loop talks with the operator, reads and writes the work item's documents, calls
|
||
tools, calls frontier helpers when it needs them, and reports. It is not a fixed
|
||
workflow of states the conversation must pass through.
|
||
|
||
What is fixed is small:
|
||
- **Which work item is active,** and which are parked (L1).
|
||
- **Pending questions:** a question the agent asked is recorded durably, so a
|
||
restart asks it again rather than losing it.
|
||
- **Approvals as permissions** (L3), checked when an action needs one, not as
|
||
states the loop must visit.
|
||
- **Work in progress,** recorded as intents (L8), so a crash lands somewhere known.
|
||
|
||
Rules for the loop:
|
||
- **The agent never waits in a state it can't explain or leave.** Every wait is
|
||
either for the operator (with a question or proposal they can see) or for work
|
||
it has started. Anything else is a bug; the model checker (`specs/`) guards this.
|
||
- **Questions never change state** (L5). An operator's question gets an answer,
|
||
never an interpretation as approval, rejection or command.
|
||
- **`stop` works in every state,** for every agent: it ends the current turn or
|
||
delegation at the next tool boundary, unmounts elevated credentials, and asks
|
||
the operator what next. Unattended work needs an end that doesn't depend on the
|
||
model cooperating.
|
||
- **`abandon` ends a work item** from any waiting point, closing its external
|
||
record with a note and aborting unresolved intents.
|
||
- **Every action passes through the three layers** (P4) before it runs.
|
||
|
||
### L3. Authority: approvals and permissions
|
||
|
||
An approval grants a **permission with a scope**: "this build plan may change
|
||
this repo", "these ops steps may run", "this milestone may proceed without
|
||
discussion". Actions are checked against the permissions in force (P3); the loop
|
||
does not stop at gates for their own sake.
|
||
|
||
**What needs discussion before it happens:**
|
||
- **Architecture documents and build plans are always discussed** with the
|
||
operator (D2). Their approval is the scope for the work that follows.
|
||
- A build plan marks each milestone **pre-approved** (proceed when reached) or
|
||
**needs discussion** (stop and talk first).
|
||
- **Any class-3 action** not named by an approved plan (P3).
|
||
|
||
**Approvals bind to exactly what was approved.**
|
||
- Every plan revision carries a tag, numbered across the whole work item, so
|
||
`rev-N` names exactly one plan.
|
||
- **A reply that names no revision is about the one up when it arrived:** a bare
|
||
`approve` or any feedback is stamped on arrival with the revision last put up
|
||
(in Matrix, the one whose request had been sent; in Gitea, the latest plan
|
||
comment older than the reply). If that isn't the current revision, the reply is
|
||
not applied, and the agent says which revision is up. A repeated approval, an
|
||
approval on two channels, or an afterthought therefore can never land on the next
|
||
plan. (Found by the model checker.)
|
||
- **Approval messages are rendered by code from what will actually happen,** never
|
||
from a model's description of it (L11).
|
||
- **Approvals expire** where the world can change underneath them (ops, O2).
|
||
|
||
**Approval sources.** Matrix (L5), Gitea comments (D2) and Vikunja comments (O5)
|
||
are approval sources; each authenticates the approver against its own operator
|
||
allowlist. The first valid approval wins and is echoed to the other channels. The
|
||
knowledge store (L10) is **never** an approval source or an instruction channel.
|
||
|
||
**The trust dial.** How much the operator reviews after the fact is **data the
|
||
operator sets**, per project and per milestone: for example, whether PR review is
|
||
required before merging, or whether the agent may merge a pre-approved milestone
|
||
itself once checks pass. The operator turns it up as confidence grows. The agent
|
||
never changes it.
|
||
|
||
### L4. The action classifier
|
||
|
||
An independent check that sorts an action the fast path doesn't cover.
|
||
|
||
**Verdicts:**
|
||
- **safe** — proceed.
|
||
- **dangerous but in scope** — proceed, logged and reported to the operator.
|
||
- **dangerous, needs discussion** — pause and ask, in plain language.
|
||
|
||
**Rules:**
|
||
- **It judges the action, not the actor's story.** Its input is the structured
|
||
action (tool, command, target, a summary of the change) and the **agreed scope**
|
||
(the work item's task, the approved plan or milestone). Never the actor's
|
||
transcript or reasoning, and never untrusted content such as issue text or web
|
||
pages. Otherwise injected text, or the actor's own rationalisation, can argue past
|
||
it.
|
||
- **It fails closed.** An error, a timeout or an unclear verdict counts as *needs
|
||
discussion*.
|
||
- **It is independent of the actor:** its own prompt and context, ideally a
|
||
different model.
|
||
- **Hard walls don't depend on it** (P4).
|
||
- **Every verdict is logged** with its outcome. Operator overrides ("that was
|
||
fine" / "that should have asked") become an evaluation set (L9), and patterns it
|
||
keeps agreeing on become deterministic fast-path rules.
|
||
|
||
The classifier is shared by all agents. Each agent contributes its own fast-path
|
||
rules and scope sources (D6, O3).
|
||
|
||
### L5. Conversation and transport
|
||
|
||
**Transport.** Matrix is the conversation channel: a self-hosted homeserver, one
|
||
identity and room per agent. The orchestrator depends only on a `Transport`
|
||
interface (send, ask, receive), so another chat system is one new class.
|
||
Existing Matrix clients are the UI.
|
||
|
||
**What the operator can say.** Plain language, plus a small command vocabulary
|
||
that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, and
|
||
work-item selection (e.g. `use <project>`).
|
||
- **Commands are matched without regard to case, surrounding whitespace, trailing
|
||
punctuation, or a leading mention of the agent.** Messages that begin by
|
||
addressing someone else are not for this agent.
|
||
- **Questions never change state.** A question gets an answer from the agent,
|
||
drawn from the work item's context, and is never read as approval, rejection or
|
||
a command. How a question is recognised (explicit marker or classification) is
|
||
an open question (Q); a misread must default to asking, never to acting.
|
||
- **Everything the operator says is recorded** in the work item's transcript
|
||
(L7), even when no action follows.
|
||
|
||
**What the agent says.**
|
||
- **It explains as well as acts:** a plan comes with its rationale and the main
|
||
alternatives considered; a revision says what changed and why; finished work is
|
||
summarised (what was done, what was verified, what is left open).
|
||
- **It reports what happened to the operator's message** when it can't act on it
|
||
now ("noted; I'll use that once you say which project"), rather than refusing.
|
||
- **Progress is prose, at a readable rate:** what it is working on and what it has
|
||
found. Raw tool activity is available on request.
|
||
- **It asks when unsure**, including in the middle of implementation.
|
||
|
||
**Other channels.** Gitea (issues, PR comments, reviews) and Vikunja (tickets,
|
||
comments) carry work and records. They are polled or received as structured JSON.
|
||
Comments from allowlisted operators are instructions or approvals; comments from
|
||
anyone else are data (L11).
|
||
|
||
### L6. Context and memory
|
||
|
||
What each model sees, what persists, and how memory is written and used.
|
||
|
||
**1. The working context of one model call.** Rendered from the log at call time,
|
||
for one role, fitted to that model's window. It is never stored; only the log is.
|
||
Fitting, in order: shorten old tool results to their first and last lines; drop
|
||
the oldest events (with a note saying so); never drop the system prompt, the
|
||
approved plan or the most recent turns. A call that still doesn't fit is not sent;
|
||
the caller treats it as a failure (L9).
|
||
|
||
**2. The work-item transcript** — the canonical log (L7).
|
||
- **Durable, and scoped to one work item.** A model call for a work item renders
|
||
only that item's events. Nothing crosses items; agent-level calls (switch
|
||
decisions, duty runs) see other items only as code-rendered summaries.
|
||
- **Every entry is attributed:** the operator, the driver, a frontier helper, or
|
||
the orchestrator.
|
||
- **The orchestrator writes facts** into it: workspace prepared, milestone
|
||
executed, commit made, PR opened or merged, approval given. Models learn what
|
||
happened from these, not by inferring it.
|
||
- **Each role gets its own view:**
|
||
- the driver sees the conversation, the facts, and helpers' reports;
|
||
- a helper (delegation, `ask_expert`) sees its brief and its own work;
|
||
- no role sees another role's raw turns presented as its own.
|
||
|
||
**3. Scope memory** — outlives work items.
|
||
- **For a repo:** the project's own documents (`ARCHITECTURE.md`, build plans,
|
||
conventions), versioned in the repo and changed by PR (D2). Read at work-item
|
||
start; the repo is the authority.
|
||
- **For infrastructure:** the world model in the knowledge store (L10): what
|
||
exists and how it is arranged.
|
||
|
||
**4. Agent memory** — the agent's own notes, outliving work items and scopes.
|
||
- **What it holds:** the operator's preferences, lessons learned, recurring
|
||
decisions and their reasons. Much like a good collaborator's notebook.
|
||
- **Where:** the agent's private space in the knowledge store (L10), one note per
|
||
fact, with an index.
|
||
- **Written by the agent** at defined moments (below); **visible to the operator**,
|
||
who corrects it through conversation, not by editing.
|
||
- **Read at work-item start** (the index, plus notes relevant to the task) and when
|
||
the agent is about to make a decision the notes cover.
|
||
|
||
**5. What is never memory:** Matrix room history, provider-side session state,
|
||
build artifacts, and the contents of a previous task's workspace.
|
||
|
||
**How memory is written.**
|
||
- **At work-item close:** a summary of what was done and decided, into scope
|
||
memory where it belongs to the scope (a repo document change goes through a PR),
|
||
and into agent memory where it concerns the operator or the agent's own practice.
|
||
- **When a fact is learned:** with its source and when it was observed.
|
||
- **Operator corrections are stored verbatim, with attribution,** never
|
||
paraphrased. A model rewriting a precise fact loses precision invisibly.
|
||
- **One writer per document** (L10), so concurrent writes never happen.
|
||
|
||
**How memory is used.**
|
||
- **Everything read is snapshotted into the transcript** with its provenance
|
||
(document, revision, when retrieved), once per work item at first use.
|
||
- **Memory is a cache, never authority.** An agent may rely on it to know what
|
||
exists and where to look; facts an action depends on are re-checked against the
|
||
live system immediately before the action (preconditions, O2). When a proposal
|
||
rests on recalled facts, it says so.
|
||
- **Stale memory is corrected, not worked around:** when a check contradicts a
|
||
note, the agent updates the note and says so.
|
||
|
||
### L7. The canonical log
|
||
|
||
The durable record of every work item is a **canonical event log** in the state
|
||
store, not a provider-shaped message array. Provider requests are rendered from it
|
||
at call time.
|
||
|
||
| Event | Payload |
|
||
|---|---|
|
||
| `user_message` | text as sent, source (channel and event ID) |
|
||
| `assistant_text` | text, actor (role) and model |
|
||
| `tool_call` | canonical call ID, actor, tool, arguments |
|
||
| `tool_result` | canonical call ID, result, success |
|
||
| `plan_submitted` | plan body, revision, authoring model |
|
||
| `fact` | what the orchestrator observed or did (PR opened, milestone executed, …) |
|
||
| `delegation_started` / `delegation_completed` | brief, target model, caps / handback note, artifacts, tokens, duration, status |
|
||
| `classifier_verdict` | action, scope reference, verdict, model |
|
||
| `knowledge_retrieved` | text as read, document, revision, retrieved-at |
|
||
| `compaction` | summary, producing model, range replaced |
|
||
|
||
Rules:
|
||
- **Every event carries its work item and its actor.** Rendering uses both (L6).
|
||
- **Canonical IDs are minted locally.** Provider tool-call IDs are mapped at the
|
||
boundary and never used as join keys.
|
||
- **No provider-side handles for content.** Bytes or local paths are stored and
|
||
uploaded or inlined at render time.
|
||
- **Vendor-opaque reasoning artifacts** (signed thinking blocks, encrypted
|
||
reasoning items, thought signatures) are stored with the producing model and
|
||
replayed only within the session that produced them, per that vendor's rules. A
|
||
session is a delegation, including one resumed after a crash.
|
||
- **Token budgeting is a render-time concern** (L6).
|
||
- **Compaction summaries are first-class events,** so a model change never
|
||
inherits opaquely compacted history.
|
||
- **Prompt-cache breakpoints are placed by the renderer, never stored.**
|
||
- **The log is never edited.** Corrections are new events.
|
||
|
||
### L8. Durability and recovery
|
||
|
||
**The state store.** SQLite in WAL mode. It holds each work item's position,
|
||
permissions, pending questions, cursors, pinned roles, and the canonical log.
|
||
Build artifacts, workspaces and shipped-out logs are **not** in it.
|
||
- **Checkpoint every change,** committed and fsynced before the loop continues.
|
||
- **A schema version and migration hook** from the first commit, so a newer image
|
||
reads older state.
|
||
|
||
**Intents and idempotent effects.**
|
||
- **Before any external side effect, record an intent** (open a PR, push, merge,
|
||
run an ops step). After it, commit the intent.
|
||
- **Every external effect is idempotent against external truth.** Re-running an
|
||
interrupted step must detect that the effect already happened and adopt it. For
|
||
example, publishing adopts an open PR for the branch instead of opening a second.
|
||
- **Intake is handled at most once and answered at least once.** A durable cursor
|
||
per external thread (Gitea issue or PR, Vikunja ticket) records what was taken
|
||
up; an item being worked on is marked until its response is complete, and
|
||
redelivered after a crash. Comments that arrive while the agent can't act on
|
||
them are not consumed.
|
||
|
||
**Boot reconciliation.** Every start, before accepting input:
|
||
1. Open the store; run pending migrations.
|
||
2. **Reconcile pending intents against external truth.** Verified → committed.
|
||
Verifiably never happened → aborted. Unverifiable (the service is down) → ask
|
||
the operator.
|
||
3. **Adopt what external truth shows:** a published PR for a milestone that was
|
||
still executing, an approval posted while the agent was down.
|
||
4. **Resume waiting points as they were:** a pending question is asked again, an
|
||
open plan is announced again, watches on external threads resume from their
|
||
cursors.
|
||
5. **Interrupted work resumes only on evidence.**
|
||
- *A delegation* resumes automatically when the cut is clean (last event a
|
||
completed result, branch at the expected commit, caps unbreached, external
|
||
truth matching the log). Otherwise ask.
|
||
- *An ops step sequence* continues only when the log names exactly which step
|
||
was in flight and its expected outcome checks out against the live system,
|
||
and the approval hasn't expired (O2). Otherwise ask.
|
||
- *Anything else* is reported to the operator with what is known ("restarted
|
||
mid-milestone 2, branch at commit abc123"), with the options to continue,
|
||
re-plan or abandon. Never a blind resume.
|
||
|
||
**Verification.** The recovery rules are checked by a formal model of the loop
|
||
(`specs/`, Quint): interleavings of messages, comments, outcomes and crashes at any
|
||
point. Each counterexample it finds is reproduced against the real code before it
|
||
is fixed.
|
||
|
||
### L9. Models, roles and helpers
|
||
|
||
**LiteLLM in front of everything.** Each agent VM has exactly one LLM destination:
|
||
LiteLLM, which fans out to the local inference host and to frontier vendors. It
|
||
holds every vendor key (the agent holds one LiteLLM token), enforces budget caps
|
||
per virtual key, logs centrally, and routes fallbacks. Adding a vendor never
|
||
touches an agent VM.
|
||
|
||
**One client interface:** `complete(events, tools, model) → events`, over the
|
||
canonical log. Delegations use LiteLLM's passthrough route with a per-vendor
|
||
renderer, so vendor features aren't lost where long sessions live; quick calls use
|
||
the normalised route.
|
||
|
||
**Roles.** Models are named by role and resolved in configuration:
|
||
|
||
```yaml
|
||
roles:
|
||
driver: local/<model> # drives the loop: conversation, plans, implementation
|
||
helper: <frontier> # delegations: takes over a scoped sub-task
|
||
expert: <frontier> # ask_expert: one question, no tools
|
||
classifier: <model> # the action classifier (L4)
|
||
```
|
||
|
||
The resolved mapping is **pinned when a work item opens** and recorded in its log,
|
||
so a configuration change can't split a work item across two models.
|
||
|
||
**Capability profiles.** Each model in configuration carries a short profile,
|
||
filled in from benchmark results: its tools, window, what it does reliably and
|
||
where it is weak. Prompts that brief one role about another use the profile, never
|
||
a model name or a vague "weaker model".
|
||
|
||
**Escalation.** The driver calls a frontier helper:
|
||
- **when it chooses to** — its prompt encourages asking when unsure, and treats
|
||
asking as normal;
|
||
- **on observable triggers** — repeated failed actions, a context that won't fit,
|
||
hitting turn or time caps.
|
||
|
||
**Delegation.** A frontier model takes over a scoped sub-task with tools, works
|
||
autonomously, and hands back.
|
||
- **One harness, not one per vendor.** Vendor agent SDKs would bring back the
|
||
lock-in the design avoids.
|
||
- **Scoped by the agreed plan.** A delegation can't widen what the work item was
|
||
agreed to do; its actions pass through the same three layers (P4).
|
||
- **Branch at delegation start;** abandoning it discards the branch.
|
||
- **Hard caps** on turns, tokens and wall-clock time.
|
||
- **Checkpointed as it goes,** so it can resume after a crash (L8).
|
||
- **Handback is a note written for the driver:** what changed, what is now true,
|
||
what to do next. That note plus the resulting files is the whole interface back.
|
||
|
||
**`ask_expert`.** One stateless question with a text answer, for things needing no
|
||
repository access. A per-work-item budget stops a confused driver circling.
|
||
|
||
**Local-model accommodations.**
|
||
- **Tool-call enforcement:** `tool_choice` is requested only of models whose server
|
||
declares it can enforce it; otherwise the loop enforces the contract itself (an
|
||
explicit `finish`, prose treated as a violation).
|
||
- **A reduced tool schema** for roles on small models: fewer tools, flatter
|
||
arguments.
|
||
|
||
**Evaluation capture.** Delegations, `ask_expert` calls and classifier verdicts
|
||
are recorded as input/output pairs from the start, with outcomes marked. Changing
|
||
a model is then a configuration change and a re-run, not a matter of benchmark
|
||
chatter.
|
||
|
||
### L10. Knowledge store (Outline)
|
||
|
||
Durable knowledge that outlives work items: world models, agent memory (L6), and
|
||
later operator-written procedures.
|
||
|
||
**Layout: one writer per document.**
|
||
- **One collection per writing principal:** `nisse-ops-world`, `nisse-dev-world`,
|
||
each agent's memory, and `procedures` (the operator's). Sharing grants read
|
||
access only; concurrent writers never exist.
|
||
- **`nisse-dev` gets private space only.** Shared infrastructure documents are a
|
||
map of exactly what its sealed network keeps it from (D7).
|
||
- **Each agent authenticates as its own Outline user,** with its own API token. A
|
||
token from the operator's account would silently void every permission here.
|
||
- **Collections and documents are identified by ID** from configuration and state,
|
||
never discovered, and document IDs recorded when created make writes idempotent.
|
||
|
||
**Never an instruction channel, never an approval channel.** Work is never read
|
||
out of the knowledge store, and an agent never writes there to make another agent
|
||
act. Coordination between agents goes through the operator, Matrix, or a ticket.
|
||
|
||
**Corrections go through conversation.** The operator tells the agent, which
|
||
records the correction verbatim (L6). The operator keeps write permission as a
|
||
break-glass: stop the agent, edit, restart. An agent re-reads a document's
|
||
revision before writing and merges rather than overwrites if it moved.
|
||
|
||
**Retrieval is structural first.** Procedures are bound to entries in ops policy
|
||
(O2) and fetched whenever a plan uses that entry; search exists in addition, never
|
||
instead.
|
||
|
||
**Outages.** A failed write never blocks work (retry, or record locally and
|
||
reconcile). A failed read of a bound procedure stops planning that action; other
|
||
failed reads proceed and log the gap.
|
||
|
||
### L11. Prompt injection
|
||
|
||
**Any model that reads untrusted content may be steered by it.** No marker reliably
|
||
separates data from instructions. The design limits what a steered model can do
|
||
rather than trying to stop it being steered.
|
||
|
||
Untrusted content: issue, comment and ticket text from non-operators, repository
|
||
contents, dependencies, web pages, alert payloads, log lines.
|
||
|
||
- **Hard walls hold against a steered model** (P4): nothing a model says can grant
|
||
a permission, widen an approved scope, or reach a credential that isn't mounted.
|
||
- **Approval messages are rendered by code** from what will run, never from a
|
||
model's description.
|
||
- **The classifier never sees untrusted content** (L4).
|
||
- **Instruction channels are operator-scoped:** Matrix from allowlisted
|
||
identities, comments from allowlisted Gitea and Vikunja accounts. Everything
|
||
else is data.
|
||
- **Intake is parsed into structured fields in code** where the source allows it,
|
||
so less free text reaches a model, and what does is labelled by source.
|
||
- **Some injection is accepted because it can only reorder work:** an alert title
|
||
can bias which ready item is chosen next; the switch notice makes it visible.
|
||
- **Each agent is checked against the lethal trifecta** — private data, untrusted
|
||
content, and a way to send data out. Damage needs all three. `nisse-dev` holds
|
||
little private data and can reach only LiteLLM, Gitea and package registries;
|
||
`nisse-ops` has all three legs and relies on its hard walls and step sequences
|
||
(O2).
|
||
|
||
### L12. Secrets, identity and logging
|
||
|
||
**Secrets.** Fetched at run time from Vaultwarden machine accounts, never baked
|
||
into images. One minimally scoped machine account per agent and one per MCP server
|
||
instance (X3); each has a single bootstrap token on its own host, and nothing
|
||
else is stored there. Only credentials go in secret stores; every other setting
|
||
lives in reviewable configuration files.
|
||
|
||
**Identity.** Each agent is its own LLDAP principal (`svc-nisse-dev`,
|
||
`svc-nisse-ops`) in a tightly scoped Authelia group, never the operator's account.
|
||
Headless agents get a single-factor exception scoped by source address, group and
|
||
target.
|
||
|
||
**Logging.** Structured JSON lines to the host journal and the external log stack,
|
||
not kept in containers. Every durable state change emits exactly one line, written
|
||
by the state store right after the commit, with a fixed envelope (time, agent,
|
||
event, work item, cause). Every classifier verdict, escalation and external effect
|
||
is logged. **Logs are diagnostic, never load-bearing:** nothing reads them to
|
||
derive state.
|
||
|
||
---
|
||
|
||
## Part D — `nisse-dev`
|
||
|
||
Designs and builds software projects, producing reviewed changes in Gitea.
|
||
|
||
### D1. How it works
|
||
|
||
A dev work item is a task on one repository: a feature, a fix, a new project.
|
||
Through conversation, the agent and the operator agree on an architecture (for a
|
||
new project, or a significant change) and a build plan; the agent then works
|
||
through the plan's milestones, each producing a PR, and keeps the project's
|
||
documents up to date as it goes.
|
||
|
||
`use <project>` opens or resumes a work item on a repository. Messages sent before
|
||
it are kept for it (L5).
|
||
|
||
### D2. Documents are the workflow
|
||
|
||
The project's own documents carry the agreement between the operator and the
|
||
agent, and live in its repository:
|
||
|
||
- **`ARCHITECTURE.md`** — what the project is, how it fits together, and why. The
|
||
durable "what and why". Created with a new project; changed whenever the design
|
||
changes.
|
||
- **Build plans** — how a piece of work gets done, as a sequence of milestones.
|
||
Short-lived: one per body of work, kept in the repo for the record.
|
||
|
||
**Both are always discussed.** The agent drafts, the operator and agent talk it
|
||
through, and the document lands as a PR the operator approves. An approved
|
||
architecture and build plan are the scope (L3) for everything that follows.
|
||
|
||
**A build plan specifies; it doesn't implement.** Each milestone states its goal,
|
||
the interfaces and behaviour it adds or changes, edge cases, and **tests as
|
||
acceptance criteria**. It includes code only where the code *is* the clearest
|
||
specification (a tricky algorithm, an exact format). It never manages branches
|
||
(D4). How much detail it needs is informed by the driver's capability profile
|
||
(L9).
|
||
|
||
**Each milestone is marked** pre-approved (proceed when reached) or needs discussion
|
||
(stop and talk first), and says whether it is expected to change code (a review or
|
||
investigation milestone may not).
|
||
|
||
The Gitea issue for the work item carries the conversation's record: plans and
|
||
approvals are posted there as comments, so the agreement survives loss of the
|
||
agent's state (L8).
|
||
|
||
### D3. The workspace
|
||
|
||
**A fresh, clean workspace at `use <project>`.**
|
||
- Fetch from the repository, and check out a clean tree at the latest base branch.
|
||
- Anything left in a previous workspace (uncommitted changes, local branches,
|
||
unpushed commits) is **set aside**, never silently discarded or carried over,
|
||
and reported to the operator.
|
||
- Build artifacts and caches never survive into a new task.
|
||
|
||
The milestones of one work item share that workspace, since they build on each
|
||
other, often before earlier PRs are merged.
|
||
|
||
**Toolchain preflight.** When the workspace is prepared, the agent determines what
|
||
the project needs (from its files, e.g. `Cargo.toml`, `pyproject.toml`,
|
||
`package.json`, or a declaration in the repo) and checks it is available. A gap is
|
||
reported **before any planning**, with the options. The environment's toolchain
|
||
versions become facts in the transcript (L6), so plans match reality.
|
||
|
||
**Toolchains come from the environment, not from the agent improvising.**
|
||
Installing software is class 3 (P3). How toolchains are provided (project dev
|
||
containers on the VM's Docker daemon, or the VM image) is an open question (Q).
|
||
|
||
**Repairing the environment is in scope.** Within a task, the agent may clean
|
||
caches and build artifacts or reinstall project dependencies to fix a broken
|
||
build, and reports what it did. It doesn't stop and wait for permission to delete
|
||
a corrupt `.pyc`.
|
||
|
||
### D4. Git, branches and PRs
|
||
|
||
- **The harness owns branches.** Each milestone works on a branch the harness
|
||
creates from the work item's current state; plans and models never create or
|
||
switch branches.
|
||
- **Only the milestone's own changes are published:** what the agent changed,
|
||
never build artifacts (standard excludes in the clone, and committing only what
|
||
the work produced). Anything left out is reported.
|
||
- **"Changes to publish" are counted from where the milestone started,** never
|
||
from a possibly stale remote branch.
|
||
- **Publishing is idempotent** (L8): an open PR for the branch is adopted and
|
||
updated, never duplicated.
|
||
- **A milestone that completes without changes is done, not failed,** when it
|
||
wasn't expected to change code. The agent reports its result and moves on as the
|
||
build plan says. An unexpected "no changes" is raised with the operator.
|
||
- **Moving past a milestone checks its PR's state** in Gitea. If it is still open
|
||
and unmerged, the agent says so and asks, rather than leaving it behind.
|
||
- **The project's default branch is protected,** and this is verified, not
|
||
assumed: the agent's own token must be refused when pushing to it, and when
|
||
merging without whatever approval the trust dial requires (L3). A test asserts
|
||
both, so relaxing protection fails loudly.
|
||
- **A runnable test service's address** goes in the PR description.
|
||
|
||
### D5. Review and merge
|
||
|
||
- **The review loop:** operator comments on a PR (the conversation and inline
|
||
line comments) are instructions; the agent responds with commits to the same PR
|
||
and replies in the thread where the comment was made.
|
||
- **Intake is exactly as in L8:** cursors, at-most-once taking, at-least-once
|
||
answering, redelivery after a crash.
|
||
- **Merging follows the trust dial** (L3). Today the operator merges. As confidence
|
||
grows, the dial may let the agent merge a pre-approved milestone once checks pass
|
||
and the required approvals are recorded; the branch protection must then require
|
||
an operator approval the agent's account can't satisfy, so the merge rule is
|
||
enforced by Gitea, not by the agent.
|
||
- **Finishing a work item** closes its Gitea issue with a note (completed or
|
||
abandoned), after checking what is left open.
|
||
|
||
### D6. `nisse-dev`'s action classes
|
||
|
||
| Class | Examples |
|
||
|---|---|
|
||
| 1 | Reading the repo, running tests and builds in the workspace or a test container, querying Gitea |
|
||
| 2 (in scope) | Editing the workspace, committing to milestone branches, pushing them, opening and updating PRs, commenting on its issues and PRs, cleaning caches, installing project dependencies from package registries |
|
||
| 3 | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, installing system software, changing repository settings, anything outside the work item's repo |
|
||
|
||
Its scope sources are the approved architecture, the build plan and each
|
||
milestone's marking.
|
||
|
||
### D7. Deployment and isolation
|
||
|
||
- **Its own VM** (X1), with a real Docker daemon, because it builds and runs
|
||
containers. The VM is the trust boundary for unattended command execution.
|
||
- **Sealed:** its egress allowlist (X2) permits only LiteLLM, Gitea and package
|
||
registries; there is no ingress. Denial covers RFC1918 (except the specific
|
||
LiteLLM and Gitea addresses) and the tailnet's `100.64.0.0/10`, so it can't reach
|
||
the NAS, Proxmox or the gateways. The inference host sits behind LiteLLM, so it is
|
||
not a second exception.
|
||
- **No Tailscale** on this VM; it would route around the firewall.
|
||
- **Its own VLAN,** and test services on predictable ports (e.g. 18000–18099), so
|
||
opening access later is a firewall change.
|
||
- **Test containers on an internal Docker network with no route out.**
|
||
- **Build storage on a separate mount** from the state volume.
|
||
- **A browser** (a shared Playwright tool, headless) **reaches only the internal
|
||
test network,** to exercise web apps the agent built and reproduce bugs. No SSO.
|
||
|
||
---
|
||
|
||
## Part O — `nisse-ops`
|
||
|
||
Operates and maintains infrastructure across both sites, driven by tickets and
|
||
duties, with stricter controls because live changes have no review step before
|
||
they take effect.
|
||
|
||
### O1. How it differs
|
||
|
||
| Axis | `nisse-dev` | `nisse-ops` |
|
||
|---|---|---|
|
||
| Undo | discard a branch, revert a snapshot | often none (`zfs destroy`, a firewall lockout) |
|
||
| Scope source | architecture and build plan | approved step sequences (O2) |
|
||
| Default for class 3 | discuss | discuss, and exact-step approval |
|
||
| Work shape | milestones → PRs | short reactive tasks and scheduled checks → live changes |
|
||
| Driver of work | the operator in Matrix, Gitea | Vikunja tickets, alerts, duties, the operator |
|
||
| Placement | sealed, own VLAN | Tailscale, credentials, broad reach |
|
||
|
||
The two are separate deployments over the same library; one instance playing both
|
||
roles would collapse the isolation.
|
||
|
||
### O2. Approved step sequences
|
||
|
||
**Every infrastructure write needs an approved plan first,** and approval covers
|
||
exactly the steps written. This is `nisse-ops`' scope source for the classifier
|
||
(L4) and a hard wall in its own right.
|
||
|
||
- **Write credentials exist only in elevated MCP instances** (X3), mounted after
|
||
approval; read-only instances are mounted by default, so looking around needs no
|
||
plan.
|
||
- **A write plan is a list of literal steps,** each naming the tool and target, the
|
||
full arguments (a config change carries its exact diff), an optional
|
||
**precondition** checked against the live system just before the step, an optional
|
||
**expected outcome** checked just after, and a retry allowance (default one).
|
||
Prose may explain the plan; it is never scope.
|
||
- **The approval message is rendered by code from the steps,** flagging at the top
|
||
anything irreversible, anything touching a protected target, and anything that
|
||
takes down the agent's own host.
|
||
- **Execution is matched against the steps.** Read-only calls are always allowed.
|
||
An elevated call runs only if it matches the next unfinished step exactly
|
||
(arguments normalised against the tool's schema). Anything else halts and asks:
|
||
a call not in the plan, changed arguments, steps out of order, a completed step
|
||
run again. A failed precondition or expected outcome halts without retry.
|
||
- **Approvals expire.** A plan not started within the expiry window must be approved
|
||
again.
|
||
- **Policy is small, conservative, and data:** a versioned file declaring each MCP
|
||
instance's privilege, protected targets (the NAS, gateways, the agents' own
|
||
hosts), and each elevated tool's target argument. Anything unclassified is
|
||
treated as elevated and protected. Server-declared tool hints can make a tool
|
||
stricter, never looser. A model may propose entries for a new server as a PR; no
|
||
classification happens at run time.
|
||
|
||
**Parked, to add when real friction shows the shape:** bounded matching (any call
|
||
within an approved target and action, for reversible tools, one tool at a time),
|
||
and standing approvals (a plan template with a trigger, rate limit and expiry, for
|
||
a routine fix approved by hand often enough).
|
||
|
||
### O3. `nisse-ops`' action classes
|
||
|
||
| Class | Examples |
|
||
|---|---|
|
||
| 1 | Metrics, logs, status, read-only MCP calls, reading tickets and the world model |
|
||
| 2 (in scope) | Approved steps with a known undo; updating its own world model and tickets |
|
||
| 3 | Every other infrastructure write; browser actions on admin UIs (each confirmed, since a click isn't a stable literal); **anything on network gear**, the most conservatively handled action in the system, since a bad change can sever the path used to fix it |
|
||
|
||
### O4. Interfaces
|
||
|
||
- **MCP servers** (X3) for Proxmox and UniFi at both sites, one instance per
|
||
service, site and privilege: the primary path to infrastructure.
|
||
- **Browser** (a shared Playwright tool) for admin UIs where no API exists; real APIs
|
||
are preferred. For network gear it is a fallback only.
|
||
- **SSO** as `svc-nisse-ops`, whose blast radius equals its Authelia scope.
|
||
- **Event intake** as structured JSON (directly or via Alertmanager), never through
|
||
a Matrix bridge. The agent posts curated updates to `#ops`.
|
||
|
||
### O5. Tickets and duties
|
||
|
||
- **A Vikunja ticket is a work item's external record** (durable, with status and
|
||
history); **Matrix is the live conversation about it.**
|
||
- **Scheduled checks and alert intake are duties** (L1). Tickets they generate are
|
||
deduplicated findings, and like any auto-generated ticket can only lead to a plan
|
||
the operator approves.
|
||
- **Flow:** ticket filed → agent picks it up → discuss over Matrix → status updates →
|
||
closed with the result.
|
||
- **Vikunja comments are an approval source** for plans on their own tickets
|
||
(`approve rev-N`). Live prompts stay in Matrix, with outcomes written back.
|
||
- **Action-triggering tickets are operator-scoped:** a ticket is an instruction
|
||
channel into an agent that changes infrastructure.
|
||
|
||
### O6. Deployment
|
||
|
||
- **Its own long-lived, stateful VM** (X1).
|
||
- **Tailscale** and scoped credentials to reach infrastructure at both sites.
|
||
- **Its MCP servers run on its own Docker host;** its egress allowlist therefore
|
||
includes the Proxmox and UniFi endpoints at both sites.
|
||
- **Updated in place,** with restarts gated on an idle flag (X4).
|
||
|
||
---
|
||
|
||
## Part X — Deployment model (all agents)
|
||
|
||
### X1. Every agent gets its own VM
|
||
|
||
No agent runs as a container on a shared host, and no two agents share a VM.
|
||
- **The VM is the isolation boundary between agents:** a compromised or confused
|
||
agent can't reach another's state, credentials or processes.
|
||
- **Per-agent attribution:** everything an agent holds is issued to its own
|
||
identity, so every upstream action traces to one agent.
|
||
|
||
### X2. Egress policy per agent instance
|
||
|
||
- **Default-deny allowlist per agent instance,** declared as a versioned artifact
|
||
next to the agent's definition, each entry justified by the component that
|
||
needs it.
|
||
- **Declared in one place, enforced in several:** at the VM, the Docker network,
|
||
and the UniFi firewall. Network-layer enforcement survives compromise of the
|
||
agent's host.
|
||
- **The allowlist is the union** of what the agent and its MCP servers reach.
|
||
- **The tailnet is `100.64.0.0/10`,** not RFC1918; deny it explicitly.
|
||
|
||
### X3. MCP servers
|
||
|
||
- **One instance per agent VM,** as containers on that agent's Docker host. A shared
|
||
process would mean a shared credential, losing attribution in the upstream API's
|
||
own audit log. Duplication is nearly free for stateless servers.
|
||
- **One instance per (service, site, privilege).** The site is part of the server's
|
||
identity, so a wrong-site action needs a tool that isn't mounted, not just a wrong
|
||
argument. Read-only and elevated instances have separate credentials, each from
|
||
its own machine account.
|
||
- **Per-task tool profiles** mount only the servers a work item's scope needs.
|
||
- **Rejected: search-then-dispatch meta-tools** over large generated catalogues:
|
||
extra round trips for a local driver, no discovery of capabilities not already
|
||
sought, opaque arguments, and a dynamic tool list that defeats prompt caching.
|
||
|
||
### X4. Containers, images and restarts
|
||
|
||
- **Stateless images, named volumes** for state.
|
||
- **Delivery:** Gitea Actions → Gitea's OCI registry → Watchtower pulls.
|
||
- **Gate the restart, not the pull:** an idle flag; Watchtower never restarts an
|
||
agent mid-work. Unplanned interruptions are handled by recovery (L8).
|
||
- **Third-party MCP server images are digest-pinned** and updated deliberately,
|
||
never by Watchtower: they hold root-equivalent credentials.
|
||
- **Survive reboots:** VMs and Docker start on boot, containers restart unless
|
||
stopped, and the state dataset has ZFS snapshots (scheduled, and on each
|
||
approval).
|
||
|
||
### X5. Supporting services
|
||
|
||
All self-hosted: Matrix (Synapse), Gitea, LiteLLM, the local inference host
|
||
(llama-swap on an RTX 4090 host), Vikunja, Outline, Vaultwarden, LLDAP with
|
||
Authelia. Each agent gets its own account, scoped token and room in each service
|
||
it uses; nothing is shared between agents.
|
||
|
||
---
|
||
|
||
## Part Q — Open questions
|
||
|
||
- **The action classifier's model and placement.** A cheap frontier model, or the
|
||
local model with a different prompt; how its verdicts are calibrated against
|
||
operator overrides.
|
||
- **Recognising questions.** An explicit marker, or classifying free text; either
|
||
way a misread must default to asking (L5).
|
||
- **How far the local driver goes.** Which kinds of work it handles well and where
|
||
a frontier driver is needed, measured on real tasks, not assumed (P7).
|
||
- **Providing toolchains** for `nisse-dev`: project dev containers on its Docker
|
||
daemon, or the VM image, or both (D3).
|
||
- **One local model for every agent.** The inference host keeps one model resident;
|
||
different models per agent would reload weights on every switch. Preferred: one
|
||
model good enough for every local role. Fallback: agents take turns holding the
|
||
host. Measured before deciding.
|
||
- **Vision.** Needed for 3D design work (render and look) and video-file
|
||
management; not for ordinary code. Tools returning images, image content in the
|
||
log and renderers, and image tokens in the context budget. Measured when that work
|
||
starts.
|
||
- **More than one active work item per agent.** Park-and-switch first; concurrency
|
||
costs per-item serialisation, a scheduler for the shared inference host, strict
|
||
approval threading, separate workspaces per item, and per-item intent ownership.
|
||
- **When a work item must have an external record.** Plan-driven dev work and
|
||
ticket-driven ops work already do; whether every item with an approval must.
|
||
- **Fallback browser access to network gear:** standing, or handed off per task.
|
||
- **Relocating stateful MCP servers** off agent VMs, one at a time, if one ever
|
||
justifies losing per-agent attribution.
|
||
- **Infrastructure as code** (Terraform with `bpg/proxmox` and cloud-init) for agent
|
||
VMs.
|
||
|
||
---
|
||
|
||
## Appendix — What changed from the Build Specification
|
||
|
||
For reviewing this rewrite against `docs/Architecture-Build-Spec.md`:
|
||
|
||
- **Threat model and control model (P2–P4).** Checks are built around mistakes,
|
||
injection and crashes, in three layers with an action classifier, rather than a
|
||
rigid state machine gating every phase.
|
||
- **The agent loop (L2)** replaces the per-agent state machines (old A4, B1). What
|
||
stays fixed is only what authority and durability need.
|
||
- **Approvals as scoped permissions (L3),** with pre-approved and needs-discussion
|
||
milestones and an operator-set trust dial. Revision binding is kept and extended
|
||
(task-wide revisions, stamping on arrival).
|
||
- **The local model drives everything (P7, L9);** frontier models are helpers. The
|
||
old "planning always escalates" phase policy is dropped.
|
||
- **Context and memory (L6)** is new: transcript scoping, attribution, orchestrator
|
||
facts, scope memory, agent memory, and how memory is written and used.
|
||
- **Documents as the dev workflow (D2),** a fresh workspace per task and toolchain
|
||
preflight (D3), and git hygiene learned from the live runs (D4).
|
||
- **Conversation (L5)** is a first-class requirement, not a command interpreter.
|
||
- **Kept, largely as they were:** the work model, durability and reconciliation,
|
||
LiteLLM and the canonical log, delegation, the knowledge store, prompt injection,
|
||
secrets and identity, `nisse-ops`' approved step sequences, and the deployment
|
||
model.
|
||
- **Moved to the Development Plan (to be written):** build order, supporting-service
|
||
status, and interim arrangements.
|