Before each driver call, over compact_at (0.6) of the prompt budget, the same model is asked for notes over the very request it was about to get, plus one message, so the server's cache covers all but the notes. The notes, with code's list of the calls they replace, become a compaction event that renders in place of its range, keeping word for word what started the work and the last few turns. Rare, large steps keep the prompt's start unchanged between them. A later compaction folds in the earlier; one from an earlier run's view is hidden. Failed notes are logged and fitting takes over. Fitting now applies compactions before it measures, records what it cuts (Trim) and logs each cut as context.trimmed. Wired into both agents' drivers and the benchmark; nisse-dev's helper loop is a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
1176 lines
65 KiB
Markdown
1176 lines
65 KiB
Markdown
# 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:
|
|
|
|
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
|
|
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` |
|
|
|
|
- **`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.
|
|
- **A long run is compacted:** well before the window fills, the model's own
|
|
notes replace the middle of its run in one step (a `compaction` event, L7),
|
|
keeping word for word what started the work and its last few turns. Rare,
|
|
large steps keep the start of the prompt unchanged between them, so the
|
|
model server reuses its cache.
|
|
- **Fitting is the last guard:** it never drops the system prompt, the current
|
|
plan or the most recent turns, and a call that still doesn't fit fails (L9).
|
|
Each cut it makes is logged; after compaction one is a sign of trouble.
|
|
|
|
**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:
|
|
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 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-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 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-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.
|
|
|
|
**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-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:
|
|
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 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 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-dev` does (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. The code is a git
|
|
clone kept on its host between runs, checked out at the PR's head (and its
|
|
base when asked); git runs only the harness's fixed commands, walled off from
|
|
the host's git configuration and from anything the code could make it run
|
|
(links, hooks, submodules).
|
|
- **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-dev` weighs 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-dev` for now (P7), with
|
|
`ask_expert` for 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
|
|
`stop` there (a review is asked for again with `review`: a fresh one of the
|
|
whole PR, with the round count reset), and stops it entirely by stopping its
|
|
container. Stopping it ends a review in progress at once: at its next turn,
|
|
or by breaking off the model call it is waiting on. Nothing drafted is
|
|
posted, and the PR is reviewed again when it starts, with no failure
|
|
counted. 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-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.
|
|
- **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-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.
|
|
- **Letting `nisse-review` approve 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-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.
|