Back on a PR it reviewed: a new head gets a later review of the commits since (each one's diff, through the API) and of its earlier points; what was said since gets replies, answered where it was said or skipped with a reason. It signs off once and stays quiet while still happy. Past tuning.review.max_rounds it sums up for the operator and goes quiet, until a push or someone other than the PR's author joins in. An operator's `stop` quiets it; `review` asks for a fresh review. Reviews carry numbered markers and replies their point's key, so recovery posts nothing twice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
64 KiB
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) |
| Review Agent | nisse-review |
Reviews pull requests (Part R) |
| 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:
- 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).
- 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).
- 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
- 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.
- 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.
- 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 fixed the usual ways, and replaced when that doesn't work: the agent may ask for its build VM to be reimaged rather than diagnose it at length.
- 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. In code it is the nisse_core
package: the transcript (L7) and its renderers, the LLM client and model catalog
(L9), context fitting and per-role views (L6), the state store and reconciliation
(L8), transports (L5), SSH execution (L13), the vault and memory (L10, L6.4),
logging and secrets loading (L12), the Gitea client (L1), shared configuration,
and the parts of a loop (L2): the driver run, the common control tools,
ask_expert, and the vault and memory as tools. An agent extends the store with
its own tables, roles and views, and keeps its own scheduler.
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 |
generalis 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. stopworks in every state: it ends the current turn or delegation at the next tool boundary, unmounts elevated credentials, and asks what next.abandonends 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-Nnames 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,
statusis about the agent andstopstops 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
generalscope 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 explicitapprove. - 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), with the repository as the authority, read by the driver as it needs them (not put into every item's context); for infrastructure, the world model in the vault (L10). What the agent knows about a project that doesn't belong in its repository is agent memory kept with that project (below). A memory note that disagrees with the repository is corrected.
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 the
harness keeps, in its folder under memory/ in the vault (L10): general notes at
its top, a project's under projects/<project>/, written only from that project's
items. The model
writes, updates and forgets them itself, without asking; each change is said in
the thread. The general index and the item's project's are put into the
transcript at a work item's start; a note is read before decisions it covers. Visible to the operator, who corrects it
through conversation, not by editing it.
5. Never memory: Matrix history, provider-side session state, build artifacts, a previous task's workspace.
Writing memory: when a fact is learned, with its source and time; at work-item close, by a bounded pass of its own (what was decided and learned, into agent memory; repository documents it left out of date are flagged to the operator, since they change in PRs), so it never depends on the model thinking of it; operator corrections faithfully, with attribution. Paraphrase is fine: after a back-and-forth, the gist is what matters.
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:
- Run pending migrations.
- Reconcile pending intents against external truth: verified → committed; verifiably never happened → aborted; unverifiable → ask the operator.
- Adopt what external truth shows (a published PR, an approval posted while down, a dependency closed).
- Resume waiting points as they were: pending questions and open proposals are put up again, and watches resume from their cursors.
- 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 plain format SilverBullet reads (frontmatter, [[links]], tags), so
the operator browses and edits it in SilverBullet.
- 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-devgets 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 access is SilverBullet on the vault itself, in its own container beside the agents: one copy, nothing synced. It mounts the operator's folders read-write and the agents' folders read-only, so the kernel keeps one writer per folder even within one SilverBullet space, and it sees nothing of the agent host but the vault. Agents never go through SilverBullet: they keep working while it is down, and their mounts keep them apart.
- 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. Anyone else's comments are a peer's (R4), whoever wrote them: another agent, a person, a bot. They are weighed on their merits, labelled with their author, and never instructions.
- 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-opshas 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-devby 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-agentis 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
generalconversation, however vague. Nothing needs Gitea before the repository exists: drafts live in the conversation (a Markdown file in the thread if long).- 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.
- 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). - 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.
- 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.
- The architecture lands in one or more PRs, treated as
design, then the plan's milestones.
generalnever 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
designmakes 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.
A broken build VM blocks its projects. When the usual fixes don't help, the
driver asks for the VM to be reimaged from its template. The harness pushes the
work in progress first, then blocks the VM: nothing runs on any project on it,
across restarts, until the operator says vm ready (which also trusts its new
host key), and a reimaged VM is set up again by the toolchain step. A VM that
can't be reached blocks the same way, whether the check before a run finds it
or a run loses it (which ends that run there); it is tried again in the
background, and work goes on by itself once it has answered for a while. A VM
that answers with a new host key blocks until the operator's vm ready.
Questions are still answered while a VM is blocked, without it. The operator
recovers a VM for now; nisse-ops later (O5).
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-agentis 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: on the same line for an inline comment (quoted instead when Gitea won't place it there). Intake is as in L8.
- Anyone else's comments are a peer's (
nisse-review, Part R, or any other account; its own are skipped): constructive criticism, not instruction. Each comment, review body or inline comment is a point with an id, kept on the item and labelled with its author. The agent answers every point where it was made, changing the code within the item's scope or pushing back with its reasons, and asks the operator about anything that would change the scope. A point that needs no reply (an automated notice) is skipped with a reason, which is recorded and said in the item's thread. The harness holds this, not the model: a run that would go back to waiting with points open is reminded once, then the harness lists the unanswered ones on the PR. Points still open when the PR merges or closes are dropped, with a fact saying so. - A peer's comment wakes the item like an operator's, but going back to waiting afterwards says nothing to the operator beyond one line in the thread (what was answered and skipped). A peer's comment never approves, never counts as a go-ahead and never runs a command. The agent doesn't wait for a review before asking the operator to merge: the operator may weigh in on the discussion.
- 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 toapproved, 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
nisseproject 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 R — nisse-review
Reviews every pull request that is ready, in the repositories it can see, and
discusses its points on the PR. It reads and comments; it never changes code. Its
purpose is a second, always-available look that pushes nisse-dev (and anyone
else) towards better changes.
R1. How it works
- The duty: watching its repositories for PRs ready for review: open, not
a draft, and not marked work in progress. Any author's: the operator's,
nisse-dev's, or another tool's. Its repositories are the ones its Gitea account was given, or, when configured, those matching patterns (dev/*,*/*) among all it can read, public ones included, so a public repository needs no grant in Gitea: access to public repositories is managed in configuration, and to private ones in Gitea. - A work item per PR, scoped to its repository (L1). It closes when the PR merges or closes.
- What starts a run: the PR becoming ready, a new push, or a new comment or review on it. A new head's review waits for its checks to finish (passed or failed), so it is judged with the CI result and stays off a GPU that CI is using; up to a tuning limit, and not at all where there is no CI.
- The first review is given the PR (title, description, its commits and diff, its comments, and the issue it closes); the reviewer reads the rest as it needs it (R3).
- A later review covers what changed since its last one and its earlier points: whether each was addressed, answered, or left alone, and it says so when one was left unaddressed.
- What it checks: code health; consistency with the PR's description, its issue, and the repository's documented architecture and conventions; tests for what changed, judged by reading them; and the CI result as Gitea reports it.
- It discusses on the PR, in inline threads and top-level comments, and
answers replies to its points: it concedes a good argument and holds a point it
still believes, with reasons. Each thing said to it since its last look is
answered where it was said, or skipped with a reason (a bot's notice, say):
the model decides, as
nisse-devdoes (D5). - Nothing to raise is said once, as a short sign-off review. While it stays happy, a later look that finds nothing new posts nothing.
R2. Rounds
Two agents can argue forever, so a PR's exchanges are capped.
- A round is one review or set of replies the reviewer posts. After a capped number of rounds (a tuning value) it posts a summary of what is still open, for the operator, and stops answering on that PR. Pushes have no cap of their own: each brings new code (operator).
- The count resets when someone other than the reviewer and the PR's agent author comments (in practice, the operator has joined the discussion), and when the PR gets a new push: new code is worth a fresh look.
R3. Authority and reach
- Read-only on the repository: code at any ref (not only what the PR touches), documents, issues, other PRs, CI status, through its Gitea account. The wall is checked at startup and every round: an account that can push to, or administer, a repository it reviews is refused, and stops if granted that while running. Reads are bound by the harness to the item's repository, so what it might repeat in a comment is already visible to that repository's readers.
- It writes only comments and reviews on the item's PR. Its reviews never approve and never request changes: the operator's approval alone counts for merging (D5), and Gitea's branch protection enforces it.
- No execution for now: no build VM and no commands (L13); coverage is judged
by reading the tests until a CI coverage report exists. If a reason appears
(running tests itself, measuring coverage), it can have a build VM of its own,
like
nisse-dev's (D3, D7). - Its own memory (L6.4), with notes per repository (L6.3), so it learns each project's conventions and what the operator accepted or overruled.
| Class | Examples |
|---|---|
| Harmless | Reading the repository, its issues and PRs, CI status, its notes |
| Contained (in scope) | Reviews, comments and replies on its item's PR; its own notes |
| Dangerous | Nothing is offered: no approval, no merge, no other repository |
R4. Its peers
- Its comments are a peer's (L1, L11):
nisse-devweighs them and answers each point (D5). Neither side's comments are instructions to the other. - The operator's comments on a PR are the operator's: the reviewer may take them as settling a point, and stops pressing it.
- Prompt injection (L11): PR text and code are untrusted; a steered reviewer can only post a misleading comment on that PR, which the operator reads with the rest.
R5. Models and deployment
- The same local model drives it as
nisse-devfor now (P7), withask_expertfor a frontier model's view (L9). - No Matrix room for now: the PR is its conversation. If every agent gets a
room, it gets one too. Until then the operator stops it on a PR by commenting
stopthere (a review is asked for again withreview: a fresh one of the whole PR, with the round count reset), and stops it entirely by stopping its container. A command is an operator's whole top-level comment, acknowledged with a reaction rather than a comment the PR's author would have to answer. - Its own identity (L12): a Gitea account (
nisse-review) with read access to the repositories it reviews, its own token and LiteLLM key, and its own container on the agent host (X1), reaching only Gitea and LiteLLM.
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-devasking 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-devthrough 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.
- Avoid search-then-dispatch meta-tools over large catalogues (extra round trips for a local driver, opaque arguments, a tool list that defeats caching). A special-purpose server that works that way may still be worth it, as a deliberate exception.
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 SilverBullet for the operator's view 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-opsissuing 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.
- Letting
nisse-reviewapprove a limited subset of PRs: which subset, and how Gitea would enforce it (R3). - Recovering the agent host. It is a single point of failure for every agent,
nisse-opsincluded, 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.