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