# Nisse — Architecture **Status: agreed (PR #83); revised in PR #90.** How and in what order it gets built is in `docs/Development-Plan.md`, which also holds implementation notes too detailed for this document. The former `docs/Architecture-Build-Spec.md` is in git history. This document records decisions and the reasons for them: what Nisse is, how its parts fit together, and why. Each rule is stated once, in its home section, and referred to elsewhere by section ID. --- ## 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 (3D design work, video-file management), each another application over `nisse-core`. One string names all of an agent's artifacts (package, image, hostname, Matrix identity, service account), so grepping the infrastructure for `nisse-ops` finds everything. **Sites** are named by their domain stem: `garrett-hall`, `hartman-hut`. Everything lives in **one repository, `nisse`**. The repository was never a trust boundary (the hosts are, X1), and a monorepo saves a solo operator tripled admin. --- ## Part P — Principles ### P1. What Nisse is Self-hosted agents that **do real work and talk about it.** Each is a collaborator the operator converses with in Matrix: it explains what it is doing and why, answers questions, asks when unsure, and summarises what it did. It is not a command interpreter behind a menu of keywords. The agents run **local-first** (P7) and **self-hosted**: everything that can run inside the operator's network does, and no outside provider holds state the system needs to resume. ### P2. Threat model The checks exist to stop **mistakes and manipulation** from doing damage, not to contain a model that schemes against the operator. In order of how often they matter: 1. **Honest mistakes:** a wrong command, a misread instruction, confidently wrong code, an action on a stale belief. They call for reversibility, visibility and a second look before anything irreversible (P3, P5). 2. **Prompt injection:** untrusted content (issue text, repository files, dependencies, web pages, alerts, logs) steers a well-meaning model. It calls for hard boundaries where untrusted input meets real power (P4, L11). 3. **Crashes and environment surprises:** a process dies mid-action, a host reboots, a workspace is corrupt. They call for durable state and idempotent effects (L8). No check relies on an agent's intentions: one that assumes honesty fails under injection, and one built to contain a hostile model makes the agent useless. ### P3. Actions by their effects Every action is **harmless**, **contained** or **dangerous**: | Class | What it is | Examples | Rule | |---|---|---|---| | **Harmless** | Changes nothing outside a disposable sandbox | Reading, querying, searching, running tests in a throwaway environment | Always allowed | | **Contained** | 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 its own notes; for ops, an approved step with a known undo | Allowed in scope; logged and reported | | **Dangerous** | Irreversible, visible beyond the agent's area, or outside what was agreed | Merging to a default branch, deleting data, changing a firewall, installing on a host the agent doesn't own, contacting third parties, spending beyond a budget | Needs discussion, unless an approved step sequence (O2) or the operator in conversation names the exact action | **In scope** means within what the operator agreed to (L3). The same action can be contained in one work item and dangerous in another. **Make actions less dangerous wherever it is cheap** (P5). ### P4. Three layers of control 1. **Hard walls, outside any model:** credentials the agent doesn't hold, network destinations it can't reach (X2), branches it can't push to or merge into, mounts it doesn't have, budget caps, the kill switch, idempotent external effects (L8). They hold even if every model is fooled. 2. **A deterministic fast path:** harmless actions, and contained ones a static rule recognises as in scope, proceed without asking any model. Most actions go here. 3. **The action classifier** (L4) for everything else. **Conversation sits above all three:** when anything needs discussion, the agent asks in plain language and waits. Anything truly irreversible also sits behind a hard wall; the classifier is a filter, not a guarantee. ### P5. Reversibility first - Work happens on **branches**, never on what others depend on (D4). - Work starts from a **fresh, clean workspace** (D3): a broken environment is replaced, not diagnosed. - **What must survive lives on the agent host and is snapshotted** (X4), and snapshots are taken before risky changes where the platform allows. Tool VMs, build artifacts and caches are disposable and rebuilt rather than repaired (X1). - Where a tool offers a **dry run** or preview, the agent uses it first. ### P6. Structural, not model judgment Anything 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. Examples: adopting an already-open PR after a crash (L8), binding approvals to what they were about (L3), harness-owned branches (D4). Models are trusted outright only where a wrong choice costs time rather than safety: choosing the next ready item (L1), judging whether a report is clear (D1), naming a milestone (D4). ### P7. Local first, frontier on call - **A local model drives every step** (conversation, planning, implementation, review), on the operator's own inference host behind LiteLLM (L9). - **Frontier models are helpers the driver calls:** a quick question (`ask_expert`) or a scoped sub-task (delegation), L9. - **Escalation doesn't wait for the model to notice.** The driver is prompted to ask for help when unsure, which is its own judgement; observable triggers (repeated failures, caps) escalate as well, even when it doesn't ask, since small models are poorly calibrated about their own competence. - **This is an experiment with a cheap exit:** if a local model falls short at some kind of work, a frontier model drives it instead, as a configuration change. --- ## Part L — The shared library (`nisse-core`) Everything in this part is used by every agent. ### L1. The work model | Concept | What it is | `nisse-dev` | `nisse-ops` | |---|---|---|---| | **Duty** | A standing responsibility that produces work items | Watching for issues assigned to it (D1) | Scheduled checks, log monitoring, alert intake | | **Work item** | One effort, opened and eventually closed | A bug, feature or new project | Diagnosing a failure; making one change | | **Scope** | What a work item operates on; context that outlives it | A repository, or `general` | A site and service, or `general` | - **`general` is the scope of conversation not (yet) about a project:** feasibility, hardware, how something works. Each conversation is its own item, one per thread (L5), so unrelated chats stay out of each other's context. It has no repository, no build VM and no execution, so its context is the conversation and agent memory (L6). It closes after a day idle, with a summary in its log; a later reply in its thread starts a new item linked to it. If it turns into work, the work moves to a project's item (D1). - **Sources of work items:** duties, the operator (always), and other agents. A request from another agent is an issue assigned to the receiving agent, and carries no authority of its own: the receiver applies its own rules. - **A duty run is read-only;** a finding that needs attention becomes a work item, deduplicated by a stable fingerprint so a repeat attaches to the open item. - **A work item is the unit of state:** its conversation, proposals, pending questions, pinned roles (L9) and transcript (L7). Its locally minted ID is also its correlation ID in logs. - **One tracker: Gitea issues,** for every agent. An issue assigned to an agent is a work item for it, and the issue is the item's record (D1, O5). External truth wins on divergence (L8). - **Work items don't nest:** a milestone is a phase of one; a delegation is an episode in one. **States.** | State | Waiting for | |---|---| | **active** | nothing; it is being worked on | | **ready** | its turn | | **waiting on the operator** | an answer, a go-ahead, a review or a merge | | **blocked** | a recorded dependency to close | - **Blocked comes from external truth:** a Gitea issue dependency, including on an issue in another repository, such as a request to another agent. It becomes ready only when the dependency actually closes (P6); the operator can remove the dependency. An agent that finds a blocker mid-work proposes recording it, and records it only with the operator's agreement. **One active work item per agent; the rest are parked.** - **Switching happens only at safe points:** the active item is waiting, blocked or closed, and no intent is unresolved (L8). - **Conversation is not work.** The agent answers any message about any item between turns, without switching. A reply may change an item's state (an answer makes it ready); only the active item has tools run for it. - **The agent switches on its own** when the active item has waited on the operator past a grace period (so a watching operator can answer first), is blocked, or has closed. The operator can switch at any time. - **Next item:** the operator's order when given ("do #14 next", a priority label), otherwise the oldest ready item. An agent may let the driver choose from code-rendered summaries where judgement helps (ops, by severity); choosing grants nothing. - **Parking gives up authority:** elevated credentials are unmounted (O2) and remounted only if the approval hasn't expired. - **Nothing is lost while parked:** intake for it waits on a durable cursor (L8). - **"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: it talks with the operator, reads and writes the work item's documents, calls tools and helpers, and reports. It is not a fixed workflow of states. What is fixed: which item is active (L1), pending questions (recorded durably, asked again after a restart), permissions (L3), and work in progress as intents (L8). - **Every wait is explainable and leavable:** for the operator (with something they can see), for work the agent started, or for a recorded dependency. Anything else is a bug, which the model checker (`specs/`) guards against. - **`stop` works in every state:** it ends the current turn or delegation at the next tool boundary, unmounts elevated credentials, and asks what next. - **`abandon` ends a work item** from any waiting point, closing its record and any open PRs with a note and aborting its unresolved intents. - **Every action passes through the three layers** (P4). ### L3. Authority: scope and approvals **Scope** is what the operator agreed to, and what actions are checked against (P3). The loop doesn't stop at gates for their own sake. - **Dev work:** the assigned issue and what was agreed in its conversation (D1). Documents inform it but don't widen it (D2). The real check is the PR review. - **Ops work:** an approved step sequence, exactly (O2). - **Always discussed first:** any dangerous action not named by an approved step sequence or by the operator in conversation. - **Labels and words steer, and are never required** (D1): leaving one off is never an error. **Approvals bind to exactly what was approved.** This covers everything put up for agreement: a restatement awaiting a go-ahead (D1), a design (D2), a step sequence (O2). - Every proposal revision carries a tag, numbered across the work item: `rev-N` names exactly one. - **A reply that names no revision is about the one up when it arrived.** If that isn't the current one, the reply isn't applied and the agent says which is up. A repeated approval, one on two channels, or an afterthought can never land on the next proposal. (Found by the model checker.) - **Approval messages are rendered by code from what will happen,** never from a model's description (L11). - **Approvals expire** where the world can change underneath them (O2). **Approval sources:** Matrix (L5) and Gitea comments (D1, O5), each checked against its own operator allowlist. The first valid approval wins and is echoed to the other channels. The knowledge vault is never one (L10). **The trust dial:** how much the operator reviews after the fact, set by the operator per project (e.g. whether the agent may merge its own PRs once checks pass, D5). The agent never changes it. ### L4. The action classifier An independent model call that sorts an action the fast path doesn't cover: **safe** (proceed), **dangerous but in scope** (proceed, logged and reported), or **dangerous, needs discussion** (ask). - **It judges the action, not the actor's story:** its input is the structured action and the agreed scope (L3), never the actor's transcript or untrusted content, which could argue past it. - **It fails closed:** an error, timeout or unclear verdict means *needs discussion*. - **It is independent of the actor:** its own prompt and context, ideally a different model. - **Every verdict is logged.** Operator overrides become an evaluation set (L9), and verdicts it keeps agreeing on become fast-path rules. It is shared by all agents; each contributes its own fast-path rules (D6, O3). It executes nothing, so it runs on the agent host. ### L5. Conversation and transport **Matrix is the conversation channel:** a self-hosted homeserver, one identity and room per agent, behind a `Transport` interface so another chat system is one new class. Existing Matrix clients are the UI. **A thread per work item.** Each item's conversation is a Matrix thread, and the thread is how the agent knows which item a message is about. - **When a thread starts:** with the item. For an assigned issue, the agent's acknowledgement ("queued myproject#14") is the thread's first message. For a conversation, the operator's own top-level message is, and the agent replies in a thread under it. The item records its thread, so the mapping survives restarts. - **Inside a thread,** every message and command (`go`, `stop`, `abandon`, an answer) applies to that item, even while it is parked. - **At top level,** `status` is about the agent and `stop` stops the active item. A command that names its item applies to it ("abandon #14"). Any other command is ambiguous, so the agent asks which item rather than guessing. Any other message starts a new conversation, and the agent offers to continue an existing item if it clearly belongs there. - **Every conversation is a work item from its first message,** in the `general` scope until it is about a project (L1), however vague ("I'm having trouble with X…"). Chit-chat is simply answered in its thread; only work gets a Gitea issue (D1). It may end with a project, an issue, or just an answer. **What the operator says** is plain language, plus a small command vocabulary that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, `go`, and work-item selection. - **Commands are matched leniently:** case, surrounding whitespace, trailing punctuation and a leading mention of the agent don't matter. A message addressed to someone else isn't for this agent. - **Questions never change state.** A question gets an answer, never an interpretation as approval, rejection or command; a misread defaults to asking. - **One exception: a plain-language go-ahead.** "Yep, that's it" in reply to a restatement may be taken as `go` (D1), because a wrong reading costs only time. The agent says so before acting ("taking that as a go-ahead on rev-2"), and it binds to the revision up on arrival (L3). Everything else still needs explicit `approve`. - **Documents as input:** pasted, attached as a text or Markdown file, or a note in the vault the operator points to (L10). The message is the instruction; the document is material. - **Everything the operator says is recorded** in the transcript (L7). **What the agent says:** it explains as well as acts (a proposal with its rationale and alternatives, a revision with what changed, finished work with what was done, verified and left open); it says what happened to a message it can't act on yet, rather than refusing; progress is prose at a readable rate, with raw tool activity on request; and it asks when unsure, including mid-implementation. **Gitea carries work and records,** read as structured JSON (never through a chat bridge). Comments from allowlisted operators are instructions or approvals; anything else is data (L11). **Outages delay, never fail.** Intake retries indefinitely with capped backoff and logs the outage; interrupted outbound effects are covered by intents (L8). ### L6. Context and memory **1. The working context of one model call** is rendered from the log at call time for one role, fitted to that model's window, and never stored. Fitting never drops the system prompt, the current plan or the most recent turns; a call that still doesn't fit fails (L9). **2. The work-item transcript** (the log, L7) is durable and scoped to one item: nothing crosses items, and agent-level calls (switching, duty runs) see other items only as code-rendered summaries. - **Every entry is attributed** (operator, driver, helper, orchestrator). - **The orchestrator writes facts** (workspace prepared, milestone started or finished, PR opened or merged, approval given), so models learn what happened instead of inferring it. - **Each role gets its own view.** The driver sees the conversation, facts and helpers' reports; a delegated helper sees its brief, the current plan, the facts, its own work and the driver's work in the milestone as compact narration; the expert sees only its question. **No role sees another's turns as its own:** whatever isn't the viewer's own arrives labelled with its origin. **3. Scope memory** outlives work items: a repository's own documents (D2), read at work-item start, with the repository as the authority; for infrastructure, the world model in the vault (L10). **4. Agent memory:** the agent's own notes (the operator's preferences, lessons, recurring decisions and their reasons), one note per fact with an index, in its folder under `memory/` in the vault (L10). Read at work-item start and before decisions the notes cover. Visible to the operator, who corrects it through conversation. **5. Never memory:** Matrix history, provider-side session state, build artifacts, a previous task's workspace. **Writing memory:** at work-item close (what was done and decided, into scope memory or agent memory as it belongs); when a fact is learned, with its source and time; operator corrections verbatim, with attribution, never paraphrased. **Using memory:** everything read is snapshotted into the transcript with its provenance (source, revision or content hash, time), once per item. **Memory is a cache, never authority:** facts an action depends on are re-checked against the live system first, and a proposal resting on recalled facts says so. A note contradicted by a check is corrected, not worked around. ### L7. The canonical log Every work item's durable record is a **canonical event log** in the state store, never 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` / `tool_call` / `tool_result` | the model's turns, with canonical call IDs | | `plan_submitted` | a proposal put up for agreement, with its revision (L3) | | `fact` | what the orchestrator observed or did | | `delegation_started` / `delegation_completed` | brief, model, caps / handback, artifacts, cost, status | | `classifier_verdict` | action, scope reference, verdict, model | | `knowledge_retrieved` | text as read, source, revision or hash, time | | `compaction` | summary, producing model, range replaced | - **Every event carries its work item and actor** (L6). - **Nothing provider-specific is load-bearing:** IDs are minted locally, content is stored as bytes, and vendor-opaque reasoning is replayed only within the delegation that produced it. - **The log is never edited;** corrections and compactions are new events. ### L8. Durability and recovery **The state store** (SQLite) holds each work item's position, permissions, pending questions, cursors, pinned roles and log, never workspaces or artifacts. Every change is committed before the loop continues, and the schema is versioned with a migration hook. **Intents and idempotent effects.** - **Before any external side effect, record an intent;** after it, commit it. - **Every external effect is idempotent against external truth:** re-running an interrupted step detects and adopts what already happened (e.g. an open PR for the branch). What the operator has since undone is an answer, not a gap to fill: a PR they closed is reported, never opened again. - **Intake is taken at most once and answered at least once:** a durable cursor per external thread; an item being answered stays marked until done and is redelivered after a crash; comments the agent can't act on yet aren't consumed. **Boot reconciliation,** before accepting input: 1. Run pending migrations. 2. **Reconcile pending intents against external truth:** verified → committed; verifiably never happened → aborted; unverifiable → ask the operator. 3. **Adopt what external truth shows** (a published PR, an approval posted while down, a dependency closed). 4. **Resume waiting points as they were:** pending questions and open proposals are put up again, and watches resume from their cursors. 5. **Resume interrupted work only on evidence:** a delegation when the cut is clean; an ops step sequence when the log names the step in flight, its outcome checks out and its approval hasn't expired (O2); anything else is reported with what is known, and the operator chooses. Never a blind resume. **Verification:** these rules, and the loop's (L1, L2, D1, D4, D5), are checked by formal models (`specs/`, Quint) over interleavings of messages, comments, outcomes and crashes. Each counterexample is reproduced against the real code before it is fixed. ### L9. Models, roles and helpers **LiteLLM is every agent's only LLM destination.** It holds the vendor keys (each agent holds one LiteLLM token), enforces budget caps per key, logs, and routes fallbacks; adding a vendor never touches an agent. **One client interface,** `complete(events, tools, model) → events`, over the log; long sessions use vendor passthrough so vendor features aren't lost. **Roles** resolve to models in configuration: `driver` (drives the loop), `helper` (delegations), `expert` (`ask_expert`), `classifier` (L4). The mapping is **pinned when a work item opens**, so a configuration change can't split one item across models. Each model carries a **capability profile** from benchmarks (tools, window, strengths, weaknesses), used whenever one role is briefed about another. **Escalation:** the driver calls a helper when it chooses to (its prompt treats asking as normal) and on observable triggers (repeated failures, a context that won't fit, turn or time caps). **Delegation:** a frontier model takes over a scoped sub-task with tools. - One harness for every vendor, not vendor agent SDKs. - Bound by the work item's scope (L3) and the same three layers (P4). - On its own branch; abandoning it discards the branch. - Hard caps on turns, tokens and time; checkpointed, so it can resume (L8). - Hands back a note for the driver (what changed, what is now true, what next): that note and the files are the whole interface. **`ask_expert`:** one stateless question, text answer, with a per-item budget so a confused driver can't circle. **Small models** get reduced tool schemas and a tool-call contract the loop enforces itself where the server can't. **Evaluation capture:** delegations, `ask_expert` calls and classifier verdicts are recorded as input/output pairs with outcomes, so changing a model is a re-run, not an argument. ### L10. Knowledge vault Durable knowledge outside any repository (world models, agent memory, the operator's material such as procedures) is **a folder of Markdown files on the agent host** in the format Obsidian reads (frontmatter, `[[links]]`, tags), so the operator browses it in Obsidian. - **Agent memory is under `memory/`,** a folder per agent. Other folders' names and layout grow with need. - **Every folder has exactly one writer,** an agent or the operator, and **mounts enforce it** (P4): each agent gets the folders it writes read-write, those it may read read-only, and nothing else. No agent reads another's memory; `nisse-dev` gets no infrastructure material, which would map what its builds are kept from. - **Links point from memory to shared notes,** never back. - **Access is through control tools** (L13); no tool VM sees the vault. The harness writes atomically and records each note's provenance (source, work item, time). It may also keep the vault's history in git; recovery doesn't depend on it. - **The operator's copy** is kept by Syncthing, with direction per folder: agents' folders, memory included, are send-only from the host; the operator's are receive-only on it. So conflicts can't arise. - **Never an instruction or approval channel.** Work is never read out of the vault, and no agent writes there to make another act; agents coordinate through the operator, Matrix or issues (L1). The operator points to a note in conversation when it is input (L5). - **Corrections go through conversation** (L6). Break-glass: stop the agent, edit on the host, restart. - **Procedures are bound to ops policy entries by path** (O2) and read whenever a plan uses the entry; search exists in addition, never instead. A missing bound procedure stops planning that action; other failed reads and writes never block work. ### L11. Prompt injection **Any model that reads untrusted content may be steered by it,** and no marker reliably separates data from instructions. So the design limits what a steered model can do rather than trying to prevent steering. - **Hard walls hold against a steered model** (P4): nothing a model says grants a permission, widens a scope or reaches a credential it doesn't hold. - **Instruction channels are operator-scoped:** Matrix from allowlisted identities, comments and assignments from allowlisted accounts. When someone else wrote an issue, the operator's assignment is the instruction and the text stays data. - **Intake is parsed into structured fields in code** where possible, and what reaches a model is labelled by source (L6). - **Approval messages are rendered by code;** the classifier never sees untrusted content (L4). - **Some injection is accepted because it can only reorder work** (an alert title biasing which item is next). - **The lethal trifecta** (private data, untrusted content, a way out) is checked per agent. In `nisse-dev`, untrusted content meets command execution only on build VMs, which hold nothing 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,** on the agent host, mounted only into its container, never baked into images. An agent may structure its set (`nisse-dev` by project, D7). - **The harness hands secrets out per operation,** decided by code and configuration, never by a model, and never on a command line. - **Every value handed out is redacted** from tool output before it reaches the transcript, a model or a log. - **Each MCP server instance** has its own minimally scoped credentials (X3). - **Only credentials are secrets;** every other setting is reviewable configuration. **Identity:** each agent is its own LLDAP principal in a tightly scoped Authelia group, never the operator's account, with its own account and token in each service it uses (X5). **Logging:** structured JSON lines to the host journal and the log stack. Every durable state change emits exactly one line, with a fixed envelope (time, agent, event, work item, cause); every verdict, escalation and external effect is logged. **Logs are never load-bearing:** nothing reads them to derive state. ### L13. Tools and execution | Kind | Examples | Where it runs | |---|---|---| | **Control tools** | `ask_human`, `ask_expert`, `start_milestone`, `finish_milestone`, notes (L10) | In the harness; they act only on the agent's own state, conversation and records | | **Execution tools** | Commands; reading, writing and listing files | On a tool VM, over SSH, through `execute(host, …)` | | **MCP tools** | Proxmox and UniFi APIs | In MCP server containers beside the agent (X3) | - **`execute(host, command, *, secrets, stdin)` is the only way to run anything.** There is no local execution path, so **the agent host never runs a command a model chose** (X1). - **The harness binds the host,** not the model: for `nisse-dev`, the work item's build VM (D3). Where a model must choose (ops: its tool VM or a device), it picks from the hosts the harness allows for that item; others are refused before connecting. - **MCP tools are the one deliberate exception:** fixed code against an API, with the model choosing only arguments. They still pass through P3, P4 and L4, and their output is redacted (L12). - **Tests use SSH too,** against a throwaway `sshd`, so the tested path is the production path. --- ## Part D — `nisse-dev` Designs and builds software projects, producing reviewed changes in Gitea. ### D1. How it works Work arrives as Gitea issues or conversation, and leaves as reviewed PRs. **Intake.** - **An issue assigned to `dev-agent` is a work item;** assignment says whose job it is. The agent acknowledges it ("queued myproject#14, 1 ahead"). Your issue is the item's record. Only issues filed or assigned by allowlisted operators count (L11). - **Unassigned issues are backlog,** never work, though the agent will discuss them. - **An issue assigned again after its item closed is a new item,** which notes the one before it; an issue whose item is still open, or whose assignment predates the close, stays with that item. - **Open Gitea dependencies make an issue blocked** (L1). - **Work that starts in conversation gets a record** once it is work on a project, not at its first message: the transcript already keeps the conversation. The agent opens an issue, assigned to itself, that carries the work item's ID from the moment it is created. Intake ignores any issue carrying an ID, so a record is never taken for new work, even before the agent has stored the link. This is the only time the agent assigns itself. **Starting an item.** The agent reads the project's documents (D2), then **restates its understanding** in the item's thread: what it takes the request to mean, how it fits, its first step. Labels steer what happens next: | Label | After the restatement | |---|---| | `bug` | Proceeds after a grace period (the item waits on the operator meanwhile) | | none, or anything else | Waits for a go-ahead: `go`, or a plain-language yes (L5) | | `discuss` | Talks it through before anything | | `design` | Agrees the design (D2) before any code | - **Until the go-ahead it only looks:** reading and checking (versions, builds, tests) run; anything that changes something is refused by the fast path (P4). What the work will need, a toolchain change included, goes in the restatement, so the go-ahead covers it. - **It also talks first** when it finds the report unclear (it asks instead of restating) or can't reproduce a bug. It never fixes a bug it couldn't see. - **It never classifies an issue itself;** it may suggest a label. A missing label costs at most a wait. **The flows.** - **A bug:** reproduce (ideally as a failing test), fix, verify, PR. - **A feature:** agree the design in conversation, write a build plan if it's longer than about one PR (D2), work through it as milestones (D4). - **A new project** starts as a `general` conversation, however vague. Nothing needs Gitea before the repository exists: drafts live in the conversation (a Markdown file in the thread if long). 1. **The proposal:** a name, what it's for, and the decisions provisioning depends on (toolchain, so the build VM template; services; secrets; egress). The operator agrees as to a restatement. 2. **A provisioning checklist** in the thread: a repository with protection, the agent as a collaborator with write access, a build VM, and the project entry (D8), given as configuration to add. The operator provides them (later `nisse-ops`, O6). 3. **Verified, not assumed:** when the operator says done, the agent checks the repository, a push to a branch, that protection refuses it on the default branch (D4), and the build VM. 4. **The record:** a tracking issue ("implement the build plan") whose body is what was decided, distilled, linking the conversation's item. The raw conversation isn't carried into the project's context: it holds ideas that were dropped, and the documents are what gets reviewed and what later work reads. 5. **The architecture lands in one or more PRs,** treated as `design`, then the plan's milestones. `general` never gets a build VM: an experiment that needs one (does a library parse this format?) is a reason to start the project for real. - **Progress** goes on the record issue: facts posted by the harness, prose by the agent when there's something to say. - **Finishing:** the last PR closes the issue on merge ("Closes #N"; earlier ones say "Part of #N"). If no merge closes it, the agent closes it with a note, after checking what is left open. **Filing issues from conversation** (later). When the operator asks or agrees, the agent files an issue on any project where it is a collaborator: unassigned, so it can't create work for itself; after checking for duplicates; with a note of the conversation it came from. ### D2. Documents A project's own documents live in its repository: - **`ARCHITECTURE.md`:** what the project is, how it fits together, and why. Kept current by the agent; a design change updates it. - **Build plans** (e.g. under `docs/plans/`): how a body of work gets done, as milestones. The agent's working documents, for staying organised across context windows and sessions. **Neither is a gate;** both are reviewed in PRs. - **A small architecture change** goes in the PR with the code; the agent may raise a larger one first, and `design` makes that mandatory. Otherwise it's the agent's judgement, and a wrong call costs a review, not safety. - **A build plan is written for work longer than about one PR** and committed with the work, so every PR shows it. The first PR carries the whole plan, so a wrong direction shows at the first review. The operator doesn't approve plans; the agent asks for feedback when it wants it. - **A plan specifies; it doesn't implement:** per milestone, the goal, interfaces and behaviour, edge cases, and **tests as acceptance criteria**, with code only where code is the clearest specification. Detail is sized to the driver's capability profile (L9). It may note where the agent will check in, and which milestones change no code. - **Progress is recorded in the plan itself,** milestones marked done by each PR. The framework never parses a plan. ### D3. The workspace **Each project has its own build VM** (X1), where its workspace lives and its commands run. It is kept between work items (toolchains and package caches stay warm), collected after the project has been idle long enough, and re-created from the template plus the project's recorded toolchains. Work pushed to Gitea survives; an unpushed commit may be lost when a VM is wiped, and that is accepted. **A fresh, clean workspace when a work item starts:** fetch, check out a clean tree at the latest default branch, and **set aside**, never silently discard, anything left over (uncommitted changes, local branches, unpushed commits), reporting it. Build artifacts and caches in the workspace never survive into a new task. **The toolchain step** makes the build VM and the project's own files agree: a bounded run of the item's driver model that installs what the project needs and keeps its pins, setup documentation (the README or wherever the project keeps it) and CI configuration in step. **The repository is the record:** reviewed in PRs, surviving any VM, and the same instructions a person cloning it would follow. It starts two ways: - **The harness starts it when the build VM isn't set up for the project** (new, rebuilt or wiped, which the model can't know): before every run it checks the VM's baseline, the workspace clone (cloned again if gone) and a per-project marker that a finished step leaves. This step sets the VM up from the repository and changes nothing in it; files out of step with reality are reported, for the driver to act on. - **The driver requests it** when the work needs a toolchain change, once the work is agreed (D1); its edits land in the current milestone, so they are reviewed with the work. **Versions are facts the harness reads itself** (L6): it runs each tool's version command (version commands only), checks them again before later runs and reports a change, and a PR says what its work was tested with. **A rebuilt VM's new host key** is trusted only on the operator's word. **The build VM is the agent's to change:** installing packages and toolchains there, cleaning caches or reinstalling dependencies to fix a broken build are contained, reported but not asked about. Changes meant to last go into the repository's files through a toolchain step; recurring ones go into the template (X4). Installing on any other host is dangerous. ### D4. Git, branches and PRs - **The harness owns branches;** plans and models never create or switch them. - The workspace is never on the default branch: a work item starts on a branch of its own. - `start_milestone(name)` makes the harness cut the milestone's branch; `finish_milestone(summary)` makes it commit, push, open the PR and record the fact. The model decides when, from its plan. - **Mistakes are made harmless:** finishing without a start cuts the branch then; a second start while one is open is refused; work outside a milestone is still on a harness branch. - **Wait for the merge between milestones:** the next one starts from the default branch only after the previous PR has merged (read from Gitea). Meanwhile the item waits on the operator and the agent does other work. A review comment makes the item ready (D5); a PR closed without merging makes it ready with that fact, and the agent asks what next. Redoing the milestone gets a new branch and PR, and the merge rule still applies to it. - **Parking pushes the item's work:** its branch, and anything uncommitted to a side branch of its own, never onto a PR's branch, which may have merged meanwhile. Another item on the same project starts fresh, and this one resumes where it was. - **Publish only the work:** what the agent changed, counted from where the milestone started, never build artifacts; anything left out is reported. - **Pushes run on the build VM** with a push credential (a deploy key, or a push-only account's token) the model's commands never receive (L12); PRs are opened from the agent's container with its API token, which never reaches a build VM. Gitea is the only copy that has to survive. - **Publishing is idempotent** (L8). A milestone with no changes is done if it wasn't expected to change code; otherwise it's raised with the operator. - **The agent never owns or administers a repository:** whoever does can turn protection off. The operator (later `nisse-ops`) creates and protects repositories; `dev-agent` is a collaborator. - **Protection is verified, not assumed:** the agent's token must be refused when pushing to the default branch and when merging without the approval the trust dial requires. A test asserts both. - **A runnable test service's address** goes in the PR description. ### D5. Review and merge - **Operator comments on a PR** (conversation and inline) are instructions; the agent answers with commits to the same PR, replying where the comment was made. Intake is as in L8. - **The PR may have moved on meanwhile.** After pushing a fix, the agent checks the PR: if it merged before the fix landed, the fix goes out as a new PR; if it was closed, the agent says so and asks. - **Merging follows the trust dial** (L3), set per project (`trust.merge`). By default the operator merges. Set to `approved`, the agent merges a PR once an operator has approved its current commit and its checks pass; branch protection requires that approval from an operators-only whitelist and dismisses stale approvals, so Gitea enforces the rule, and the agent checks at start that its own unapproved merge is refused. ### D6. Action classes | Class | Examples | |---|---| | Harmless | Reading the repository, running builds and tests on the build VM, querying Gitea | | Contained (in scope) | Editing the workspace; milestone branches, pushes and PRs; commenting on its issues and PRs; filing agreed issues (D1); changing its build VM (D3) | | Dangerous | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, changing repository settings, anything outside the item's repository or build VM | Changing branches by hand is neither: it is refused, with a pointer to the milestone tools, since the harness owns branches (D4). Deleting on the build VM is contained, as changing it is; only what would break the host (system and home directories, `dd`, `mkfs`, `reboot`) is dangerous, since in tests the "build VM" is the test machine itself. ### D7. Deployment and isolation **The agent** is a container on the agent host (X1) with its state, secrets and API tokens. It reaches LiteLLM, Gitea, Matrix and SSH to its build VMs, nothing else. **Build VMs** (one per project, D3) are where untrusted content meets command execution: - **A real Docker daemon,** since projects build and run containers. - **Sealed:** egress only to package sources and Gitea's git endpoint, plus what its project entry (D8) declares its tests need (e.g. LiteLLM for the `nisse` project itself), reviewed like any allowlist (X2). Ingress only SSH from the agent host. No other route into RFC1918, and none to the agent host or the tailnet; no Tailscale. - **Their own VLAN,** with test services on predictable ports, and test containers on an internal network with no route out. - **A headless browser** reaches only the internal test network, to exercise what the agent built. No SSO. **Secrets are namespaced by project.** A build VM's commands receive only their project's, and each secret's configuration says whether the model's commands may see it or only harness operations use it (the push credential). Anything the model's commands can see must be scoped for testing, never production. **Provisioning:** build VMs come from a template via Terraform; eventually at `nisse-dev`'s request to `nisse-ops` (an issue in the infrastructure repository, O5), until then by the operator. ### D8. Projects **A project is an entry in the agent's configuration,** reviewable and changed by PR like any configuration (L12). An entry is what makes a project known to the agent, and it holds everything per-project the rest of this part refers to: - the repository and its default branch; - the build VM and the template it comes from (D3); - the trust dial (L3) and label defaults (e.g. always discuss first, D1); - the project's secrets by name, each marked as visible to the model's commands or harness-only (D7); - egress its tests need beyond the default (D7). **For a new project,** the agent proposes the entry alongside its provisioning request (D1); the operator merges it when the repository and build VM exist. The agent may propose entries and changes, but never changes the trust dial or a secret's visibility itself. --- ## Part O — `nisse-ops` Operates infrastructure across both sites, driven by issues 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 | often none (`zfs destroy`, a firewall lockout) | | Scope | the assigned issue and its conversation | approved step sequences (O2) | | Dangerous actions | discuss | discuss, and exact-step approval | | Work shape | milestones → PRs | short reactive tasks → live changes | | Intake | issues in the project's repository, Matrix | issues in the infrastructure repository, alerts, duties, Matrix | | Reach | sealed build VMs | Tailscale, credentials, both sites | They are separate deployments over one library; one instance playing both would collapse the isolation. ### O2. Approved step sequences **Every infrastructure write needs an approved plan,** and approval covers exactly the steps written. This is ops' scope (L3) and a hard wall in its own right. - **Write credentials exist only in elevated MCP instances** (X3), mounted after approval; read-only ones are always mounted, so looking around needs no plan. - **A plan is a list of literal steps:** tool and target, full arguments (a config change carries its exact diff), an optional **precondition** checked just before, an optional **expected outcome** checked just after, and a retry allowance (default one). Prose explains; it is never scope. - **The approval message is rendered by code from the steps,** flagging anything irreversible, anything touching a protected target, and anything that takes down the agent host (which runs every agent, ops included). - **Execution is matched against the steps.** Reads are always allowed; an elevated call runs only if it exactly matches the next unfinished step (arguments normalised by schema). Anything else halts and asks. A failed precondition or outcome halts without retry. - **Approvals expire** if not started in time. - **Policy is small, conservative data:** a versioned file declaring each MCP instance's privilege, protected targets (the NAS, gateways, the agent host, tool VMs) and each elevated tool's target argument. Anything unclassified is elevated and protected. Server hints can only make a tool stricter. A model may propose entries as a PR; nothing is classified at run time. **Parked until real friction shows the shape:** bounded matching (calls within an approved target and action, for reversible tools), and standing approvals (a template with a trigger, rate limit and expiry, for a routine fix). ### O3. Action classes | Class | Examples | |---|---| | Harmless | Metrics, logs, status, read-only MCP calls, issues, the world model | | Contained (in scope) | Approved steps with a known undo; its own world model; commenting on and filing issues in the infrastructure repository | | Dangerous | Every other infrastructure write; browser actions on admin UIs (each confirmed); **anything on network gear**, the most conservative case, since a bad change can sever the path to fix it | ### O4. Interfaces - **MCP servers** for Proxmox and UniFi at both sites: the primary path (X3). - **A browser** for admin UIs without an API; only a fallback for network gear. - **SSO** as `svc-nisse-ops`, whose blast radius is its Authelia scope. - **Event intake** as structured JSON (directly or via Alertmanager); curated updates go to `#ops`. ### O5. Issues and duties - **One infrastructure repository for both sites,** holding ops' issues (it need hold no code). Sites are labels (`garrett-hall`, `hartman-hut`), so cross-site work is one issue. Intake works as in D1: an issue assigned to the ops agent is a work item and its record, labels steer, and Gitea dependencies mark it blocked. Matrix is the live conversation. - **Action is operator-scoped.** Issues from duties (findings, opened by ops itself and deduplicated by fingerprint) and requests from other agents (L1, e.g. `nisse-dev` asking for a build VM, which then waits blocked on that issue) may be picked up and planned, but any write needs a step sequence the operator approves (O2). Until standing approvals exist, that means every provisioning request. - **Comments on the issue approve its plan** (`approve rev-N`), as an alternative to Matrix (L3). ### O6. Deployment - **A container on the agent host** (X1), with its MCP servers beside it. - **Its own tool VM,** long-lived, for network tools and its commands (L13), with Tailscale and the reach they need at both sites. - **Egress** includes the Proxmox and UniFi endpoints at both sites. - **Provisions build VMs for `nisse-dev`** through Terraform, under O2. --- ## Part X — Deployment (all agents) ### X1. The agent host and tool VMs **One agent host,** stable and long-lived. Each agent is a container on it with its own volume (state, transcripts), secrets (L12) and network; no agent can reach another's. Its MCP servers run beside it (X3). The vault lives here (L10). **It never runs a command a model chose** (L13), which is what makes sharing it safe. **Tool VMs** are where model-chosen commands run, reached only over SSH from the agent host, each built from a template plus recorded installs and disposable: - `nisse-dev`: a build VM per project (D3, D7); - `nisse-ops`: its own tool VM (O6); - a purely conversational agent: none. **Per-agent attribution:** everything an agent holds is issued to its own identity, so every upstream action traces to one agent. ### X2. Egress - **Default-deny allowlists,** versioned next to the agent's definition, each entry justified by what needs it. - **Agent containers:** the union of what the agent and its MCP servers reach, enforced by Docker networking and the agent host's firewall. The site firewall sees the host as one address; that is acceptable because nothing on the host executes untrusted input. - **Tool VMs:** enforced at the VM, its Docker network and the site firewall on its VLAN, because this is where untrusted input meets execution. They never reach the agent host except as replies on its SSH sessions. - **The tailnet is `100.64.0.0/10`,** not RFC1918; deny it explicitly where it isn't needed. ### X3. MCP servers - **One instance per agent,** beside it on the agent host: a shared process would share a credential and lose attribution in the upstream audit log. - **One instance per (service, site, privilege),** each with its own credentials, so a wrong-site or wrong-privilege action needs a tool that isn't mounted. - **Per-task tool profiles** mount only what a work item's scope needs. - **Rejected: search-then-dispatch meta-tools** over large catalogues (extra round trips for a local driver, opaque arguments, a tool list that defeats caching). ### X4. Images, VMs and restarts - **Stateless images, named volumes** for state, on the agent host. Images are built by Gitea Actions and pulled by Watchtower, which **never restarts an agent mid-work** (an idle flag gates the restart); crashes are handled by L8. - **Third-party MCP images are digest-pinned** and updated deliberately: they hold root-equivalent credentials. - **Tool VMs come from templates** provisioned by Terraform; recurring installs are baked in. They are rebuilt, never snapshotted. - **The agent host's state is snapshotted** (scheduled, and on each approval), and everything restarts on boot. ### X5. Supporting services All self-hosted: Matrix (Synapse), Gitea, LiteLLM, the local inference host, Vaultwarden, LLDAP with Authelia, and Syncthing for the operator's copy of the vault. Each agent has its own account, token and room in each; nothing is shared between agents. --- ## Part Q — Open questions - **The classifier's model,** and how its verdicts are calibrated against overrides. - **Recognising questions:** an explicit marker or classification; a misread asks. - **How far the local driver goes,** measured on real work (P7). - **One local model for every agent:** preferred, since the inference host keeps one resident; otherwise agents take turns. Measured first. - **Vision,** for 3D design and video work: images in tools, the log and the budget. - **More than one active work item:** needs per-item serialisation, an inference scheduler and per-item intents. Build VMs and threads already separate workspaces and conversations. - **Stacked PRs:** starting a milestone before the previous one merges, after experience with waiting (D4). - **Build VM collection:** how long idle, and whether it needs the operator's word. - **Where secrets come from:** by hand, synced from Vaultwarden, or Terraform. - **How ops' MCP servers reach the second site** from a shared host (Tailscale on the host serves every container). - **Browser access to network gear:** standing, or handed off per task. - **Relocating a stateful MCP server** off the agent host, if one ever justifies losing attribution. - **The state store's engine.** SQLite, one database per agent, for now. If scaling or backups call for it, a server database such as PostgreSQL on the agent host (still one database and role per agent). - **`nisse-ops` issuing credentials** to other agents: an SSH key for a new VM, Gitea credentials for a new project. Writing into another agent's secret set crosses the isolation in L12, so it needs a designed channel and its own approval rule. - **Releases.** Building and publishing artifacts at each release, usually by tag, sometimes from release branches: what the agent may do (a release is visible beyond its own area, so dangerous unless the trust dial says otherwise) and how a build plan names one. - **Recovering the agent host.** It is a single point of failure for every agent, `nisse-ops` included, so its recovery can't depend on ops running. A recovery plan (rebuilding the host, restoring state from snapshots) is ops work still to be written.