Files
nisse/docs/Architecture.md
T
dongarandClaude Opus 5.5 230b927ebe
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m20s
Revise the deployment model: an agent host and tool VMs
Agents run as containers on one stable agent host that keeps their
state and secrets and never runs a command a model chose. Model-chosen
commands run on disposable tool VMs over SSH: a build VM per project
for nisse-dev, its own tool VM for nisse-ops. Secrets are a set per
agent, handed out per operation by the harness and redacted from tool
output. MCP servers stay on the agent host as the one exception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-02 16:35:55 +00:00

1134 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Nisse — Architecture
**Status: agreed (PR #83); deployment model revised (PR #90).** How and in what
order it gets built is in `docs/Development-Plan.md`. It replaced
`docs/Architecture-Build-Spec.md`, which remains in git history; its section letters
(A–H) are not reused here.
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 hosts are, 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 on sealed build VMs
(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 the agent doesn't own, 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.
- **Tool VMs are disposable** (X1): a broken one is re-created, not repaired. What
must survive lives on the agent host.
### 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`'s work runs on disposable,
sealed build VMs; `nisse-ops` holds live-infrastructure reach. They never share a
running instance, and no command a model chose runs where agents keep their state
and secrets (X1, L13).
- **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.
- **Another agent can be a source.** A request from one agent to another (for
example, `nisse-dev` asking `nisse-ops` for a build VM) arrives through the
receiving agent's normal intake, such as a ticket, and becomes a work item there.
It carries no authority of its own: the receiving agent applies its own rules
(P3, L3) exactly as for any other work item, and the requester waits on it like
any external dependency.
- **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).
**Intake outages delay, never fail.** When Matrix, Gitea or Vikunja is
unreachable, intake backs off exponentially (capped at a few minutes), retries
indefinitely, and logs the outage. An outage delays a reaction; it never fails
work or forces a change of state. Outbound effects interrupted by an outage are
covered by intents (L8).
### 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 delegated helper sees its brief, the approved plan, the facts, its own work,
and the driver's work in the same milestone as compact narration (each step
and how it went);
- `ask_expert` is stateless: the expert sees only the question;
- no role sees another role's raw turns presented as its own. Whatever is not
the viewer's own reaches it as input labelled with its origin
(`[orchestrator]`, `[operator, Matrix]`, `[helper report]`).
**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 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.
**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 its builds 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 (L1).
**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 the build VMs where untrusted content meets command
execution hold no credential beyond their project's (D7); `nisse-ops` has all
three legs and relies on its hard walls and step sequences (O2).
### L12. Secrets, identity and logging
**Secrets.**
- **Each agent has its own set of secrets,** held on the agent host (X1) and
mounted only into that agent's container. No agent can read another's. They are
never baked into images.
- **An agent may give its set structure.** `nisse-dev` namespaces it by project
(D7); `nisse-ops` may do the same by site. The framework only needs names.
- **The harness hands secrets out per operation.** Which secrets a command or tool
call receives, and how (environment or stdin, never a command line), is decided
by code and configuration, never by a model.
- **Every value handed out is redacted** from tool output before it reaches the
transcript, a model or a log. A test that prints its environment must not put a
key into a frontier request.
- **MCP server instances** (X3) each have their own minimally scoped credentials,
given only to that instance.
- **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.
### L13. Tools and execution
Tool calls come in three kinds, and the framework allows no others.
| Kind | Examples | Where it runs |
|---|---|---|
| **Control tools** | `submit_plan`, `ask_human`, `finish`, `ask_expert` | In the harness. They act only on the agent's own state and conversation |
| **Execution tools** | Running a command; reading, writing and listing files | On a remote host, over SSH, through `execute(host, …)` |
| **MCP tools** | Proxmox, UniFi, Gitea APIs (X3) | In MCP server containers beside the agent |
- **`execute(host, command, *, secrets, stdin)` is the only way to run anything.**
There is no local execution path in the framework, so the agent host never runs a
command a model chose. File tools are SSH operations too.
- **The harness binds the host,** not the model. For `nisse-dev` it comes from the
work item: that project's build VM (D7). Where an agent must let the model choose
(`nisse-ops` running a command on its tool VM or on a device), the model picks
from the hosts the harness allows for that work item; any other host is refused
before a connection is made.
- **MCP tools are the one exception,** made deliberately: their servers run fixed
code against an API, and the model chooses only arguments. They pass through the
action classes (P3) and the classifier (L4) like everything else, and their output
is redacted (L12).
- **Tests use SSH too:** a throwaway `sshd` container stands in for a tool VM, so the
code path under test is the one used in production.
- **The action classifier is not a tool.** It is a model call made by the harness
and executes nothing, so it runs on the agent host.
---
## 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
**Each project has its own build VM** (X1), where its workspace lives and its
commands run.
- **Kept between work items,** so installed toolchains and package caches stay
warm, and isolated from every other project's.
- **Collected when idle.** A build VM whose project has been idle long enough may be
destroyed, and is re-created when work on the project resumes.
- **Re-created from a template** plus the toolchains recorded for the project (see
below). Work pushed to Gitea survives; a commit not yet pushed may be lost when a
VM is wiped, and that is accepted.
**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 in the workspace 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
installed **before any planning**. The environment's toolchain versions become
facts in the transcript (L6), so plans match reality.
**The agent installs what it needs on the project's build VM.** The VM is
disposable and is the boundary (D7), so installing packages and toolchains there
is class 2 (P3): a contained side effect. Every install is recorded as a fact (L6)
and in the project's scope memory, so the VM can be rebuilt; toolchains that keep
being needed are baked into the template (X4). Installing on any other host stays
class 3.
**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.
- **Pushes are harness operations on the build VM.** The harness runs `git push`
there, handing the push key over stdin to a short-lived `ssh-agent` for that one
push (L12); the model's commands never receive it. Nothing is synced through the
agent host: Gitea is the only copy that has to survive.
- **Pull requests are opened from the agent's container** with its Gitea API token,
which never reaches a build VM.
- **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, packages and toolchains on the project's build VM |
| 3 | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, changing repository settings, anything outside the work item's repo or its project's build VM |
Its scope sources are the approved architecture, the build plan and each
milestone's marking.
### D7. Deployment and isolation
**The agent** runs as a container on the agent host (X1), holding its state, its
secrets and its Gitea API token. It reaches LiteLLM, Gitea, Matrix, and SSH to its
build VMs, and nothing else. It never runs a command a model chose (L13).
**Build VMs,** one per project (D3), are where the work happens:
- **A real Docker daemon,** because projects build and run containers. The VM is
the trust boundary for unattended command execution.
- **Sealed:** egress allows package sources (language registries and OS package
mirrors) and Gitea's git endpoint; there is no ingress except SSH from the agent
host. Denial covers RFC1918 (except those specific addresses), the agent host,
and the tailnet's `100.64.0.0/10`, so a build can't reach the NAS, Proxmox, the
gateways or the agent's state.
- **A project may add egress its tests need,** for example LiteLLM and a Matrix test
homeserver for the `nisse` project itself. Additions are declared in the
project's configuration and reviewed like any allowlist (X2).
- **No Tailscale** on build VMs; it would route around the firewall.
- **On their own VLAN,** with 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.**
- **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.
**Secrets are namespaced by project** (L12), e.g. `nisse/litellm-key`,
`nisse/gitea-test-token`. A build VM's commands receive only their own project's
secrets, and each secret's configuration says whether the model's commands may see
it (a test credential the project's tests need) or only harness operations use it
(the push key). Anything the model's commands receive must be scoped for testing,
never able to touch production.
**Provisioning.** Build VMs are created and destroyed by Terraform from the
template. Eventually `nisse-dev` asks `nisse-ops` for them, as an agent-to-agent
request (L1); until then the operator provisions them.
---
## 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 agent host and
tool VMs), 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
- **A container on the agent host** (X1), with its MCP servers beside it (X3).
- **Its own tool VM,** long-lived, for network-management tools and the commands it
runs (L13). It holds Tailscale and the reach those commands need at both sites;
like any tool VM it can be rebuilt from its template.
- **Its egress allowlist** includes the Proxmox and UniFi endpoints at both sites,
for its MCP servers.
- **Accepts provisioning requests** from other agents (L1): creating and
destroying build VMs for `nisse-dev` through Terraform, under its own rules (O2).
- **Updated in place,** with restarts gated on an idle flag (X4).
---
## Part X — Deployment model (all agents)
### X1. The agent host and tool VMs
**One agent host,** a stable, long-lived VM.
- **Each agent runs on it as a container,** with its own volume (state, transcripts,
memory), its own secrets (L12) and its own network. No agent can reach another's
state, credentials or processes.
- **Its MCP servers run beside it** as containers (X3).
- **It never runs a command a model chose** (L13). That is what makes it safe for
agents to share it: nothing on it executes untrusted input.
**Tool VMs** are where model-chosen commands run, reached only over SSH from the
agent host. Each agent has the tool VMs its work needs:
- `nisse-dev`: one build VM per project, collected when idle and re-created on
demand (D3, D7);
- `nisse-ops`: its own tool VM, for network-management tools (O6);
- a purely conversational agent: none.
Tool VMs are **disposable** (P5): each is built from a template plus recorded
installs, and nothing on one has to survive.
**Per-agent attribution:** everything an agent holds is issued to its own
identity, so every upstream action traces to one agent.
### X2. Egress policy
- **Default-deny allowlists,** declared as versioned artifacts next to the agent's
definition, each entry justified by the component that needs it.
- **For each agent container,** the union of what the agent and its MCP servers
reach, enforced by its Docker network and the agent host's firewall. The UniFi
firewall sees the agent host as one address, so these allowlists rest on the
host; that is acceptable because nothing on the host executes untrusted input.
- **For each tool VM,** enforced at the VM, its Docker network and the UniFi
firewall on its VLAN. Network-layer enforcement survives compromise of the VM,
which matters here because this is where untrusted input meets command execution.
- **Tool VMs never reach the agent host,** except as replies on the SSH sessions it
opens.
- **The tailnet is `100.64.0.0/10`,** not RFC1918; deny it explicitly wherever it
isn't needed.
### X3. MCP servers
- **One instance per agent,** as containers beside it on the agent 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.
- **The one exception to "execution happens on tool VMs"** (L13): a server runs
fixed code against an API, and the model supplies only arguments.
- **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, VMs and restarts
- **Stateless images, named volumes** for state, on the agent host.
- **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.
- **Tool VMs come from templates,** provisioned by Terraform (`bpg/proxmox` and
cloud-init). Installs that keep recurring are baked into the template.
- **Survive reboots:** VMs and Docker start on boot, containers restart unless
stopped, and the agent host's state dataset has ZFS snapshots (scheduled, and on
each approval). Tool VMs are not snapshotted; they are rebuilt.
### 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).
- **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 the agent host, one at a time, if one
ever justifies losing per-agent attribution.
- **Where secrets come from.** Each agent's set lives on the agent host (L12); whether
it is filled by hand, synced from Vaultwarden, or written by Terraform.
- **How `nisse-ops`' MCP servers reach the second site** from the agent host
(Tailscale on the host, with other agents' networks denied the tailnet, or a
subnet route), since Tailscale on the host serves every container on it.
- **How long idle before a build VM is collected,** and whether collection needs the
operator's word.
---
## Appendix — What changed from the Build Specification
For readers who knew the former `docs/Architecture-Build-Spec.md` (in git
history):
- **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.
- **Deployment (X1, L13),** revised in PR #90: one agent host with each agent as a
container, and disposable tool VMs (a build VM per project for `nisse-dev`) where
every model-chosen command runs over SSH, replacing a VM per agent.
- **Kept, largely as they were:** the work model, durability and reconciliation,
LiteLLM and the canonical log, delegation, the knowledge store, prompt injection,
identity, and `nisse-ops`' approved step sequences.
- **Moved to the Development Plan (to be written):** build order, supporting-service
status, and interim arrangements.