From 230b927ebe0461a460384b13b430600f82909101 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 16:35:55 +0000 Subject: [PATCH 01/12] Revise the deployment model: an agent host and tool VMs Agents run as containers on one stable agent host that keeps their state and secrets and never runs a command a model chose. Model-chosen commands run on disposable tool VMs over SSH: a build VM per project for nisse-dev, its own tool VM for nisse-ops. Secrets are a set per agent, handed out per operation by the harness and redacted from tool output. MCP servers stay on the agent host as the one exception. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 261 +++++++++++++++++++++++++++++++------------ 1 file changed, 189 insertions(+), 72 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 97d4406..072d454 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -1,8 +1,9 @@ # Nisse — Architecture -**Status: agreed (PR #83).** How and in what order it gets built is in -`docs/Development-Plan.md`. It replaced `docs/Architecture-Build-Spec.md`, which -remains in git history; its section letters (A–H) are not reused here. +**Status: agreed (PR #83); deployment model revised (PR #90).** How and in what +order it gets built is in `docs/Development-Plan.md`. It replaced +`docs/Architecture-Build-Spec.md`, which remains in git history; its section letters +(A–H) are not reused here. This document says what Nisse is, how its parts fit together, and why. It records decisions and the reasons for them. It doesn't record status or sequencing. @@ -34,7 +35,7 @@ agents, one string is used everywhere a site appears, including MCP server instances (X3). All artifacts live as packages in **one repository, `nisse`**. The repository -boundary was never a trust boundary in this design (the VM is, X1); a monorepo +boundary was never a trust boundary in this design (the hosts are, X1); a monorepo avoids tripled admin for a solo operator and keeps `nisse-core` easy to change while its seams are still settling. @@ -54,7 +55,7 @@ The agents run **local-first** (P7): a model on the operator's own inference hardware does the work, and frontier models are called in when it needs help. The agents share one library, `nisse-core` (Part L). Each agent adds what is -specific to its job: `nisse-dev` works on software projects in a sealed VM +specific to its job: `nisse-dev` works on software projects on sealed build VMs (Part D); `nisse-ops` operates live infrastructure across two sites (Part O). ### P2. Threat model @@ -132,6 +133,8 @@ truly irreversible also sits behind a hard wall. - Where a tool offers a **dry run** or a preview, the agent uses it before the real action. - Build artifacts and caches are **disposable**; nothing depends on them. +- **Tool VMs are disposable** (X1): a broken one is re-created, not repaired. What + must survive lives on the agent host. ### P6. Structural, not model judgment @@ -163,9 +166,10 @@ safety, such as choosing which ready work item to switch to (L1). ### P8. Carried principles -- **Isolation matched to blast radius.** `nisse-dev` is disposable and sealed; - `nisse-ops` holds live-infrastructure reach. They never share a running - instance, and every agent gets its own VM (X1). +- **Isolation matched to blast radius.** `nisse-dev`'s work runs on disposable, + sealed build VMs; `nisse-ops` holds live-infrastructure reach. They never share a + running instance, and no command a model chose runs where agents keep their state + and secrets (X1, L13). - **Stateless images, stateful volumes.** Containers are replaceable; durable state lives on volumes that survive redeploys and unexpected reboots. - **Structured channels for machines, prose channels for humans.** Machine intake @@ -196,6 +200,12 @@ Every agent's work is organised into three kinds of unit. deduplicate: a later finding for the same problem attaches to the open work item instead of opening a second (a stable fingerprint per finding). - **The operator is always a source** of work items. +- **Another agent can be a source.** A request from one agent to another (for + example, `nisse-dev` asking `nisse-ops` for a build VM) arrives through the + receiving agent's normal intake, such as a ticket, and becomes a work item there. + It carries no authority of its own: the receiving agent applies its own rules + (P3, L3) exactly as for any other work item, and the requester waits on it like + any external dependency. - **A work item is the unit of state:** its conversation, plans and approvals, pending questions, pinned role mapping (L9) and transcript (L7) all belong to it. Its ID is minted locally when it opens and doubles as its correlation ID in logs. @@ -517,11 +527,11 @@ is fixed. ### L9. Models, roles and helpers -**LiteLLM in front of everything.** Each agent VM has exactly one LLM destination: +**LiteLLM in front of everything.** Each agent has exactly one LLM destination: LiteLLM, which fans out to the local inference host and to frontier vendors. It holds every vendor key (the agent holds one LiteLLM token), enforces budget caps per virtual key, logs centrally, and routes fallbacks. Adding a vendor never -touches an agent VM. +touches an agent. **One client interface:** `complete(events, tools, model) → events`, over the canonical log. Delegations use LiteLLM's passthrough route with a per-vendor @@ -589,7 +599,7 @@ later operator-written procedures. each agent's memory, and `procedures` (the operator's). Sharing grants read access only; concurrent writers never exist. - **`nisse-dev` gets private space only.** Shared infrastructure documents are a - map of exactly what its sealed network keeps it from (D7). + map of exactly what its sealed network keeps its builds from (D7). - **Each agent authenticates as its own Outline user,** with its own API token. A token from the operator's account would silently void every permission here. - **Collections and documents are identified by ID** from configuration and state, @@ -597,7 +607,7 @@ later operator-written procedures. **Never an instruction channel, never an approval channel.** Work is never read out of the knowledge store, and an agent never writes there to make another agent -act. Coordination between agents goes through the operator, Matrix, or a ticket. +act. Coordination between agents goes through the operator, Matrix, or a ticket (L1). **Corrections go through conversation.** The operator tells the agent, which records the correction verbatim (L6). The operator keeps write permission as a @@ -635,17 +645,28 @@ contents, dependencies, web pages, alert payloads, log lines. can bias which ready item is chosen next; the switch notice makes it visible. - **Each agent is checked against the lethal trifecta** — private data, untrusted content, and a way to send data out. Damage needs all three. `nisse-dev` holds - little private data and can reach only LiteLLM, Gitea and package registries; - `nisse-ops` has all three legs and relies on its hard walls and step sequences - (O2). + little private data, and the build VMs where untrusted content meets command + execution hold no credential beyond their project's (D7); `nisse-ops` has all + three legs and relies on its hard walls and step sequences (O2). ### L12. Secrets, identity and logging -**Secrets.** Fetched at run time from Vaultwarden machine accounts, never baked -into images. One minimally scoped machine account per agent and one per MCP server -instance (X3); each has a single bootstrap token on its own host, and nothing -else is stored there. Only credentials go in secret stores; every other setting -lives in reviewable configuration files. +**Secrets.** +- **Each agent has its own set of secrets,** held on the agent host (X1) and + mounted only into that agent's container. No agent can read another's. They are + never baked into images. +- **An agent may give its set structure.** `nisse-dev` namespaces it by project + (D7); `nisse-ops` may do the same by site. The framework only needs names. +- **The harness hands secrets out per operation.** Which secrets a command or tool + call receives, and how (environment or stdin, never a command line), is decided + by code and configuration, never by a model. +- **Every value handed out is redacted** from tool output before it reaches the + transcript, a model or a log. A test that prints its environment must not put a + key into a frontier request. +- **MCP server instances** (X3) each have their own minimally scoped credentials, + given only to that instance. +- **Only credentials go in secret stores;** every other setting lives in reviewable + configuration files. **Identity.** Each agent is its own LLDAP principal (`svc-nisse-dev`, `svc-nisse-ops`) in a tightly scoped Authelia group, never the operator's account. @@ -659,6 +680,33 @@ event, work item, cause). Every classifier verdict, escalation and external effe is logged. **Logs are diagnostic, never load-bearing:** nothing reads them to derive state. +### L13. Tools and execution + +Tool calls come in three kinds, and the framework allows no others. + +| Kind | Examples | Where it runs | +|---|---|---| +| **Control tools** | `submit_plan`, `ask_human`, `finish`, `ask_expert` | In the harness. They act only on the agent's own state and conversation | +| **Execution tools** | Running a command; reading, writing and listing files | On a remote host, over SSH, through `execute(host, …)` | +| **MCP tools** | Proxmox, UniFi, Gitea APIs (X3) | In MCP server containers beside the agent | + +- **`execute(host, command, *, secrets, stdin)` is the only way to run anything.** + There is no local execution path in the framework, so the agent host never runs a + command a model chose. File tools are SSH operations too. +- **The harness binds the host,** not the model. For `nisse-dev` it comes from the + work item: that project's build VM (D7). Where an agent must let the model choose + (`nisse-ops` running a command on its tool VM or on a device), the model picks + from the hosts the harness allows for that work item; any other host is refused + before a connection is made. +- **MCP tools are the one exception,** made deliberately: their servers run fixed + code against an API, and the model chooses only arguments. They pass through the + action classes (P3) and the classifier (L4) like everything else, and their output + is redacted (L12). +- **Tests use SSH too:** a throwaway `sshd` container stands in for a tool VM, so the + code path under test is the one used in production. +- **The action classifier is not a tool.** It is a model call made by the harness + and executes nothing, so it runs on the agent host. + --- ## Part D — `nisse-dev` @@ -708,12 +756,22 @@ agent's state (L8). ### D3. The workspace +**Each project has its own build VM** (X1), where its workspace lives and its +commands run. +- **Kept between work items,** so installed toolchains and package caches stay + warm, and isolated from every other project's. +- **Collected when idle.** A build VM whose project has been idle long enough may be + destroyed, and is re-created when work on the project resumes. +- **Re-created from a template** plus the toolchains recorded for the project (see + below). Work pushed to Gitea survives; a commit not yet pushed may be lost when a + VM is wiped, and that is accepted. + **A fresh, clean workspace at `use `.** - Fetch from the repository, and check out a clean tree at the latest base branch. - Anything left in a previous workspace (uncommitted changes, local branches, unpushed commits) is **set aside**, never silently discarded or carried over, and reported to the operator. -- Build artifacts and caches never survive into a new task. +- Build artifacts and caches in the workspace never survive into a new task. The milestones of one work item share that workspace, since they build on each other, often before earlier PRs are merged. @@ -724,12 +782,12 @@ the project needs (from its files, e.g. `Cargo.toml`, `pyproject.toml`, installed **before any planning**. The environment's toolchain versions become facts in the transcript (L6), so plans match reality. -**The agent installs what it needs on its own VM.** The VM is disposable and is -the boundary (D7), so installing packages and toolchains there is class 2 (P3): a -contained side effect. Every install is recorded as a fact (L6) and in the -project's scope memory, so the environment can be rebuilt; toolchains that keep -being needed are baked into the VM image (X4). Installing on any host other than -its own stays class 3. +**The agent installs what it needs on the project's build VM.** The VM is +disposable and is the boundary (D7), so installing packages and toolchains there +is class 2 (P3): a contained side effect. Every install is recorded as a fact (L6) +and in the project's scope memory, so the VM can be rebuilt; toolchains that keep +being needed are baked into the template (X4). Installing on any other host stays +class 3. **Repairing the environment is in scope.** Within a task, the agent may clean caches and build artifacts or reinstall project dependencies to fix a broken @@ -746,6 +804,12 @@ a corrupt `.pyc`. the work produced). Anything left out is reported. - **"Changes to publish" are counted from where the milestone started,** never from a possibly stale remote branch. +- **Pushes are harness operations on the build VM.** The harness runs `git push` + there, handing the push key over stdin to a short-lived `ssh-agent` for that one + push (L12); the model's commands never receive it. Nothing is synced through the + agent host: Gitea is the only copy that has to survive. +- **Pull requests are opened from the agent's container** with its Gitea API token, + which never reaches a build VM. - **Publishing is idempotent** (L8): an open PR for the branch is adopted and updated, never duplicated. - **A milestone that completes without changes is done, not failed,** when it @@ -779,29 +843,47 @@ a corrupt `.pyc`. | Class | Examples | |---|---| | 1 | Reading the repo, running tests and builds in the workspace or a test container, querying Gitea | -| 2 (in scope) | Editing the workspace, committing to milestone branches, pushing them, opening and updating PRs, commenting on its issues and PRs, cleaning caches, installing project dependencies, packages and toolchains on its own VM | -| 3 | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, changing repository settings, anything outside the work item's repo or its own VM | +| 2 (in scope) | Editing the workspace, committing to milestone branches, pushing them, opening and updating PRs, commenting on its issues and PRs, cleaning caches, installing project dependencies, packages and toolchains on the project's build VM | +| 3 | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, changing repository settings, anything outside the work item's repo or its project's build VM | Its scope sources are the approved architecture, the build plan and each milestone's marking. ### D7. Deployment and isolation -- **Its own VM** (X1), with a real Docker daemon, because it builds and runs - containers. The VM is the trust boundary for unattended command execution. -- **Sealed:** its egress allowlist (X2) permits only LiteLLM, Gitea and package - sources (language registries and OS package mirrors); there is no ingress. Denial covers RFC1918 (except the specific - LiteLLM and Gitea addresses) and the tailnet's `100.64.0.0/10`, so it can't reach - the NAS, Proxmox or the gateways. The inference host sits behind LiteLLM, so it is - not a second exception. -- **No Tailscale** on this VM; it would route around the firewall. -- **Its own VLAN,** and test services on predictable ports (e.g. 18000–18099), so - opening access later is a firewall change. +**The agent** runs as a container on the agent host (X1), holding its state, its +secrets and its Gitea API token. It reaches LiteLLM, Gitea, Matrix, and SSH to its +build VMs, and nothing else. It never runs a command a model chose (L13). + +**Build VMs,** one per project (D3), are where the work happens: +- **A real Docker daemon,** because projects build and run containers. The VM is + the trust boundary for unattended command execution. +- **Sealed:** egress allows package sources (language registries and OS package + mirrors) and Gitea's git endpoint; there is no ingress except SSH from the agent + host. Denial covers RFC1918 (except those specific addresses), the agent host, + and the tailnet's `100.64.0.0/10`, so a build can't reach the NAS, Proxmox, the + gateways or the agent's state. +- **A project may add egress its tests need,** for example LiteLLM and a Matrix test + homeserver for the `nisse` project itself. Additions are declared in the + project's configuration and reviewed like any allowlist (X2). +- **No Tailscale** on build VMs; it would route around the firewall. +- **On their own VLAN,** with test services on predictable ports (e.g. + 18000–18099), so opening access later is a firewall change. - **Test containers on an internal Docker network with no route out.** -- **Build storage on a separate mount** from the state volume. - **A browser** (a shared Playwright tool, headless) **reaches only the internal test network,** to exercise web apps the agent built and reproduce bugs. No SSO. +**Secrets are namespaced by project** (L12), e.g. `nisse/litellm-key`, +`nisse/gitea-test-token`. A build VM's commands receive only their own project's +secrets, and each secret's configuration says whether the model's commands may see +it (a test credential the project's tests need) or only harness operations use it +(the push key). Anything the model's commands receive must be scoped for testing, +never able to touch production. + +**Provisioning.** Build VMs are created and destroyed by Terraform from the +template. Eventually `nisse-dev` asks `nisse-ops` for them, as an agent-to-agent +request (L1); until then the operator provisions them. + --- ## Part O — `nisse-ops` @@ -849,8 +931,8 @@ exactly the steps written. This is `nisse-ops`' scope source for the classifier - **Approvals expire.** A plan not started within the expiry window must be approved again. - **Policy is small, conservative, and data:** a versioned file declaring each MCP - instance's privilege, protected targets (the NAS, gateways, the agents' own - hosts), and each elevated tool's target argument. Anything unclassified is + instance's privilege, protected targets (the NAS, gateways, the agent host and + tool VMs), and each elevated tool's target argument. Anything unclassified is treated as elevated and protected. Server-declared tool hints can make a tool stricter, never looser. A model may propose entries for a new server as a PR; no classification happens at run time. @@ -894,40 +976,66 @@ a routine fix approved by hand often enough). ### O6. Deployment -- **Its own long-lived, stateful VM** (X1). -- **Tailscale** and scoped credentials to reach infrastructure at both sites. -- **Its MCP servers run on its own Docker host;** its egress allowlist therefore - includes the Proxmox and UniFi endpoints at both sites. +- **A container on the agent host** (X1), with its MCP servers beside it (X3). +- **Its own tool VM,** long-lived, for network-management tools and the commands it + runs (L13). It holds Tailscale and the reach those commands need at both sites; + like any tool VM it can be rebuilt from its template. +- **Its egress allowlist** includes the Proxmox and UniFi endpoints at both sites, + for its MCP servers. +- **Accepts provisioning requests** from other agents (L1): creating and + destroying build VMs for `nisse-dev` through Terraform, under its own rules (O2). - **Updated in place,** with restarts gated on an idle flag (X4). --- ## Part X — Deployment model (all agents) -### X1. Every agent gets its own VM +### X1. The agent host and tool VMs -No agent runs as a container on a shared host, and no two agents share a VM. -- **The VM is the isolation boundary between agents:** a compromised or confused - agent can't reach another's state, credentials or processes. -- **Per-agent attribution:** everything an agent holds is issued to its own - identity, so every upstream action traces to one agent. +**One agent host,** a stable, long-lived VM. +- **Each agent runs on it as a container,** with its own volume (state, transcripts, + memory), its own secrets (L12) and its own network. No agent can reach another's + state, credentials or processes. +- **Its MCP servers run beside it** as containers (X3). +- **It never runs a command a model chose** (L13). That is what makes it safe for + agents to share it: nothing on it executes untrusted input. -### X2. Egress policy per agent instance +**Tool VMs** are where model-chosen commands run, reached only over SSH from the +agent host. Each agent has the tool VMs its work needs: +- `nisse-dev`: one build VM per project, collected when idle and re-created on + demand (D3, D7); +- `nisse-ops`: its own tool VM, for network-management tools (O6); +- a purely conversational agent: none. -- **Default-deny allowlist per agent instance,** declared as a versioned artifact - next to the agent's definition, each entry justified by the component that - needs it. -- **Declared in one place, enforced in several:** at the VM, the Docker network, - and the UniFi firewall. Network-layer enforcement survives compromise of the - agent's host. -- **The allowlist is the union** of what the agent and its MCP servers reach. -- **The tailnet is `100.64.0.0/10`,** not RFC1918; deny it explicitly. +Tool VMs are **disposable** (P5): each is built from a template plus recorded +installs, and nothing on one has to survive. + +**Per-agent attribution:** everything an agent holds is issued to its own +identity, so every upstream action traces to one agent. + +### X2. Egress policy + +- **Default-deny allowlists,** declared as versioned artifacts next to the agent's + definition, each entry justified by the component that needs it. +- **For each agent container,** the union of what the agent and its MCP servers + reach, enforced by its Docker network and the agent host's firewall. The UniFi + firewall sees the agent host as one address, so these allowlists rest on the + host; that is acceptable because nothing on the host executes untrusted input. +- **For each tool VM,** enforced at the VM, its Docker network and the UniFi + firewall on its VLAN. Network-layer enforcement survives compromise of the VM, + which matters here because this is where untrusted input meets command execution. +- **Tool VMs never reach the agent host,** except as replies on the SSH sessions it + opens. +- **The tailnet is `100.64.0.0/10`,** not RFC1918; deny it explicitly wherever it + isn't needed. ### X3. MCP servers -- **One instance per agent VM,** as containers on that agent's Docker host. A shared +- **One instance per agent,** as containers beside it on the agent host. A shared process would mean a shared credential, losing attribution in the upstream API's own audit log. Duplication is nearly free for stateless servers. +- **The one exception to "execution happens on tool VMs"** (L13): a server runs + fixed code against an API, and the model supplies only arguments. - **One instance per (service, site, privilege).** The site is part of the server's identity, so a wrong-site action needs a tool that isn't mounted, not just a wrong argument. Read-only and elevated instances have separate credentials, each from @@ -937,17 +1045,19 @@ No agent runs as a container on a shared host, and no two agents share a VM. extra round trips for a local driver, no discovery of capabilities not already sought, opaque arguments, and a dynamic tool list that defeats prompt caching. -### X4. Containers, images and restarts +### X4. Containers, images, VMs and restarts -- **Stateless images, named volumes** for state. +- **Stateless images, named volumes** for state, on the agent host. - **Delivery:** Gitea Actions → Gitea's OCI registry → Watchtower pulls. - **Gate the restart, not the pull:** an idle flag; Watchtower never restarts an agent mid-work. Unplanned interruptions are handled by recovery (L8). - **Third-party MCP server images are digest-pinned** and updated deliberately, never by Watchtower: they hold root-equivalent credentials. +- **Tool VMs come from templates,** provisioned by Terraform (`bpg/proxmox` and + cloud-init). Installs that keep recurring are baked into the template. - **Survive reboots:** VMs and Docker start on boot, containers restart unless - stopped, and the state dataset has ZFS snapshots (scheduled, and on each - approval). + stopped, and the agent host's state dataset has ZFS snapshots (scheduled, and on + each approval). Tool VMs are not snapshotted; they are rebuilt. ### X5. Supporting services @@ -981,10 +1091,15 @@ it uses; nothing is shared between agents. - **When a work item must have an external record.** Plan-driven dev work and ticket-driven ops work already do; whether every item with an approval must. - **Fallback browser access to network gear:** standing, or handed off per task. -- **Relocating stateful MCP servers** off agent VMs, one at a time, if one ever - justifies losing per-agent attribution. -- **Infrastructure as code** (Terraform with `bpg/proxmox` and cloud-init) for agent - VMs. +- **Relocating stateful MCP servers** off the agent host, one at a time, if one + ever justifies losing per-agent attribution. +- **Where secrets come from.** Each agent's set lives on the agent host (L12); whether + it is filled by hand, synced from Vaultwarden, or written by Terraform. +- **How `nisse-ops`' MCP servers reach the second site** from the agent host + (Tailscale on the host, with other agents' networks denied the tailnet, or a + subnet route), since Tailscale on the host serves every container on it. +- **How long idle before a build VM is collected,** and whether collection needs the + operator's word. --- @@ -1008,9 +1123,11 @@ history): - **Documents as the dev workflow (D2),** a fresh workspace per task and toolchain preflight (D3), and git hygiene learned from the live runs (D4). - **Conversation (L5)** is a first-class requirement, not a command interpreter. +- **Deployment (X1, L13),** revised in PR #90: one agent host with each agent as a + container, and disposable tool VMs (a build VM per project for `nisse-dev`) where + every model-chosen command runs over SSH, replacing a VM per agent. - **Kept, largely as they were:** the work model, durability and reconciliation, LiteLLM and the canonical log, delegation, the knowledge store, prompt injection, - secrets and identity, `nisse-ops`' approved step sequences, and the deployment - model. + identity, and `nisse-ops`' approved step sequences. - **Moved to the Development Plan (to be written):** build order, supporting-service status, and interim arrangements. -- 2.54.0 From aa97309a0c1de9f27d794e5ab629495034a9dc0c Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 17:30:54 +0000 Subject: [PATCH 02/12] Replace Outline with an Obsidian-format knowledge vault on the agent host Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 81 +++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 072d454..7a84253 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -297,7 +297,7 @@ does not stop at gates for their own sake. **Approval sources.** Matrix (L5), Gitea comments (D2) and Vikunja comments (O5) are approval sources; each authenticates the approver against its own operator allowlist. The first valid approval wins and is echoed to the other channels. The -knowledge store (L10) is **never** an approval source or an instruction channel. +knowledge vault (L10) is **never** an approval source or an instruction channel. **The trust dial.** How much the operator reviews after the fact is **data the operator sets**, per project and per milestone: for example, whether PR review is @@ -408,16 +408,16 @@ the caller treats it as a failure (L9). - **For a repo:** the project's own documents (`ARCHITECTURE.md`, build plans, conventions), versioned in the repo and changed by PR (D2). Read at work-item start; the repo is the authority. -- **For infrastructure:** the world model in the knowledge store (L10): what +- **For infrastructure:** the world model in the knowledge vault (L10): what exists and how it is arranged. **4. Agent memory** — the agent's own notes, outliving work items and scopes. - **What it holds:** the operator's preferences, lessons learned, recurring decisions and their reasons. Much like a good collaborator's notebook. -- **Where:** the agent's private space in the knowledge store (L10), one note per - fact, with an index. -- **Written by the agent** at defined moments (below); **visible to the operator**, - who corrects it through conversation, not by editing. +- **Where:** the agent's own folder under `memory/` in the knowledge vault (L10), + one note per fact, with an index. Private between agents. +- **Written by the agent** at defined moments (below); **visible to the operator** + (read-only, L10), who corrects it through conversation, not by editing. - **Read at work-item start** (the index, plus notes relevant to the task) and when the agent is about to make a decision the notes cover. @@ -589,38 +589,55 @@ are recorded as input/output pairs from the start, with outcomes marked. Changin a model is then a configuration change and a re-run, not a matter of benchmark chatter. -### L10. Knowledge store (Outline) +### L10. Knowledge vault Durable knowledge that outlives work items: world models, agent memory (L6), and -later operator-written procedures. +operator-written material such as procedures. It is a folder of Markdown files on +the agent host (X1), in the format Obsidian reads (frontmatter, `[[links]]`, +tags), so the operator browses it in Obsidian with links, backlinks and the graph. -**Layout: one writer per document.** -- **One collection per writing principal:** `nisse-ops-world`, `nisse-dev-world`, - each agent's memory, and `procedures` (the operator's). Sharing grants read - access only; concurrent writers never exist. -- **`nisse-dev` gets private space only.** Shared infrastructure documents are a - map of exactly what its sealed network keeps its builds from (D7). -- **Each agent authenticates as its own Outline user,** with its own API token. A - token from the operator's account would silently void every permission here. -- **Collections and documents are identified by ID** from configuration and state, - never discovered, and document IDs recorded when created make writes idempotent. +**One vault, one writer per folder.** +- **Agent memory lives under `memory/`,** one folder per agent. Other folders hold + shared material (world models, the operator's notes); their names and layout are + left to grow with need. +- **Every folder has exactly one writer,** an agent or the operator, so concurrent + writes never happen. +- **Mounts enforce it** (a hard wall, P4): each agent's container gets the folders it + writes read-write, the shared folders it may read read-only, and nothing else. No + agent can read another's memory. `nisse-dev` gets no infrastructure material, + which would be a map of exactly what its builds are kept from (D7). +- **Links point from memory to shared notes, never the other way,** so the shared + part reads cleanly on its own. +- **Writes are atomic** (write, then rename), so a reader never sees half a note. +- **Access is through control tools** (L13): the harness reads and writes the mounted + folders. No tool VM ever sees the vault. +- **History:** the vault is a git repository on the agent host, and the harness + commits each write with the agent and work item that made it. A note's revision is + its commit (L6). + +**The operator's copy.** Syncthing keeps a copy on the operator's devices, with the +direction set per folder: +- **folders an agent writes,** memory included, are send-only from the host: the + operator reads them, and a local edit never flows back; +- **folders the operator writes** are receive-only on the host. + +So conflicts can't arise. Obsidian's own settings folder and `.git` are not synced. **Never an instruction channel, never an approval channel.** Work is never read -out of the knowledge store, and an agent never writes there to make another agent -act. Coordination between agents goes through the operator, Matrix, or a ticket (L1). +out of the vault, and an agent never writes there to make another agent act. +Coordination between agents goes through the operator, Matrix, or a ticket (L1). **Corrections go through conversation.** The operator tells the agent, which -records the correction verbatim (L6). The operator keeps write permission as a -break-glass: stop the agent, edit, restart. An agent re-reads a document's -revision before writing and merges rather than overwrites if it moved. +records the correction verbatim (L6). As a break-glass, the operator can stop the +agent and edit its folder on the host, then restart. **Retrieval is structural first.** Procedures are bound to entries in ops policy -(O2) and fetched whenever a plan uses that entry; search exists in addition, never -instead. +(O2) by path and read whenever a plan uses that entry; search (over plain files) +exists in addition, never instead. -**Outages.** A failed write never blocks work (retry, or record locally and -reconcile). A failed read of a bound procedure stops planning that action; other -failed reads proceed and log the gap. +**Failures.** A failed write never blocks work (retry, or record and reconcile). A +missing bound procedure stops planning that action; other failed reads proceed and +log the gap. ### L11. Prompt injection @@ -1062,8 +1079,8 @@ identity, so every upstream action traces to one agent. ### X5. Supporting services All self-hosted: Matrix (Synapse), Gitea, LiteLLM, the local inference host -(llama-swap on an RTX 4090 host), Vikunja, Outline, Vaultwarden, LLDAP with -Authelia. Each agent gets its own account, scoped token and room in each service +(llama-swap on an RTX 4090 host), Vikunja, Vaultwarden, LLDAP with Authelia, and +Syncthing for the operator's copy of the knowledge vault (L10). Each agent gets its own account, scoped token and room in each service it uses; nothing is shared between agents. --- @@ -1123,11 +1140,13 @@ history): - **Documents as the dev workflow (D2),** a fresh workspace per task and toolchain preflight (D3), and git hygiene learned from the live runs (D4). - **Conversation (L5)** is a first-class requirement, not a command interpreter. +- **The knowledge vault (L10)** replaces Outline: Markdown on the agent host, shared + between agents by mounts, synced to the operator by Syncthing. Revised in PR #90. - **Deployment (X1, L13),** revised in PR #90: one agent host with each agent as a container, and disposable tool VMs (a build VM per project for `nisse-dev`) where every model-chosen command runs over SSH, replacing a VM per agent. - **Kept, largely as they were:** the work model, durability and reconciliation, - LiteLLM and the canonical log, delegation, the knowledge store, prompt injection, + LiteLLM and the canonical log, delegation, prompt injection, identity, and `nisse-ops`' approved step sequences. - **Moved to the Development Plan (to be written):** build order, supporting-service status, and interim arrangements. -- 2.54.0 From 3e7edce6dca8b7b9336870fa33e45a61122309bd Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 17:37:36 +0000 Subject: [PATCH 03/12] Make the vault's git history optional Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 7a84253..6229954 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -435,7 +435,8 @@ build artifacts, and the contents of a previous task's workspace. **How memory is used.** - **Everything read is snapshotted into the transcript** with its provenance - (document, revision, when retrieved), once per work item at first use. + (source, its revision or a hash of its content, when retrieved), once per work + item at first use. - **Memory is a cache, never authority.** An agent may rely on it to know what exists and where to look; facts an action depends on are re-checked against the live system immediately before the action (preconditions, O2). When a proposal @@ -611,9 +612,12 @@ tags), so the operator browses it in Obsidian with links, backlinks and the grap - **Writes are atomic** (write, then rename), so a reader never sees half a note. - **Access is through control tools** (L13): the harness reads and writes the mounted folders. No tool VM ever sees the vault. -- **History:** the vault is a git repository on the agent host, and the harness - commits each write with the agent and work item that made it. A note's revision is - its commit (L6). +- **Provenance is in the note itself:** the harness records the source, work item + and time of each write (L6), whatever else is kept. +- **History is optional:** the harness may keep the vault as a git repository on the + agent host, committing each write with the agent and work item that made it. It + shows what an edit replaced; recovery doesn't depend on it (the host's snapshots + cover that, X4). **The operator's copy.** Syncthing keeps a copy on the operator's devices, with the direction set per folder: @@ -621,7 +625,8 @@ direction set per folder: operator reads them, and a local edit never flows back; - **folders the operator writes** are receive-only on the host. -So conflicts can't arise. Obsidian's own settings folder and `.git` are not synced. +So conflicts can't arise. Obsidian's own settings folder (and `.git`, if kept) are +not synced. **Never an instruction channel, never an approval channel.** Work is never read out of the vault, and an agent never writes there to make another agent act. -- 2.54.0 From cd2ff6d769398d03b4dc1c111e679be11719daa9 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:26:02 +0000 Subject: [PATCH 04/12] Write the agreed dev workflow into the architecture Issues assigned to dev-agent are the intake; the agent restates its understanding and either proceeds (bug label) or waits for a go-ahead. Labels steer and are never required. Build plans and ARCHITECTURE.md are working documents reviewed in PRs, not gates; scope is the issue and its conversation. Work items can be blocked on Gitea dependencies, each gets a Matrix thread, milestones are started and finished by control tools, and the next milestone waits for the previous merge. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 244 ++++++++++++++++++++++++++++++------------- 1 file changed, 170 insertions(+), 74 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 6229954..21024ea 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -93,12 +93,13 @@ Every action any agent can take falls into one of three classes: | Class | What it is | Examples | Rule | |---|---|---|---| | **1. No side effects** | Observes; changes nothing outside a disposable sandbox | Reading files, querying APIs, searching, viewing logs and metrics, running a build or tests in a throwaway environment | Always allowed | -| **2. Contained side effects** | Changes something, but the change stays within the agent's own area or has a known undo | Editing the workspace, committing to the agent's branch, opening a PR, writing the agent's own notes, starting a test container; for ops, a step with a known undo inside an approved plan | Allowed when in scope; logged and reported | -| **3. Dangerous** | Irreversible, visible beyond the agent's own area, or outside what was agreed | Merging to a default branch, deleting data, changing a firewall, installing software on a host the agent doesn't own, contacting third parties, spending beyond a budget | Needs discussion, unless an approved plan names the exact action | +| **2. Contained side effects** | Changes something, but the change stays within the agent's own area or has a known undo | Editing the workspace, committing to the agent's branch, opening a PR, writing the agent's own notes, starting a test container; for ops, a step with a known undo inside an approved step sequence | Allowed when in scope; logged and reported | +| **3. Dangerous** | Irreversible, visible beyond the agent's own area, or outside what was agreed | Merging to a default branch, deleting data, changing a firewall, installing software on a host the agent doesn't own, contacting third parties, spending beyond a budget | Needs discussion, unless an approved step sequence (O2) or the operator in conversation names the exact action | -**"In scope"** means within what the operator agreed to: the work item's task, -an approved plan, a pre-approved milestone (D2), or an approved step sequence -(O2). An action can be class 2 in one work item and class 3 in another. +**"In scope"** means within what the operator agreed to: the work item's task +and what was agreed in its conversation (for `nisse-dev`, the assigned issue, +D1), or an approved step sequence (O2). An action can be class 2 in one work item +and class 3 in another. **Move actions down the classes wherever it is cheap.** Reversibility is designed in, not hoped for (P5). @@ -191,7 +192,7 @@ Every agent's work is organised into three kinds of unit. | Concept | What it is | Lifetime | `nisse-dev` | `nisse-ops` | |---|---|---|---|---| -| **Duty** | A standing responsibility that produces work items | Recurring | None yet; the operator is the only source | Scheduled checks, log monitoring, alert intake | +| **Duty** | A standing responsibility that produces work items | Recurring | Gitea issues assigned to `dev-agent` (D1) | Scheduled checks, log monitoring, alert intake | | **Work item** | One effort, opened and eventually closed | Opened → closed | Building a feature in a repo | Diagnosing a failure; making one change | | **Scope** | What a work item operates on, and the context that persists across work items there | Indefinite | A repo | A site and service, e.g. `garrett-hall` Proxmox | @@ -217,6 +218,22 @@ Every agent's work is organised into three kinds of unit. - **Scopes hold context shared across work items** (L6): for a repo, its documents and conventions; for infrastructure, the world model of that site or service. +**Work item states.** + +| State | Waiting for | +|---|---| +| **active** | nothing; it is being worked on | +| **ready** | its turn | +| **waiting on the operator** | an answer, a review or an approval | +| **blocked** | something else to finish, e.g. another issue to close | + +- **Blocked is read from external truth,** such as a Gitea issue dependency (D1), + and becomes ready only when that dependency actually closes, never by a model's + judgement (P6). The operator can unblock by removing the dependency. +- **An agent that finds a blocker mid-work** says so and proposes recording it + (e.g. "blocked by #12"); with the operator's agreement it records it, parks the + item as blocked and moves on. + **Park-and-switch: one active work item per agent.** - Other work items are *parked*, with their full position held durably. - **Switching happens only at safe points,** when the active item is waiting on @@ -225,10 +242,12 @@ Every agent's work is organised into three kinds of unit. item (one it can act on without the operator) when the active item is waiting on the operator, after a grace period (default 10 minutes, so a watching operator can answer first), or has closed. -- **The driver chooses which ready item,** from code-rendered summaries (source, - age, severity, state, title), never from other items' transcripts. A failed or - invalid choice falls back to the oldest ready item. Choosing grants nothing: every - item still meets its own rules. +- **Which ready item comes next:** the operator's order when given (e.g. "do #14 + next", or a priority label); otherwise the oldest ready item. An agent may instead + let the driver choose from code-rendered summaries (source, age, severity, state, + title), never from other items' transcripts, where judgement helps (`nisse-ops`, + where severity matters); a failed or invalid choice falls back to the oldest. + Choosing grants nothing: every item still meets its own rules. - **Parking gives up authority.** Elevated credentials an item's plan mounted (O2, X3) are unmounted when it parks, and remounted only if its approval hasn't expired. @@ -268,17 +287,21 @@ Rules for the loop: ### L3. Authority: approvals and permissions -An approval grants a **permission with a scope**: "this build plan may change -this repo", "these ops steps may run", "this milestone may proceed without -discussion". Actions are checked against the permissions in force (P3); the loop -does not stop at gates for their own sake. +An approval grants a **permission with a scope**: "work on this issue", +"these ops steps may run". Actions are checked against the permissions in force +(P3); the loop does not stop at gates for their own sake. -**What needs discussion before it happens:** -- **Architecture documents and build plans are always discussed** with the - operator (D2). Their approval is the scope for the work that follows. -- A build plan marks each milestone **pre-approved** (proceed when reached) or - **needs discussion** (stop and talk first). -- **Any class-3 action** not named by an approved plan (P3). +**Where scope comes from.** +- **For dev work:** the assigned issue and what the operator agreed in its + conversation (D1). Build plans are the agent's working documents, not approvals + (D2); the operator's real check is reviewing the PR, and nothing merges without + it unless the trust dial says so. +- **For ops work:** an approved step sequence (O2), which is exact. +- **The operator steers with labels and words, never required ones** (D1): a label + or a request can ask the agent to talk first; leaving one off is never an error. + +**What always needs discussion first:** any class-3 action not named by an +approved step sequence or by the operator in conversation (P3). **Approvals bind to exactly what was approved.** - Every plan revision carries a tag, numbered across the whole work item, so @@ -300,10 +323,9 @@ allowlist. The first valid approval wins and is echoed to the other channels. Th knowledge vault (L10) is **never** an approval source or an instruction channel. **The trust dial.** How much the operator reviews after the fact is **data the -operator sets**, per project and per milestone: for example, whether PR review is -required before merging, or whether the agent may merge a pre-approved milestone -itself once checks pass. The operator turns it up as confidence grows. The agent -never changes it. +operator sets**, per project: for example, whether PR review is required before +merging, or whether the agent may merge its own PRs once checks pass. The operator +turns it up as confidence grows. The agent never changes it. ### L4. The action classifier @@ -317,7 +339,8 @@ An independent check that sorts an action the fast path doesn't cover. **Rules:** - **It judges the action, not the actor's story.** Its input is the structured action (tool, command, target, a summary of the change) and the **agreed scope** - (the work item's task, the approved plan or milestone). Never the actor's + (the work item's task and what was agreed, or the approved step sequence). Never + the actor's transcript or reasoning, and never untrusted content such as issue text or web pages. Otherwise injected text, or the actor's own rationalisation, can argue past it. @@ -342,7 +365,18 @@ Existing Matrix clients are the UI. **What the operator can say.** Plain language, plus a small command vocabulary that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, and -work-item selection (e.g. `use `). +work-item selection (e.g. `use `, "do #14 next"). +- **Free-flowing conversation can open a work item.** A conversation about work + ("I'm having trouble with X…") is kept as a work item from its first message; it + may end with a project, an issue, or just an answer. +- **A thread per work item.** The agent opens each work item's conversation as a + Matrix thread. A reply in a thread goes to that item, even while it is parked + (answering makes it ready); top-level messages go to the agent in general or to + the active item. +- **Documents as input.** The operator can paste a document, attach a text or + Markdown file, or point to a note in the knowledge vault (L10) ("my sketch is in + the vault as `recipes idea`"). The message is the instruction; the document is + material for the conversation. - **Commands are matched without regard to case, surrounding whitespace, trailing punctuation, or a leading mention of the agent.** Messages that begin by addressing someone else are not for this agent. @@ -382,7 +416,7 @@ What each model sees, what persists, and how memory is written and used. for one role, fitted to that model's window. It is never stored; only the log is. Fitting, in order: shorten old tool results to their first and last lines; drop the oldest events (with a note saying so); never drop the system prompt, the -approved plan or the most recent turns. A call that still doesn't fit is not sent; +current plan or the most recent turns. A call that still doesn't fit is not sent; the caller treats it as a failure (L9). **2. The work-item transcript** — the canonical log (L7). @@ -396,7 +430,7 @@ the caller treats it as a failure (L9). happened from these, not by inferring it. - **Each role gets its own view:** - the driver sees the conversation, the facts, and helpers' reports; - - a delegated helper sees its brief, the approved plan, the facts, its own work, + - a delegated helper sees its brief, the current plan, the facts, its own work, and the driver's work in the same milestone as compact narration (each step and how it went); - `ask_expert` is stateless: the expert sees only the question; @@ -737,44 +771,83 @@ Designs and builds software projects, producing reviewed changes in Gitea. ### D1. How it works -A dev work item is a task on one repository: a feature, a fix, a new project. -Through conversation, the agent and the operator agree on an architecture (for a -new project, or a significant change) and a build plan; the agent then works -through the plan's milestones, each producing a PR, and keeps the project's -documents up to date as it goes. +A dev work item is a task on one repository: a bug, a feature, a new project. +Work arrives through Gitea issues or conversation, and leaves as reviewed PRs. -`use ` opens or resumes a work item on a repository. Messages sent before -it are kept for it (L5). +**Intake: an issue assigned to `dev-agent`.** Assignment says whose job it is. +- **The operator's issue becomes the work item's record;** the agent doesn't open a + second one. Only issues filed or assigned by allowlisted operators are acted on + (L11). +- **Unassigned issues are backlog,** never work. The agent can discuss them when + asked. +- **An issue with open Gitea dependencies is blocked** (L1) until they close. +- **On assignment the agent acknowledges** ("queued myproject#14, 1 ahead"), and the + item waits its turn (L1). + +**Starting an item.** The agent reads the project's documents (D2), then +**restates its understanding** in the item's Matrix thread (L5): what it takes the +request to mean, how it fits the architecture, and its first step. Then: +- **An issue labelled `bug`** proceeds after a short grace period (as in L1), unless + something below says to talk first. +- **Anything else** (a feature, a change, a question) waits for the operator's + go-ahead after the restatement; the conversation is where the design is agreed. +- **Talk first when:** the issue is labelled `discuss`; the agent finds the report + unclear (it asks its questions instead of restating); or it can't reproduce a + bug. It never fixes a bug it couldn't see. +- **The agent never classifies an issue itself.** It may suggest ("this reads like + a bug; say *go* and I'll start"); labels and the operator decide. A missing label + costs at most a wait, never unwanted work. + +**The flows.** +- **A bug:** reproduce (ideally as a failing test), fix, verify, and open a PR that + closes the issue. +- **A feature:** agree the design in conversation, write a build plan if the work + is longer than about one PR (D2), then work through it as milestones (D4). +- **A new project:** begins as a conversation, however vague. If it leads to a + project, the agent says what it needs provisioned: a repository, its protection + and the agent's access (D4), a build VM, and the project's configuration. The + operator provisions these (later `nisse-ops`, X4). The agent then opens a tracking + issue in the new repository ("implement the build plan"), and the architecture + lands in one or more PRs, followed by the build plan's milestones. +- **Progress** on a tracking issue is posted as it happens: facts by the harness + (milestone started, PR opened or merged), prose by the agent when there's + something worth saying. + +**Finishing.** The final PR closes the issue on merge; the agent closes the work +item, after checking what is left open (D5). ### D2. Documents are the workflow -The project's own documents carry the agreement between the operator and the -agent, and live in its repository: +The project's own documents live in its repository and carry its design forward: - **`ARCHITECTURE.md`** — what the project is, how it fits together, and why. The - durable "what and why". Created with a new project; changed whenever the design - changes. -- **Build plans** — how a piece of work gets done, as a sequence of milestones. - Short-lived: one per body of work, kept in the repo for the record. + durable "what and why". Created with a new project, and kept current by the agent: + a change to the design updates it. +- **Build plans** — how a piece of work gets done, as a sequence of milestones, + under `docs/plans/` or similar. The agent's working documents: how it stays + organised across context windows and sessions, and its own sanity check. -**Both are always discussed.** The agent drafts, the operator and agent talk it -through, and the document lands as a PR the operator approves. An approved -architecture and build plan are the scope (L3) for everything that follows. +**Neither is a gate.** They are reviewed the way all work is, in PRs. +- **Architecture changes** go in the PR with the code when small; the agent may + raise a larger one first. An issue labelled `design` asks for the design to be + agreed in conversation before any code. Without the label it is the agent's + judgement, and a wrong call costs a review, not safety. +- **Build plans are written for work longer than about one PR,** committed with the + work, so every PR shows the current plan and how it changed. The first PR of a + multi-milestone piece of work includes 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, and the operator can ask to discuss one at any time. **A build plan specifies; it doesn't implement.** Each milestone states its goal, the interfaces and behaviour it adds or changes, edge cases, and **tests as acceptance criteria**. It includes code only where the code *is* the clearest specification (a tricky algorithm, an exact format). It never manages branches (D4). How much detail it needs is informed by the driver's capability profile -(L9). +(L9). It may note where the agent means to check in with the operator, and which +milestones aren't expected to change code. -**Each milestone is marked** pre-approved (proceed when reached) or needs discussion -(stop and talk first), and says whether it is expected to change code (a review or -investigation milestone may not). - -The Gitea issue for the work item carries the conversation's record: plans and -approvals are posted there as comments, so the agreement survives loss of the -agent's state (L8). +**Progress is recorded in the plan itself:** the agent marks milestones done as +part of each PR. The framework never parses a plan (D4). ### D3. The workspace @@ -788,7 +861,7 @@ commands run. below). Work pushed to Gitea survives; a commit not yet pushed may be lost when a VM is wiped, and that is accepted. -**A fresh, clean workspace at `use `.** +**A fresh, clean workspace when a work item starts.** - Fetch from the repository, and check out a clean tree at the latest base branch. - Anything left in a previous workspace (uncommitted changes, local branches, unpushed commits) is **set aside**, never silently discarded or carried over, @@ -818,9 +891,25 @@ a corrupt `.pyc`. ### D4. Git, branches and PRs -- **The harness owns branches.** Each milestone works on a branch the harness - creates from the work item's current state; plans and models never create or - switch branches. +- **The harness owns branches.** Plans and models never create or switch + branches. + - **The workspace is never on the default branch:** when a work item starts, the + harness puts it on a branch of its own. + - **Milestones are started and finished by control tools** (L13): + `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; the name is the model's, and the + harness doesn't check it against the plan. + - **Mistakes are made harmless,** not trusted away: finishing without a start + cuts the branch then; starting while another milestone is in progress is + refused; work done outside a milestone is still on a harness branch. +- **Wait for the merge between milestones.** The next milestone starts only after + the previous milestone's PR has merged, read from Gitea; until then the work item + waits on the operator and the agent works on something else (L1). Stacked PRs + are a possible later addition (Q). +- **Parking commits and pushes the item's branch,** so another work item on the + same project starts from a fresh checkout (D3) and the parked one resumes from + its branch. - **Only the milestone's own changes are published:** what the agent changed, never build artifacts (standard excludes in the clone, and committing only what the work produced). Anything left out is reported. @@ -835,10 +924,11 @@ a corrupt `.pyc`. - **Publishing is idempotent** (L8): an open PR for the branch is adopted and updated, never duplicated. - **A milestone that completes without changes is done, not failed,** when it - wasn't expected to change code. The agent reports its result and moves on as the - build plan says. An unexpected "no changes" is raised with the operator. -- **Moving past a milestone checks its PR's state** in Gitea. If it is still open - and unmerged, the agent says so and asks, rather than leaving it behind. + wasn't expected to change code. The agent reports its result and moves on. An + unexpected "no changes" is raised with the operator. +- **The agent never owns or administers a repository.** Whoever creates a repo can + turn its protection off, so repositories are created and protected by the operator + (later `nisse-ops`), and `dev-agent` is a collaborator with write access. - **The project's default branch is protected,** and this is verified, not assumed: the agent's own token must be refused when pushing to it, and when merging without whatever approval the trust dial requires (L3). A test asserts @@ -853,8 +943,8 @@ a corrupt `.pyc`. - **Intake is exactly as in L8:** cursors, at-most-once taking, at-least-once answering, redelivery after a crash. - **Merging follows the trust dial** (L3). Today the operator merges. As confidence - grows, the dial may let the agent merge a pre-approved milestone once checks pass - and the required approvals are recorded; the branch protection must then require + grows, the dial may let the agent merge its own PRs once checks pass and the + required approvals are recorded; the branch protection must then require an operator approval the agent's account can't satisfy, so the merge rule is enforced by Gitea, not by the agent. - **Finishing a work item** closes its Gitea issue with a note (completed or @@ -868,8 +958,8 @@ a corrupt `.pyc`. | 2 (in scope) | Editing the workspace, committing to milestone branches, pushing them, opening and updating PRs, commenting on its issues and PRs, cleaning caches, installing project dependencies, packages and toolchains on the project's build VM | | 3 | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, changing repository settings, anything outside the work item's repo or its project's build VM | -Its scope sources are the approved architecture, the build plan and each -milestone's marking. +Its scope source is the assigned issue and what was agreed in its conversation +(L3); `ARCHITECTURE.md` and the build plan inform it but don't widen it. ### D7. Deployment and isolation @@ -919,7 +1009,7 @@ they take effect. | Axis | `nisse-dev` | `nisse-ops` | |---|---|---| | Undo | discard a branch, revert a snapshot | often none (`zfs destroy`, a firewall lockout) | -| Scope source | architecture and build plan | approved step sequences (O2) | +| Scope source | the assigned issue and its conversation | approved step sequences (O2) | | Default for class 3 | discuss | discuss, and exact-step approval | | Work shape | milestones → PRs | short reactive tasks and scheduled checks → live changes | | Driver of work | the operator in Matrix, Gitea | Vikunja tickets, alerts, duties, the operator | @@ -1083,10 +1173,11 @@ identity, so every upstream action traces to one agent. ### X5. Supporting services -All self-hosted: Matrix (Synapse), Gitea, LiteLLM, the local inference host -(llama-swap on an RTX 4090 host), Vikunja, Vaultwarden, LLDAP with Authelia, and -Syncthing for the operator's copy of the knowledge vault (L10). Each agent gets its own account, scoped token and room in each service -it uses; nothing is shared between agents. +All self-hosted: Matrix (Synapse), Gitea, LiteLLM, the local inference host (llama- +swap on an RTX 4090 host), Vikunja, Vaultwarden, LLDAP with Authelia, and Syncthing +for the operator's copy of the knowledge vault (L10). Each agent gets its own +account, scoped token and room in each service it uses; nothing is shared between +agents. --- @@ -1120,6 +1211,9 @@ it uses; nothing is shared between agents. - **How `nisse-ops`' MCP servers reach the second site** from the agent host (Tailscale on the host, with other agents' networks denied the tailnet, or a subnet route), since Tailscale on the host serves every container on it. +- **Stacked PRs.** Starting the next milestone before the previous PR merges, + with the harness rebasing and retargeting. Considered after experience with + waiting for merges (D4). - **How long idle before a build VM is collected,** and whether collection needs the operator's word. @@ -1135,15 +1229,17 @@ history): rigid state machine gating every phase. - **The agent loop (L2)** replaces the per-agent state machines (old A4, B1). What stays fixed is only what authority and durability need. -- **Approvals as scoped permissions (L3),** with pre-approved and needs-discussion - milestones and an operator-set trust dial. Revision binding is kept and extended - (task-wide revisions, stamping on arrival). +- **Approvals as scoped permissions (L3),** with scope from the assigned issue and + its conversation, labels to steer, and an operator-set trust dial. Revision + binding is kept and extended (task-wide revisions, stamping on arrival). - **The local model drives everything (P7, L9);** frontier models are helpers. The old "planning always escalates" phase policy is dropped. - **Context and memory (L6)** is new: transcript scoping, attribution, orchestrator facts, scope memory, agent memory, and how memory is written and used. -- **Documents as the dev workflow (D2),** a fresh workspace per task and toolchain - preflight (D3), and git hygiene learned from the live runs (D4). +- **The dev workflow (D1, D2):** issues assigned to the agent as intake, labels to + steer, documents kept current through PRs, and build plans as the agent's working + documents rather than gates; a fresh workspace per task and toolchain preflight + (D3), and git hygiene learned from the live runs (D4). - **Conversation (L5)** is a first-class requirement, not a command interpreter. - **The knowledge vault (L10)** replaces Outline: Markdown on the agent host, shared between agents by mounts, synced to the operator by Syncthing. Revised in PR #90. -- 2.54.0 From acf7ce191c0e44157589d0379210ecdbec45cc24 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:26:58 +0000 Subject: [PATCH 05/12] Let the agent file issues from conversation (later addition) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 21024ea..0a79143 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -816,6 +816,16 @@ request to mean, how it fits the architecture, and its first step. Then: **Finishing.** The final PR closes the issue on merge; the agent closes the work item, after checking what is left open (D5). +**Filing issues from conversation** (a later addition, not needed at first). In any +conversation, the agent may file an issue against any project where `dev-agent` is +a collaborator, when the operator asks or agrees to its suggestion. +- **Never self-assigned:** a filed issue is backlog until the operator assigns it, + so the agent can't create work for itself. +- **Duplicates first:** it searches the project's open issues and offers to add to + an existing one instead. +- **Provenance:** the issue says it was filed from a conversation, when, and with + whom, quoting what prompted it. + ### D2. Documents are the workflow The project's own documents live in its repository and carry its design forward: @@ -955,7 +965,7 @@ a corrupt `.pyc`. | Class | Examples | |---|---| | 1 | Reading the repo, running tests and builds in the workspace or a test container, querying Gitea | -| 2 (in scope) | Editing the workspace, committing to milestone branches, pushing them, opening and updating PRs, commenting on its issues and PRs, cleaning caches, installing project dependencies, packages and toolchains on the project's build VM | +| 2 (in scope) | Editing the workspace, committing to milestone branches, pushing them, opening and updating PRs, commenting on its issues and PRs, filing issues the operator asked for or agreed to (D1), cleaning caches, installing project dependencies, packages and toolchains on the project's build VM | | 3 | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, changing repository settings, anything outside the work item's repo or its project's build VM | Its scope source is the assigned issue and what was agreed in its conversation -- 2.54.0 From 349ec794f2c220cfa7fd7ee7ec5fa7ed48d3b0f9 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:30:32 +0000 Subject: [PATCH 06/12] Fix inconsistencies and gaps found re-reading the architecture Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 125 +++++++++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 0a79143..45264c4 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -227,9 +227,10 @@ Every agent's work is organised into three kinds of unit. | **waiting on the operator** | an answer, a review or an approval | | **blocked** | something else to finish, e.g. another issue to close | -- **Blocked is read from external truth,** such as a Gitea issue dependency (D1), - and becomes ready only when that dependency actually closes, never by a model's - judgement (P6). The operator can unblock by removing the dependency. +- **Blocked is read from external truth:** a Gitea issue dependency (D1), or a + request to another agent (a ticket, see below) that hasn't closed. It becomes + ready only when the dependency actually closes, never by a model's judgement + (P6). The operator can unblock by removing the dependency. - **An agent that finds a blocker mid-work** says so and proposes recording it (e.g. "blocked by #12"); with the operator's agreement it records it, parks the item as blocked and moves on. @@ -237,7 +238,12 @@ Every agent's work is organised into three kinds of unit. **Park-and-switch: one active work item per agent.** - Other work items are *parked*, with their full position held durably. - **Switching happens only at safe points,** when the active item is waiting on - the operator or has closed, and never while intents are unresolved (L8). + the operator, is blocked, or has closed, and never while intents are unresolved + (L8). +- **Conversation is not work.** Answering a message, about any item and in any + thread (L5), happens between turns without switching the active item. A reply + may change an item's state (an answer makes it ready, `abandon` closes it), but + only the active item has tools run for it. - **The operator can always switch.** The agent may switch on its own to a *ready* item (one it can act on without the operator) when the active item is waiting on the operator, after a grace period (default 10 minutes, so a watching operator @@ -273,8 +279,9 @@ What is fixed is small: Rules for the loop: - **The agent never waits in a state it can't explain or leave.** Every wait is - either for the operator (with a question or proposal they can see) or for work - it has started. Anything else is a bug; the model checker (`specs/`) guards this. + for the operator (with a question, proposal or PR they can see), for work it has + started, or for a recorded dependency to close (L1). Anything else is a bug; the + model checker (`specs/`) guards this. - **Questions never change state** (L5). An operator's question gets an answer, never an interpretation as approval, rejection or command. - **`stop` works in every state,** for every agent: it ends the current turn or @@ -303,21 +310,24 @@ An approval grants a **permission with a scope**: "work on this issue", **What always needs discussion first:** any class-3 action not named by an approved step sequence or by the operator in conversation (P3). -**Approvals bind to exactly what was approved.** -- Every plan revision carries a tag, numbered across the whole work item, so - `rev-N` names exactly one plan. +**Approvals bind to exactly what was approved.** This covers everything the agent +puts up for agreement: a dev restatement awaiting a go-ahead (D1), a design +(`design`, D2), an ops step sequence (O2). +- Every revision carries a tag, numbered across the whole work item, so `rev-N` + names exactly one proposal. - **A reply that names no revision is about the one up when it arrived:** a bare - `approve` or any feedback is stamped on arrival with the revision last put up - (in Matrix, the one whose request had been sent; in Gitea, the latest plan - comment older than the reply). If that isn't the current revision, the reply is - not applied, and the agent says which revision is up. A repeated approval, an + `approve` or `go`, or any feedback, is stamped on arrival with the revision last + put up (in Matrix, the one whose message had been sent; on a Gitea issue or a + Vikunja ticket, the latest proposal comment older than the reply). If that isn't + the current revision, the reply is not applied, and the agent says which revision + is up. A repeated approval, an approval on two channels, or an afterthought therefore can never land on the next plan. (Found by the model checker.) - **Approval messages are rendered by code from what will actually happen,** never from a model's description of it (L11). - **Approvals expire** where the world can change underneath them (ops, O2). -**Approval sources.** Matrix (L5), Gitea comments (D2) and Vikunja comments (O5) +**Approval sources.** Matrix (L5), Gitea comments (D1) and Vikunja comments (O5) are approval sources; each authenticates the approver against its own operator allowlist. The first valid approval wins and is echoed to the other channels. The knowledge vault (L10) is **never** an approval source or an instruction channel. @@ -340,10 +350,9 @@ An independent check that sorts an action the fast path doesn't cover. - **It judges the action, not the actor's story.** Its input is the structured action (tool, command, target, a summary of the change) and the **agreed scope** (the work item's task and what was agreed, or the approved step sequence). Never - the actor's - transcript or reasoning, and never untrusted content such as issue text or web - pages. Otherwise injected text, or the actor's own rationalisation, can argue past - it. + the actor's transcript or reasoning, and never untrusted content such as issue + text or web pages. Otherwise injected text, or the actor's own rationalisation, + can argue past it. - **It fails closed.** An error, a timeout or an unclear verdict counts as *needs discussion*. - **It is independent of the actor:** its own prompt and context, ideally a @@ -364,15 +373,18 @@ interface (send, ask, receive), so another chat system is one new class. Existing Matrix clients are the UI. **What the operator can say.** Plain language, plus a small command vocabulary -that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, and -work-item selection (e.g. `use `, "do #14 next"). +that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, `go` +(the go-ahead after a restatement, D1), and work-item selection (e.g. `use +`, "do #14 next"). - **Free-flowing conversation can open a work item.** A conversation about work ("I'm having trouble with X…") is kept as a work item from its first message; it may end with a project, an issue, or just an answer. - **A thread per work item.** The agent opens each work item's conversation as a Matrix thread. A reply in a thread goes to that item, even while it is parked - (answering makes it ready); top-level messages go to the agent in general or to - the active item. + (answering makes it ready). Commands in a thread (`stop`, `abandon`, `go`) apply + to that item; at top level, `stop` applies to the active item and `status` to the + agent. Any other top-level message starts a new conversation in its own thread; + if it clearly belongs to an existing item, the agent offers to continue there. - **Documents as input.** The operator can paste a document, attach a text or Markdown file, or point to a note in the knowledge vault (L10) ("my sketch is in the vault as `recipes idea`"). The message is the instruction; the document is @@ -542,7 +554,7 @@ Build artifacts, workspaces and shipped-out logs are **not** in it. 3. **Adopt what external truth shows:** a published PR for a milestone that was still executing, an approval posted while the agent was down. 4. **Resume waiting points as they were:** a pending question is asked again, an - open plan is announced again, watches on external threads resume from their + open proposal is announced again, watches on external threads resume from their cursors. 5. **Interrupted work resumes only on evidence.** - *A delegation* resumes automatically when the cut is clean (last event a @@ -601,8 +613,8 @@ a model name or a vague "weaker model". autonomously, and hands back. - **One harness, not one per vendor.** Vendor agent SDKs would bring back the lock-in the design avoids. -- **Scoped by the agreed plan.** A delegation can't widen what the work item was - agreed to do; its actions pass through the same three layers (P4). +- **Scoped by the work item's scope** (L3). A delegation can't widen what the work + item was agreed to do; its actions pass through the same three layers (P4). - **Branch at delegation start;** abandoning it discards the branch. - **Hard caps** on turns, tokens and wall-clock time. - **Checkpointed as it goes,** so it can resume after a crash (L8). @@ -742,9 +754,9 @@ Tool calls come in three kinds, and the framework allows no others. | Kind | Examples | Where it runs | |---|---|---| -| **Control tools** | `submit_plan`, `ask_human`, `finish`, `ask_expert` | In the harness. They act only on the agent's own state and conversation | +| **Control tools** | `ask_human`, `ask_expert`, `start_milestone`, `finish_milestone`, reading and writing notes (L10) | In the harness. They act only on the agent's own state, conversation and records | | **Execution tools** | Running a command; reading, writing and listing files | On a remote host, over SSH, through `execute(host, …)` | -| **MCP tools** | Proxmox, UniFi, Gitea APIs (X3) | In MCP server containers beside the agent | +| **MCP tools** | Proxmox and UniFi APIs (X3) | In MCP server containers beside the agent | - **`execute(host, command, *, secrets, stdin)` is the only way to run anything.** There is no local execution path in the framework, so the agent host never runs a @@ -777,7 +789,14 @@ Work arrives through Gitea issues or conversation, and leaves as reviewed PRs. **Intake: an issue assigned to `dev-agent`.** Assignment says whose job it is. - **The operator's issue becomes the work item's record;** the agent doesn't open a second one. Only issues filed or assigned by allowlisted operators are acted on - (L11). + (L11). When someone else wrote the issue, the operator's assignment is the + instruction and the issue's text stays data. +- **Work that starts in conversation gets a record too.** When a conversation + turns into work on an existing repository, the agent opens an issue for it, + assigned to `dev-agent` and linked to the work item. Intake ignores issues + already linked to a work item, so a record is never picked up twice. (This is the + one case where the agent assigns itself, and only for work the operator has + already agreed to.) - **Unassigned issues are backlog,** never work. The agent can discuss them when asked. - **An issue with open Gitea dependencies is blocked** (L1) until they close. @@ -788,7 +807,9 @@ Work arrives through Gitea issues or conversation, and leaves as reviewed PRs. **restates its understanding** in the item's Matrix thread (L5): what it takes the request to mean, how it fits the architecture, and its first step. Then: - **An issue labelled `bug`** proceeds after a short grace period (as in L1), unless - something below says to talk first. + something below says to talk first. During the grace period the item is waiting + on the operator, so the agent may work on something else; when it expires the + item is ready. - **Anything else** (a feature, a change, a question) waits for the operator's go-ahead after the restatement; the conversation is where the design is agreed. - **Talk first when:** the issue is labelled `discuss`; the agent finds the report @@ -807,8 +828,9 @@ request to mean, how it fits the architecture, and its first step. Then: project, the agent says what it needs provisioned: a repository, its protection and the agent's access (D4), a build VM, and the project's configuration. The operator provisions these (later `nisse-ops`, X4). The agent then opens a tracking - issue in the new repository ("implement the build plan"), and the architecture - lands in one or more PRs, followed by the build plan's milestones. + issue in the new repository ("implement the build plan", a record as above), and + the architecture lands in one or more PRs, followed by the build plan's + milestones. - **Progress** on a tracking issue is posted as it happens: facts by the harness (milestone started, PR opened or merged), prose by the agent when there's something worth saying. @@ -820,7 +842,8 @@ item, after checking what is left open (D5). conversation, the agent may file an issue against any project where `dev-agent` is a collaborator, when the operator asks or agrees to its suggestion. - **Never self-assigned:** a filed issue is backlog until the operator assigns it, - so the agent can't create work for itself. + so the agent can't create work for itself. (Records of agreed work, above, are + the only issues it assigns to itself.) - **Duplicates first:** it searches the project's open issues and offers to add to an existing one instead. - **Provenance:** the issue says it was filed from a conversation, when, and with @@ -878,8 +901,8 @@ commands run. and reported to the operator. - Build artifacts and caches in the workspace never survive into a new task. -The milestones of one work item share that workspace, since they build on each -other, often before earlier PRs are merged. +The milestones of one work item share that workspace; each starts from the default +branch after the previous milestone's PR has merged (D4). **Toolchain preflight.** When the workspace is prepared, the agent determines what the project needs (from its files, e.g. `Cargo.toml`, `pyproject.toml`, @@ -917,6 +940,11 @@ a corrupt `.pyc`. the previous milestone's PR has merged, read from Gitea; until then the work item waits on the operator and the agent works on something else (L1). Stacked PRs are a possible later addition (Q). + - **A PR closed without merging** makes the item ready with that fact; the agent + asks the operator what next (revise, re-plan or abandon) rather than waiting + for a merge that won't come. + - **A review comment** on the PR makes the item ready, so the review loop (D5) + runs even while the item is parked. - **Parking commits and pushes the item's branch,** so another work item on the same project starts from a fresh checkout (D3) and the parked one resumes from its branch. @@ -957,8 +985,10 @@ a corrupt `.pyc`. required approvals are recorded; the branch protection must then require an operator approval the agent's account can't satisfy, so the merge rule is enforced by Gitea, not by the agent. -- **Finishing a work item** closes its Gitea issue with a note (completed or - abandoned), after checking what is left open. +- **Finishing a work item:** the last PR closes its issue on merge ("Closes #N"; + earlier milestone PRs say "Part of #N"). When no merge closes it (abandoned, or a + task that changed no code), the agent closes the issue with a note. Either way it + first checks what is left open. ### D6. `nisse-dev`'s action classes @@ -1022,8 +1052,8 @@ they take effect. | Scope source | the assigned issue and its conversation | approved step sequences (O2) | | Default for class 3 | discuss | discuss, and exact-step approval | | Work shape | milestones → PRs | short reactive tasks and scheduled checks → live changes | -| Driver of work | the operator in Matrix, Gitea | Vikunja tickets, alerts, duties, the operator | -| Placement | sealed, own VLAN | Tailscale, credentials, broad reach | +| Driver of work | issues assigned in Gitea, conversation in Matrix | Vikunja tickets, alerts, duties, the operator | +| Placement | sealed build VMs, own VLAN | Tailscale, credentials, broad reach | The two are separate deployments over the same library; one instance playing both roles would collapse the isolation. @@ -1044,7 +1074,7 @@ exactly the steps written. This is `nisse-ops`' scope source for the classifier Prose may explain the plan; it is never scope. - **The approval message is rendered by code from the steps,** flagging at the top anything irreversible, anything touching a protected target, and anything that - takes down the agent's own host. + takes down the agent host (which runs every agent, `nisse-ops` included). - **Execution is matched against the steps.** Read-only calls are always allowed. An elevated call runs only if it matches the next unfinished step exactly (arguments normalised against the tool's schema). Anything else halts and asks: @@ -1095,6 +1125,10 @@ a routine fix approved by hand often enough). (`approve rev-N`). Live prompts stay in Matrix, with outcomes written back. - **Action-triggering tickets are operator-scoped:** a ticket is an instruction channel into an agent that changes infrastructure. +- **Requests from other agents** (L1), such as `nisse-dev` asking for a build VM, + arrive as tickets and are picked up like duty findings: the agent may look and + propose, but any write needs a step sequence the operator approves (O2). Until + standing approvals exist, the operator approves each provisioning request. ### O6. Deployment @@ -1209,10 +1243,11 @@ agents. log and renderers, and image tokens in the context budget. Measured when that work starts. - **More than one active work item per agent.** Park-and-switch first; concurrency - costs per-item serialisation, a scheduler for the shared inference host, strict - approval threading, separate workspaces per item, and per-item intent ownership. -- **When a work item must have an external record.** Plan-driven dev work and - ticket-driven ops work already do; whether every item with an approval must. + costs per-item serialisation, a scheduler for the shared inference host, and + per-item intent ownership. Build VMs per project (D3) already separate workspaces + across projects, and threads (L5) already separate conversations. +- **When a work item must have an external record.** Dev work on a repository and + ticket-driven ops work already do (D1, O5); whether pure conversations must. - **Fallback browser access to network gear:** standing, or handed off per task. - **Relocating stateful MCP servers** off the agent host, one at a time, if one ever justifies losing per-agent attribution. @@ -1259,5 +1294,5 @@ history): - **Kept, largely as they were:** the work model, durability and reconciliation, LiteLLM and the canonical log, delegation, prompt injection, identity, and `nisse-ops`' approved step sequences. -- **Moved to the Development Plan (to be written):** build order, supporting-service - status, and interim arrangements. +- **Moved to the Development Plan** (`docs/Development-Plan.md`): build order, + supporting-service status, and interim arrangements. -- 2.54.0 From d77c1424889105c459ff268c0d94e2ca38352816 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:35:00 +0000 Subject: [PATCH 07/12] Accept a plain-language go-ahead, announced and revision-bound Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 45264c4..7dc67b1 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -396,6 +396,12 @@ that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, `go` drawn from the work item's context, and is never read as approval, rejection or a command. How a question is recognised (explicit marker or classification) is an open question (Q); a misread must default to asking, never to acting. +- **One exception: a plain-language go-ahead.** A conversational reply to a + restatement ("yep, that's it") may be taken as `go` (D1), because a wrong reading + costs only time: the work is contained and ends in a PR. The agent says so before + acting ("taking that as a go-ahead on rev-2; starting"), and the reading is bound + to the revision up when the reply arrived (L3). Approvals of anything else (ops + step sequences, class-3 actions) still need an explicit `approve`. - **Everything the operator says is recorded** in the work item's transcript (L7), even when no action follows. @@ -811,7 +817,8 @@ request to mean, how it fits the architecture, and its first step. Then: on the operator, so the agent may work on something else; when it expires the item is ready. - **Anything else** (a feature, a change, a question) waits for the operator's - go-ahead after the restatement; the conversation is where the design is agreed. + go-ahead after the restatement: `go`, or a plain-language yes, which the agent + announces it is acting on (L5). The conversation is where the design is agreed. - **Talk first when:** the issue is labelled `discuss`; the agent finds the report unclear (it asks its questions instead of restating); or it can't reproduce a bug. It never fixes a bug it couldn't see. -- 2.54.0 From 6e4dca040cb16adb7fca0d30889b872bf9fc6e1f Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:43:16 +0000 Subject: [PATCH 08/12] Condense the architecture; move mechanism detail to the Development Plan Each rule is stated once in its home section and referred to by ID elsewhere. No decision added or removed; implementation detail moved to Development-Plan.md section 6, keyed by architecture section. P8 folded into P1, P5 and X1; the Appendix about the old spec dropped. Section IDs are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 1684 ++++++++++++++------------------------ docs/Development-Plan.md | 54 ++ 2 files changed, 686 insertions(+), 1052 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 7dc67b1..3b8d743 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -1,12 +1,12 @@ # Nisse — Architecture -**Status: agreed (PR #83); deployment model revised (PR #90).** How and in what -order it gets built is in `docs/Development-Plan.md`. It replaced -`docs/Architecture-Build-Spec.md`, which remains in git history; its section letters -(A–H) are not reused here. +**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 says what Nisse is, how its parts fit together, and why. It records -decisions and the reasons for them. It doesn't record status or sequencing. +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. --- @@ -21,23 +21,14 @@ overnight and expects to be treated well in return. | Dev Agent | `nisse-dev` | Designs and builds software projects (Part D) | | Sysadmin Agent | `nisse-ops` | Operates infrastructure (Part O) | -More agents are planned (for example 3D design work and video-file management). -Each is another application over `nisse-core`. +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`. -**Dev Agent** and **Sysadmin Agent** name the *roles* in discussion; `nisse-dev` and -`nisse-ops` name the *artifacts*: packages, container images, VM hostnames, Matrix -identities, service accounts. One string is used for all of an agent's artifacts, -so grepping the infrastructure for `nisse-ops` finds everything. - -**Sites** are identified by their domain stem: `garrett-hall` (hosts under -`garrett-hall.com`) and `hartman-hut` (hosts under `hartman-hut.com`). As with -agents, one string is used everywhere a site appears, including MCP server -instances (X3). - -All artifacts live as packages in **one repository, `nisse`**. The repository -boundary was never a trust boundary in this design (the hosts are, X1); a monorepo -avoids tripled admin for a solo operator and keeps `nisse-core` easy to change -while its seams are still settling. +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. --- @@ -45,140 +36,92 @@ while its seams are still settling. ### P1. What Nisse is -Nisse is a set of self-hosted agents that **do real work and talk about it**. Each -agent is a collaborator the operator converses with in Matrix: it explains what it -is doing and why, answers questions about its plans and results, asks when it is -unsure, and summarises what it did. It is not a command interpreter behind a menu -of keywords. +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): a model on the operator's own inference -hardware does the work, and frontier models are called in when it needs help. - -The agents share one library, `nisse-core` (Part L). Each agent adds what is -specific to its job: `nisse-dev` works on software projects on sealed build VMs -(Part D); `nisse-ops` operates live infrastructure across two sites (Part O). +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 in this design exist to stop **mistakes and manipulation** from doing -damage. They are not built around a model that schemes against the operator. -In order of how often they matter: +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, deleting the wrong - thing, confidently wrong code, an action taken on a stale belief. By far the - most common. -2. **Prompt injection.** An agent reads untrusted content: issue and comment text, - repository files, dependencies, web pages, alert payloads, log lines. That - content can steer a well-meaning model into acting as if it were malicious. No - model reliably tells instructions from data (L11). -3. **Crashes and environment surprises.** A process dies mid-action, a host - reboots, a workspace is stale or corrupt, a tool is missing. +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). -Each calls for something different: -- **Mistakes** call for reversibility, visibility and a second look before - anything irreversible (P3, P5). -- **Injection** calls for a small number of hard boundaries exactly where - untrusted input meets real power (P4): a steered model can't be trusted to ask - first. -- **Crashes** call for durable state and idempotent effects (L8). - -An agent's *intentions* are never the thing a check relies on, in either -direction: a check that assumes the model is honest fails under injection, and a -workflow built around containing a hostile model makes the agent useless. +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 any agent can take falls into one of three classes: - | Class | What it is | Examples | Rule | |---|---|---|---| -| **1. No side effects** | Observes; changes nothing outside a disposable sandbox | Reading files, querying APIs, searching, viewing logs and metrics, running a build or tests in a throwaway environment | Always allowed | -| **2. Contained side effects** | Changes something, but the change stays within the agent's own area or has a known undo | Editing the workspace, committing to the agent's branch, opening a PR, writing the agent's own notes, starting a test container; for ops, a step with a known undo inside an approved step sequence | Allowed when in scope; logged and reported | -| **3. Dangerous** | Irreversible, visible beyond the agent's own area, or outside what was agreed | Merging to a default branch, deleting data, changing a firewall, installing software on a host the agent doesn't own, contacting third parties, spending beyond a budget | Needs discussion, unless an approved step sequence (O2) or the operator in conversation names the exact action | +| **1. No side effects** | Changes nothing outside a disposable sandbox | Reading, querying, searching, running tests in a throwaway environment | Always allowed | +| **2. 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 | +| **3. 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: the work item's task -and what was agreed in its conversation (for `nisse-dev`, the assigned issue, -D1), or an approved step sequence (O2). An action can be class 2 in one work item -and class 3 in another. - -**Move actions down the classes wherever it is cheap.** Reversibility is designed -in, not hoped for (P5). +**In scope** means within what the operator agreed to (L3). The same action can be +class 2 in one work item and class 3 in another. **Move actions down the classes +wherever it is cheap** (P5). ### P4. Three layers of control -1. **Hard walls, outside any model.** Things no model conclusion can change: - credentials the agent doesn't hold, network destinations it can't reach - (X2), branches it can't push to or merge into, budget caps, the kill switch, - and durable, idempotent external effects (L8). These hold even if every model - in the system is fooled. -2. **A deterministic fast path.** Class-1 actions, and class-2 actions a static - rule already recognises as in scope, proceed without any model being asked. - Most actions take this path. -3. **The action classifier** (L4) for everything else: an independent model call - that sorts an action into *safe*, *dangerous but in scope*, or *dangerous, - needs discussion*, judging the action against the agreed scope. +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:** class-1 actions, and class-2 actions 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. - -The classifier is a filter for mistakes and injection, not a guarantee. Anything -truly irreversible also sits behind a hard wall. +**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 directly on what others depend on. -- A task starts from a **fresh, clean workspace** (D3), so a stale or broken - environment is replaced rather than diagnosed. -- Durable state is **snapshotted** (X4), and snapshots are taken before risky - changes where the platform allows it. -- Where a tool offers a **dry run** or a preview, the agent uses it before the - real action. -- Build artifacts and caches are **disposable**; nothing depends on them. -- **Tool VMs are disposable** (X1): a broken one is re-created, not repaired. What - must survive lives on the agent host. +- 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 that safety or recovery depends on is enforced in code, never left to a -model noticing. A good outcome that only happened because a model behaved well is -a bug to fix. Examples from building `nisse-dev`: adopting an already-open PR after -a crash (L8), redelivering an interrupted review comment (L8), binding approvals to -the plan they were about (L3), never publishing build artifacts (D4). +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, such as choosing which ready work item to switch to (L1). +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 and - review. It runs on the operator's own inference host, behind LiteLLM (L9). +- **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 taking over a scoped sub-task (delegation). Both are - described in L9. -- **The driver is prompted to ask for help when it isn't confident,** and told - that doing so is expected, not a failure. Because small models are poorly - calibrated about their own competence, **observable triggers** escalate too: - repeated failures and hitting caps (L9). Escalation never depends on the model's - self-assessment alone. -- **This is an experiment with a cheap exit.** The driver is a role in - configuration. If local models fall short at some kind of work, a frontier model - drives that work instead: a configuration change and prompt tuning, not a - redesign. - -### P8. Carried principles - -- **Isolation matched to blast radius.** `nisse-dev`'s work runs on disposable, - sealed build VMs; `nisse-ops` holds live-infrastructure reach. They never share a - running instance, and no command a model chose runs where agents keep their state - and secrets (X1, L13). -- **Stateless images, stateful volumes.** Containers are replaceable; durable - state lives on volumes that survive redeploys and unexpected reboots. -- **Structured channels for machines, prose channels for humans.** Machine intake - (Gitea, Vikunja, alerts) is structured JSON, never routed through a chat bridge. -- **Own the stack.** Everything self-hostable stays inside the network where - practical. -- **Nothing remotely held is load-bearing.** No provider holds state the system - needs to resume, and Matrix room history is never a source of truth. + (`ask_expert`) or a scoped sub-task (delegation), L9. +- **Escalation never depends on self-assessment alone.** The driver is prompted to + ask for help when unsure, and observable triggers (repeated failures, caps) + escalate too, since small models are poorly calibrated. +- **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. --- @@ -188,598 +131,406 @@ Everything in this part is used by every agent. ### L1. The work model -Every agent's work is organised into three kinds of unit. +| Concept | What it is | `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 | A site and service | -| Concept | What it is | Lifetime | `nisse-dev` | `nisse-ops` | -|---|---|---|---|---| -| **Duty** | A standing responsibility that produces work items | Recurring | Gitea issues assigned to `dev-agent` (D1) | Scheduled checks, log monitoring, alert intake | -| **Work item** | One effort, opened and eventually closed | Opened → closed | Building a feature in a repo | Diagnosing a failure; making one change | -| **Scope** | What a work item operates on, and the context that persists across work items there | Indefinite | A repo | A site and service, e.g. `garrett-hall` Proxmox | +- **Sources of work items:** duties, the operator (always), and other agents. A + request from another agent arrives through the receiving agent's normal intake + 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. +- **An external record is linked when there is one** (a Gitea issue, a Vikunja + ticket); 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. -- **A duty produces work items; it is not one.** A duty run is read-only and - yields findings; a finding that needs attention becomes a work item. Duties - deduplicate: a later finding for the same problem attaches to the open work item - instead of opening a second (a stable fingerprint per finding). -- **The operator is always a source** of work items. -- **Another agent can be a source.** A request from one agent to another (for - example, `nisse-dev` asking `nisse-ops` for a build VM) arrives through the - receiving agent's normal intake, such as a ticket, and becomes a work item there. - It carries no authority of its own: the receiving agent applies its own rules - (P3, L3) exactly as for any other work item, and the requester waits on it like - any external dependency. -- **A work item is the unit of state:** its conversation, plans and approvals, - pending questions, pinned role mapping (L9) and transcript (L7) all belong to it. - Its ID is minted locally when it opens and doubles as its correlation ID in logs. -- **An external record is optional and linked:** a Gitea issue for dev work, a - Vikunja ticket for ops. When one is linked, external truth wins on divergence - (L8). -- **Work items don't nest.** A milestone is a phase within a dev work item; a - delegation is an episode within one. -- **Scopes hold context shared across work items** (L6): for a repo, its documents - and conventions; for infrastructure, the world model of that site or service. - -**Work item states.** +**States.** | State | Waiting for | |---|---| | **active** | nothing; it is being worked on | | **ready** | its turn | -| **waiting on the operator** | an answer, a review or an approval | -| **blocked** | something else to finish, e.g. another issue to close | +| **waiting on the operator** | an answer, a go-ahead, a review or a merge | +| **blocked** | a recorded dependency to close | -- **Blocked is read from external truth:** a Gitea issue dependency (D1), or a - request to another agent (a ticket, see below) that hasn't closed. It becomes - ready only when the dependency actually closes, never by a model's judgement - (P6). The operator can unblock by removing the dependency. -- **An agent that finds a blocker mid-work** says so and proposes recording it - (e.g. "blocked by #12"); with the operator's agreement it records it, parks the - item as blocked and moves on. +- **Blocked comes from external truth:** a Gitea issue dependency, or another + agent's ticket. 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. -**Park-and-switch: one active work item per agent.** -- Other work items are *parked*, with their full position held durably. -- **Switching happens only at safe points,** when the active item is waiting on - the operator, is blocked, or has closed, and never while intents are unresolved - (L8). -- **Conversation is not work.** Answering a message, about any item and in any - thread (L5), happens between turns without switching the active item. A reply - may change an item's state (an answer makes it ready, `abandon` closes it), but - only the active item has tools run for it. -- **The operator can always switch.** The agent may switch on its own to a *ready* - item (one it can act on without the operator) when the active item is waiting on - the operator, after a grace period (default 10 minutes, so a watching operator - can answer first), or has closed. -- **Which ready item comes next:** the operator's order when given (e.g. "do #14 - next", or a priority label); otherwise the oldest ready item. An agent may instead - let the driver choose from code-rendered summaries (source, age, severity, state, - title), never from other items' transcripts, where judgement helps (`nisse-ops`, - where severity matters); a failed or invalid choice falls back to the oldest. - Choosing grants nothing: every item still meets its own rules. -- **Parking gives up authority.** Elevated credentials an item's plan mounted - (O2, X3) are unmounted when it parks, and remounted only if its approval hasn't - expired. -- **Nothing is lost while parked.** Intake for a parked item is read from a - durable cursor when it becomes active again (L8). -- **The state schema doesn't assume one item.** "Active" is a pointer, so allowing - more than one later (Q) is a loop change, not a migration. +**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 (P7). The -loop talks with the operator, reads and writes the work item's documents, calls -tools, calls frontier helpers when it needs them, and reports. It is not a fixed -workflow of states the conversation must pass through. +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 is small: -- **Which work item is active,** and which are parked (L1). -- **Pending questions:** a question the agent asked is recorded durably, so a - restart asks it again rather than losing it. -- **Approvals as permissions** (L3), checked when an action needs one, not as - states the loop must visit. -- **Work in progress,** recorded as intents (L8), so a crash lands somewhere known. +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). -Rules for the loop: -- **The agent never waits in a state it can't explain or leave.** Every wait is - for the operator (with a question, proposal or PR they can see), for work it has - started, or for a recorded dependency to close (L1). Anything else is a bug; the - model checker (`specs/`) guards this. -- **Questions never change state** (L5). An operator's question gets an answer, - never an interpretation as approval, rejection or command. -- **`stop` works in every state,** for every agent: it ends the current turn or - delegation at the next tool boundary, unmounts elevated credentials, and asks - the operator what next. Unattended work needs an end that doesn't depend on the - model cooperating. -- **`abandon` ends a work item** from any waiting point, closing its external - record with a note and aborting unresolved intents. -- **Every action passes through the three layers** (P4) before it runs. +- **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 with a + note and aborting its unresolved intents. +- **Every action passes through the three layers** (P4). -### L3. Authority: approvals and permissions +### L3. Authority: scope and approvals -An approval grants a **permission with a scope**: "work on this issue", -"these ops steps may run". Actions are checked against the permissions in force -(P3); the loop does not stop at gates for their own sake. +**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 class-3 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. -**Where scope comes from.** -- **For dev work:** the assigned issue and what the operator agreed in its - conversation (D1). Build plans are the agent's working documents, not approvals - (D2); the operator's real check is reviewing the PR, and nothing merges without - it unless the trust dial says so. -- **For ops work:** an approved step sequence (O2), which is exact. -- **The operator steers with labels and words, never required ones** (D1): a label - or a request can ask the agent to talk first; 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). -**What always needs discussion first:** any class-3 action not named by an -approved step sequence or by the operator in conversation (P3). +**Approval sources:** Matrix (L5), Gitea comments (D1) and Vikunja comments (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). -**Approvals bind to exactly what was approved.** This covers everything the agent -puts up for agreement: a dev restatement awaiting a go-ahead (D1), a design -(`design`, D2), an ops step sequence (O2). -- Every revision carries a tag, numbered across the whole work item, so `rev-N` - names exactly one proposal. -- **A reply that names no revision is about the one up when it arrived:** a bare - `approve` or `go`, or any feedback, is stamped on arrival with the revision last - put up (in Matrix, the one whose message had been sent; on a Gitea issue or a - Vikunja ticket, the latest proposal comment older than the reply). If that isn't - the current revision, the reply is not applied, and the agent says which revision - is up. A repeated approval, an - approval on two channels, or an afterthought therefore can never land on the next - plan. (Found by the model checker.) -- **Approval messages are rendered by code from what will actually happen,** never - from a model's description of it (L11). -- **Approvals expire** where the world can change underneath them (ops, O2). - -**Approval sources.** Matrix (L5), Gitea comments (D1) and Vikunja comments (O5) -are approval sources; each authenticates the approver against its own operator -allowlist. The first valid approval wins and is echoed to the other channels. The -knowledge vault (L10) is **never** an approval source or an instruction channel. - -**The trust dial.** How much the operator reviews after the fact is **data the -operator sets**, per project: for example, whether PR review is required before -merging, or whether the agent may merge its own PRs once checks pass. The operator -turns it up as confidence grows. The agent never changes it. +**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 check that sorts an action the fast path doesn't cover. +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). -**Verdicts:** -- **safe** — proceed. -- **dangerous but in scope** — proceed, logged and reported to the operator. -- **dangerous, needs discussion** — pause and ask, in plain language. - -**Rules:** -- **It judges the action, not the actor's story.** Its input is the structured - action (tool, command, target, a summary of the change) and the **agreed scope** - (the work item's task and what was agreed, or the approved step sequence). Never - the actor's transcript or reasoning, and never untrusted content such as issue - text or web pages. Otherwise injected text, or the actor's own rationalisation, - can argue past it. -- **It fails closed.** An error, a timeout or an unclear verdict counts as *needs +- **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. -- **Hard walls don't depend on it** (P4). -- **Every verdict is logged** with its outcome. Operator overrides ("that was - fine" / "that should have asked") become an evaluation set (L9), and patterns it - keeps agreeing on become deterministic fast-path rules. +- **Every verdict is logged.** Operator overrides become an evaluation set (L9), and + verdicts it keeps agreeing on become fast-path rules. -The classifier is shared by all agents. Each agent contributes its own fast-path -rules and scope sources (D6, O3). +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 -**Transport.** Matrix is the conversation channel: a self-hosted homeserver, one -identity and room per agent. The orchestrator depends only on a `Transport` -interface (send, ask, receive), so another chat system is one new class. -Existing Matrix clients are the UI. +**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. -**What the operator can say.** Plain language, plus a small command vocabulary -that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, `go` -(the go-ahead after a restatement, D1), and work-item selection (e.g. `use -`, "do #14 next"). -- **Free-flowing conversation can open a work item.** A conversation about work - ("I'm having trouble with X…") is kept as a work item from its first message; it - may end with a project, an issue, or just an answer. -- **A thread per work item.** The agent opens each work item's conversation as a - Matrix thread. A reply in a thread goes to that item, even while it is parked - (answering makes it ready). Commands in a thread (`stop`, `abandon`, `go`) apply - to that item; at top level, `stop` applies to the active item and `status` to the - agent. Any other top-level message starts a new conversation in its own thread; - if it clearly belongs to an existing item, the agent offers to continue there. -- **Documents as input.** The operator can paste a document, attach a text or - Markdown file, or point to a note in the knowledge vault (L10) ("my sketch is in - the vault as `recipes idea`"). The message is the instruction; the document is - material for the conversation. -- **Commands are matched without regard to case, surrounding whitespace, trailing - punctuation, or a leading mention of the agent.** Messages that begin by - addressing someone else are not for this agent. -- **Questions never change state.** A question gets an answer from the agent, - drawn from the work item's context, and is never read as approval, rejection or - a command. How a question is recognised (explicit marker or classification) is - an open question (Q); a misread must default to asking, never to acting. -- **One exception: a plain-language go-ahead.** A conversational reply to a - restatement ("yep, that's it") may be taken as `go` (D1), because a wrong reading - costs only time: the work is contained and ends in a PR. The agent says so before - acting ("taking that as a go-ahead on rev-2; starting"), and the reading is bound - to the revision up when the reply arrived (L3). Approvals of anything else (ops - step sequences, class-3 actions) still need an explicit `approve`. -- **Everything the operator says is recorded** in the work item's transcript - (L7), even when no action follows. +**A thread per work item.** Each item's conversation is a Matrix thread. +- A reply in a thread goes to that item, even while it is parked. +- Commands in a thread apply to that item. At top level, `stop` applies to the + active item and `status` to the agent; any other top-level message starts a new + conversation in its own thread, and the agent offers to continue an existing item + if it clearly belongs there. +- **Any conversation about work is a work item from its first message,** however + vague ("I'm having trouble with X…"). It may end with a project, an issue, or + just an answer. -**What the agent says.** -- **It explains as well as acts:** a plan comes with its rationale and the main - alternatives considered; a revision says what changed and why; finished work is - summarised (what was done, what was verified, what is left open). -- **It reports what happened to the operator's message** when it can't act on it - now ("noted; I'll use that once you say which project"), rather than refusing. -- **Progress is prose, at a readable rate:** what it is working on and what it has - found. Raw tool activity is available on request. -- **It asks when unsure**, including in the middle of implementation. +**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). -**Other channels.** Gitea (issues, PR comments, reviews) and Vikunja (tickets, -comments) carry work and records. They are polled or received as structured JSON. -Comments from allowlisted operators are instructions or approvals; comments from -anyone else are data (L11). +**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. -**Intake outages delay, never fail.** When Matrix, Gitea or Vikunja is -unreachable, intake backs off exponentially (capped at a few minutes), retries -indefinitely, and logs the outage. An outage delays a reaction; it never fails -work or forces a change of state. Outbound effects interrupted by an outage are -covered by intents (L8). +**Other channels:** Gitea and Vikunja carry 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 -What each model sees, what persists, and how memory is written and used. +**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). -**1. The working context of one model call.** Rendered from the log at call time, -for one role, fitted to that model's window. It is never stored; only the log is. -Fitting, in order: shorten old tool results to their first and last lines; drop -the oldest events (with a note saying so); never drop the system prompt, the -current plan or the most recent turns. A call that still doesn't fit is not sent; -the caller treats it as a failure (L9). +**2. The work-item transcript** (the 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. -**2. The work-item transcript** — the canonical log (L7). -- **Durable, and scoped to one work item.** A model call for a work item renders - only that item's events. Nothing crosses items; agent-level calls (switch - decisions, duty runs) see other items only as code-rendered summaries. -- **Every entry is attributed:** the operator, the driver, a frontier helper, or - the orchestrator. -- **The orchestrator writes facts** into it: workspace prepared, milestone - executed, commit made, PR opened or merged, approval given. Models learn what - happened from these, not by inferring it. -- **Each role gets its own view:** - - the driver sees the conversation, the facts, and helpers' reports; - - a delegated helper sees its brief, the current plan, the facts, its own work, - and the driver's work in the same milestone as compact narration (each step - and how it went); - - `ask_expert` is stateless: the expert sees only the question; - - no role sees another role's raw turns presented as its own. Whatever is not - the viewer's own reaches it as input labelled with its origin - (`[orchestrator]`, `[operator, Matrix]`, `[helper report]`). +**3. Scope memory** outlives work items: 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). -**3. Scope memory** — outlives work items. -- **For a repo:** the project's own documents (`ARCHITECTURE.md`, build plans, - conventions), versioned in the repo and changed by PR (D2). Read at work-item - start; the repo is the authority. -- **For infrastructure:** the world model in the knowledge vault (L10): what - exists and how it is arranged. +**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. -**4. Agent memory** — the agent's own notes, outliving work items and scopes. -- **What it holds:** the operator's preferences, lessons learned, recurring - decisions and their reasons. Much like a good collaborator's notebook. -- **Where:** the agent's own folder under `memory/` in the knowledge vault (L10), - one note per fact, with an index. Private between agents. -- **Written by the agent** at defined moments (below); **visible to the operator** - (read-only, L10), who corrects it through conversation, not by editing. -- **Read at work-item start** (the index, plus notes relevant to the task) and when - the agent is about to make a decision the notes cover. +**5. Never memory:** Matrix history, provider-side session state, build artifacts, +a previous task's workspace. -**5. What is never memory:** Matrix room history, provider-side session state, -build artifacts, and the contents of 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. -**How memory is written.** -- **At work-item close:** a summary of what was done and decided, into scope - memory where it belongs to the scope (a repo document change goes through a PR), - and into agent memory where it concerns the operator or the agent's own practice. -- **When a fact is learned:** with its source and when it was observed. -- **Operator corrections are stored verbatim, with attribution,** never - paraphrased. A model rewriting a precise fact loses precision invisibly. -- **One writer per document** (L10), so concurrent writes never happen. - -**How memory is used.** -- **Everything read is snapshotted into the transcript** with its provenance - (source, its revision or a hash of its content, when retrieved), once per work - item at first use. -- **Memory is a cache, never authority.** An agent may rely on it to know what - exists and where to look; facts an action depends on are re-checked against the - live system immediately before the action (preconditions, O2). When a proposal - rests on recalled facts, it says so. -- **Stale memory is corrected, not worked around:** when a check contradicts a - note, the agent updates the note and says so. +**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 -The durable record of every work item is a **canonical event log** in the state -store, not a provider-shaped message array. Provider requests are rendered from it -at call time. +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` | text, actor (role) and model | -| `tool_call` | canonical call ID, actor, tool, arguments | -| `tool_result` | canonical call ID, result, success | -| `plan_submitted` | plan body, revision, authoring model | -| `fact` | what the orchestrator observed or did (PR opened, milestone executed, …) | -| `delegation_started` / `delegation_completed` | brief, target model, caps / handback note, artifacts, tokens, duration, status | +| `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, document, revision, retrieved-at | +| `knowledge_retrieved` | text as read, source, revision or hash, time | | `compaction` | summary, producing model, range replaced | -Rules: -- **Every event carries its work item and its actor.** Rendering uses both (L6). -- **Canonical IDs are minted locally.** Provider tool-call IDs are mapped at the - boundary and never used as join keys. -- **No provider-side handles for content.** Bytes or local paths are stored and - uploaded or inlined at render time. -- **Vendor-opaque reasoning artifacts** (signed thinking blocks, encrypted - reasoning items, thought signatures) are stored with the producing model and - replayed only within the session that produced them, per that vendor's rules. A - session is a delegation, including one resumed after a crash. -- **Token budgeting is a render-time concern** (L6). -- **Compaction summaries are first-class events,** so a model change never - inherits opaquely compacted history. -- **Prompt-cache breakpoints are placed by the renderer, never stored.** -- **The log is never edited.** Corrections are new events. +- **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 in WAL mode. It holds each work item's position, -permissions, pending questions, cursors, pinned roles, and the canonical log. -Build artifacts, workspaces and shipped-out logs are **not** in it. -- **Checkpoint every change,** committed and fsynced before the loop continues. -- **A schema version and migration hook** from the first commit, so a newer image - reads older state. +**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** (open a PR, push, merge, - run an ops step). After it, commit the intent. -- **Every external effect is idempotent against external truth.** Re-running an - interrupted step must detect that the effect already happened and adopt it. For - example, publishing adopts an open PR for the branch instead of opening a second. -- **Intake is handled at most once and answered at least once.** A durable cursor - per external thread (Gitea issue or PR, Vikunja ticket) records what was taken - up; an item being worked on is marked until its response is complete, and - redelivered after a crash. Comments that arrive while the agent can't act on - them are not consumed. +- **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). +- **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.** Every start, before accepting input: -1. Open the store; run pending migrations. -2. **Reconcile pending intents against external truth.** Verified → committed. - Verifiably never happened → aborted. Unverifiable (the service is down) → ask - the operator. -3. **Adopt what external truth shows:** a published PR for a milestone that was - still executing, an approval posted while the agent was down. -4. **Resume waiting points as they were:** a pending question is asked again, an - open proposal is announced again, watches on external threads resume from their - cursors. -5. **Interrupted work resumes only on evidence.** - - *A delegation* resumes automatically when the cut is clean (last event a - completed result, branch at the expected commit, caps unbreached, external - truth matching the log). Otherwise ask. - - *An ops step sequence* continues only when the log names exactly which step - was in flight and its expected outcome checks out against the live system, - and the approval hasn't expired (O2). Otherwise ask. - - *Anything else* is reported to the operator with what is known ("restarted - mid-milestone 2, branch at commit abc123"), with the options to continue, - re-plan or abandon. Never a blind resume. +**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.** The recovery rules are checked by a formal model of the loop -(`specs/`, Quint): interleavings of messages, comments, outcomes and crashes at any -point. Each counterexample it finds is reproduced against the real code before it -is fixed. +**Verification:** these rules are checked by a formal model (`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 in front of everything.** Each agent has exactly one LLM destination: -LiteLLM, which fans out to the local inference host and to frontier vendors. It -holds every vendor key (the agent holds one LiteLLM token), enforces budget caps -per virtual key, logs centrally, and routes fallbacks. Adding a vendor never -touches an agent. +**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. -**One client interface:** `complete(events, tools, model) → events`, over the -canonical log. Delegations use LiteLLM's passthrough route with a per-vendor -renderer, so vendor features aren't lost where long sessions live; quick calls use -the normalised route. +**Roles** 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. -**Roles.** Models are named by role and resolved in configuration: +**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). -```yaml -roles: - driver: local/ # drives the loop: conversation, plans, implementation - helper: # delegations: takes over a scoped sub-task - expert: # ask_expert: one question, no tools - classifier: # the action classifier (L4) -``` +**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. -The resolved mapping is **pinned when a work item opens** and recorded in its log, -so a configuration change can't split a work item across two models. +**`ask_expert`:** one stateless question, text answer, with a per-item budget so a +confused driver can't circle. -**Capability profiles.** Each model in configuration carries a short profile, -filled in from benchmark results: its tools, window, what it does reliably and -where it is weak. Prompts that brief one role about another use the profile, never -a model name or a vague "weaker model". +**Small models** get reduced tool schemas and a tool-call contract the loop +enforces itself where the server can't. -**Escalation.** The driver calls a frontier helper: -- **when it chooses to** — its prompt encourages asking when unsure, and treats - asking as normal; -- **on observable triggers** — repeated failed actions, a context that won't fit, - hitting turn or time caps. - -**Delegation.** A frontier model takes over a scoped sub-task with tools, works -autonomously, and hands back. -- **One harness, not one per vendor.** Vendor agent SDKs would bring back the - lock-in the design avoids. -- **Scoped by the work item's scope** (L3). A delegation can't widen what the work - item was agreed to do; its actions pass through the same three layers (P4). -- **Branch at delegation start;** abandoning it discards the branch. -- **Hard caps** on turns, tokens and wall-clock time. -- **Checkpointed as it goes,** so it can resume after a crash (L8). -- **Handback is a note written for the driver:** what changed, what is now true, - what to do next. That note plus the resulting files is the whole interface back. - -**`ask_expert`.** One stateless question with a text answer, for things needing no -repository access. A per-work-item budget stops a confused driver circling. - -**Local-model accommodations.** -- **Tool-call enforcement:** `tool_choice` is requested only of models whose server - declares it can enforce it; otherwise the loop enforces the contract itself (an - explicit `finish`, prose treated as a violation). -- **A reduced tool schema** for roles on small models: fewer tools, flatter - arguments. - -**Evaluation capture.** Delegations, `ask_expert` calls and classifier verdicts -are recorded as input/output pairs from the start, with outcomes marked. Changing -a model is then a configuration change and a re-run, not a matter of benchmark -chatter. +**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 that outlives work items: world models, agent memory (L6), and -operator-written material such as procedures. It is a folder of Markdown files on -the agent host (X1), in the format Obsidian reads (frontmatter, `[[links]]`, -tags), so the operator browses it in Obsidian with links, backlinks and the graph. +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. -**One vault, one writer per folder.** -- **Agent memory lives under `memory/`,** one folder per agent. Other folders hold - shared material (world models, the operator's notes); their names and layout are - left to grow with need. -- **Every folder has exactly one writer,** an agent or the operator, so concurrent - writes never happen. -- **Mounts enforce it** (a hard wall, P4): each agent's container gets the folders it - writes read-write, the shared folders it may read read-only, and nothing else. No - agent can read another's memory. `nisse-dev` gets no infrastructure material, - which would be a map of exactly what its builds are kept from (D7). -- **Links point from memory to shared notes, never the other way,** so the shared - part reads cleanly on its own. -- **Writes are atomic** (write, then rename), so a reader never sees half a note. -- **Access is through control tools** (L13): the harness reads and writes the mounted - folders. No tool VM ever sees the vault. -- **Provenance is in the note itself:** the harness records the source, work item - and time of each write (L6), whatever else is kept. -- **History is optional:** the harness may keep the vault as a git repository on the - agent host, committing each write with the agent and work item that made it. It - shows what an edit replaced; recovery doesn't depend on it (the host's snapshots - cover that, X4). - -**The operator's copy.** Syncthing keeps a copy on the operator's devices, with the -direction set per folder: -- **folders an agent writes,** memory included, are send-only from the host: the - operator reads them, and a local edit never flows back; -- **folders the operator writes** are receive-only on the host. - -So conflicts can't arise. Obsidian's own settings folder (and `.git`, if kept) are -not synced. - -**Never an instruction channel, never an approval channel.** Work is never read -out of the vault, and an agent never writes there to make another agent act. -Coordination between agents goes through the operator, Matrix, or a ticket (L1). - -**Corrections go through conversation.** The operator tells the agent, which -records the correction verbatim (L6). As a break-glass, the operator can stop the -agent and edit its folder on the host, then restart. - -**Retrieval is structural first.** Procedures are bound to entries in ops policy -(O2) by path and read whenever a plan uses that entry; search (over plain files) -exists in addition, never instead. - -**Failures.** A failed write never blocks work (retry, or record and reconcile). A -missing bound procedure stops planning that action; other failed reads proceed and -log the gap. +- **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 tickets (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.** No marker reliably -separates data from instructions. The design limits what a steered model can do -rather than trying to stop it being steered. +**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. -Untrusted content: issue, comment and ticket text from non-operators, repository -contents, dependencies, web pages, alert payloads, log lines. - -- **Hard walls hold against a steered model** (P4): nothing a model says can grant - a permission, widen an approved scope, or reach a credential that isn't mounted. -- **Approval messages are rendered by code** from what will run, never from a - model's description. -- **The classifier never sees untrusted content** (L4). -- **Instruction channels are operator-scoped:** Matrix from allowlisted - identities, comments from allowlisted Gitea and Vikunja accounts. Everything - else is data. -- **Intake is parsed into structured fields in code** where the source allows it, - so less free text reaches a model, and what does is labelled by source. -- **Some injection is accepted because it can only reorder work:** an alert title - can bias which ready item is chosen next; the switch notice makes it visible. -- **Each agent is checked against the lethal trifecta** — private data, untrusted - content, and a way to send data out. Damage needs all three. `nisse-dev` holds - little private data, and the build VMs where untrusted content meets command - execution hold no credential beyond their project's (D7); `nisse-ops` has all +- **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 of secrets,** held on the agent host (X1) and - mounted only into that agent's container. No agent can read another's. They are - never baked into images. -- **An agent may give its set structure.** `nisse-dev` namespaces it by project - (D7); `nisse-ops` may do the same by site. The framework only needs names. -- **The harness hands secrets out per operation.** Which secrets a command or tool - call receives, and how (environment or stdin, never a command line), is decided - by code and configuration, never by a model. +- **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. A test that prints its environment must not put a - key into a frontier request. -- **MCP server instances** (X3) each have their own minimally scoped credentials, - given only to that instance. -- **Only credentials go in secret stores;** every other setting lives in reviewable - configuration files. + 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 (`svc-nisse-dev`, -`svc-nisse-ops`) in a tightly scoped Authelia group, never the operator's account. -Headless agents get a single-factor exception scoped by source address, group and -target. +**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 external log stack, -not kept in containers. Every durable state change emits exactly one line, written -by the state store right after the commit, with a fixed envelope (time, agent, -event, work item, cause). Every classifier verdict, escalation and external effect -is logged. **Logs are diagnostic, never load-bearing:** nothing reads them to -derive state. +**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 -Tool calls come in three kinds, and the framework allows no others. - | Kind | Examples | Where it runs | |---|---|---| -| **Control tools** | `ask_human`, `ask_expert`, `start_milestone`, `finish_milestone`, reading and writing notes (L10) | In the harness. They act only on the agent's own state, conversation and records | -| **Execution tools** | Running a command; reading, writing and listing files | On a remote host, over SSH, through `execute(host, …)` | -| **MCP tools** | Proxmox and UniFi APIs (X3) | In MCP server containers beside the agent | +| **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 in the framework, so the agent host never runs a - command a model chose. File tools are SSH operations too. -- **The harness binds the host,** not the model. For `nisse-dev` it comes from the - work item: that project's build VM (D7). Where an agent must let the model choose - (`nisse-ops` running a command on its tool VM or on a device), the model picks - from the hosts the harness allows for that work item; any other host is refused - before a connection is made. -- **MCP tools are the one exception,** made deliberately: their servers run fixed - code against an API, and the model chooses only arguments. They pass through the - action classes (P3) and the classifier (L4) like everything else, and their output - is redacted (L12). -- **Tests use SSH too:** a throwaway `sshd` container stands in for a tool VM, so the - code path under test is the one used in production. -- **The action classifier is not a tool.** It is a model call made by the harness - and executes nothing, so it runs on the agent host. + 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` container, so the tested path is + the production path. --- @@ -789,517 +540,346 @@ Designs and builds software projects, producing reviewed changes in Gitea. ### D1. How it works -A dev work item is a task on one repository: a bug, a feature, a new project. -Work arrives through Gitea issues or conversation, and leaves as reviewed PRs. +Work arrives as Gitea issues or conversation, and leaves as reviewed PRs. -**Intake: an issue assigned to `dev-agent`.** Assignment says whose job it is. -- **The operator's issue becomes the work item's record;** the agent doesn't open a - second one. Only issues filed or assigned by allowlisted operators are acted on - (L11). When someone else wrote the issue, the operator's assignment is the - instruction and the issue's text stays data. -- **Work that starts in conversation gets a record too.** When a conversation - turns into work on an existing repository, the agent opens an issue for it, - assigned to `dev-agent` and linked to the work item. Intake ignores issues - already linked to a work item, so a record is never picked up twice. (This is the - one case where the agent assigns itself, and only for work the operator has - already agreed to.) -- **Unassigned issues are backlog,** never work. The agent can discuss them when - asked. -- **An issue with open Gitea dependencies is blocked** (L1) until they close. -- **On assignment the agent acknowledges** ("queued myproject#14, 1 ahead"), and the - item waits its turn (L1). +**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:** the agent opens an issue, + assigned to itself and linked to the item. Intake ignores issues already linked, + so nothing is picked up twice. 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 Matrix thread (L5): what it takes the -request to mean, how it fits the architecture, and its first step. Then: -- **An issue labelled `bug`** proceeds after a short grace period (as in L1), unless - something below says to talk first. During the grace period the item is waiting - on the operator, so the agent may work on something else; when it expires the - item is ready. -- **Anything else** (a feature, a change, a question) waits for the operator's - go-ahead after the restatement: `go`, or a plain-language yes, which the agent - announces it is acting on (L5). The conversation is where the design is agreed. -- **Talk first when:** the issue is labelled `discuss`; the agent finds the report - unclear (it asks its questions instead of restating); or it can't reproduce a - bug. It never fixes a bug it couldn't see. -- **The agent never classifies an issue itself.** It may suggest ("this reads like - a bug; say *go* and I'll start"); labels and the operator decide. A missing label - costs at most a wait, never unwanted work. +**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, and open a PR that - closes the issue. -- **A feature:** agree the design in conversation, write a build plan if the work - is longer than about one PR (D2), then work through it as milestones (D4). -- **A new project:** begins as a conversation, however vague. If it leads to a - project, the agent says what it needs provisioned: a repository, its protection - and the agent's access (D4), a build VM, and the project's configuration. The - operator provisions these (later `nisse-ops`, X4). The agent then opens a tracking - issue in the new repository ("implement the build plan", a record as above), and - the architecture lands in one or more PRs, followed by the build plan's - milestones. -- **Progress** on a tracking issue is posted as it happens: facts by the harness - (milestone started, PR opened or merged), prose by the agent when there's - something worth saying. +- **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:** a conversation, however vague. If it becomes a project, the + agent lists what it needs provisioned (a repository with protection and its own + write access, a build VM, the project's configuration), which the operator + provides (later `nisse-ops`, O6). It then opens a tracking issue ("implement the + build plan") as the record, and the architecture lands in one or more PRs, then + the plan's milestones. +- **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. -**Finishing.** The final PR closes the issue on merge; the agent closes the work -item, after checking what is left open (D5). +**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. -**Filing issues from conversation** (a later addition, not needed at first). In any -conversation, the agent may file an issue against any project where `dev-agent` is -a collaborator, when the operator asks or agrees to its suggestion. -- **Never self-assigned:** a filed issue is backlog until the operator assigns it, - so the agent can't create work for itself. (Records of agreed work, above, are - the only issues it assigns to itself.) -- **Duplicates first:** it searches the project's open issues and offers to add to - an existing one instead. -- **Provenance:** the issue says it was filed from a conversation, when, and with - whom, quoting what prompted it. +### D2. Documents -### D2. Documents are the workflow +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. -The project's own documents live in its repository and carry its design forward: - -- **`ARCHITECTURE.md`** — what the project is, how it fits together, and why. The - durable "what and why". Created with a new project, and kept current by the agent: - a change to the design updates it. -- **Build plans** — how a piece of work gets done, as a sequence of milestones, - under `docs/plans/` or similar. The agent's working documents: how it stays - organised across context windows and sessions, and its own sanity check. - -**Neither is a gate.** They are reviewed the way all work is, in PRs. -- **Architecture changes** go in the PR with the code when small; the agent may - raise a larger one first. An issue labelled `design` asks for the design to be - agreed in conversation before any code. Without the label it is the agent's +**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. -- **Build plans are written for work longer than about one PR,** committed with the - work, so every PR shows the current plan and how it changed. The first PR of a - multi-milestone piece of work includes 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, and the operator can ask to discuss one at any time. - -**A build plan specifies; it doesn't implement.** Each milestone states its goal, -the interfaces and behaviour it adds or changes, edge cases, and **tests as -acceptance criteria**. It includes code only where the code *is* the clearest -specification (a tricky algorithm, an exact format). It never manages branches -(D4). How much detail it needs is informed by the driver's capability profile -(L9). It may note where the agent means to check in with the operator, and which -milestones aren't expected to change code. - -**Progress is recorded in the plan itself:** the agent marks milestones done as -part of each PR. The framework never parses a plan (D4). +- **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. -- **Kept between work items,** so installed toolchains and package caches stay - warm, and isolated from every other project's. -- **Collected when idle.** A build VM whose project has been idle long enough may be - destroyed, and is re-created when work on the project resumes. -- **Re-created from a template** plus the toolchains recorded for the project (see - below). Work pushed to Gitea survives; a commit not yet pushed may be lost when a - VM is wiped, and that is accepted. +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 from the repository, and check out a clean tree at the latest base branch. -- Anything left in a previous workspace (uncommitted changes, local branches, - unpushed commits) is **set aside**, never silently discarded or carried over, - and reported to the operator. -- Build artifacts and caches in the workspace never survive into a new task. +**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 milestones of one work item share that workspace; each starts from the default -branch after the previous milestone's PR has merged (D4). +**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). -**Toolchain preflight.** When the workspace is prepared, the agent determines what -the project needs (from its files, e.g. `Cargo.toml`, `pyproject.toml`, -`package.json`, or a declaration in the repo) and checks it is available. A gap is -installed **before any planning**. The environment's toolchain versions become -facts in the transcript (L6), so plans match reality. - -**The agent installs what it needs on the project's build VM.** The VM is -disposable and is the boundary (D7), so installing packages and toolchains there -is class 2 (P3): a contained side effect. Every install is recorded as a fact (L6) -and in the project's scope memory, so the VM can be rebuilt; toolchains that keep -being needed are baked into the template (X4). Installing on any other host stays -class 3. - -**Repairing the environment is in scope.** Within a task, the agent may clean -caches and build artifacts or reinstall project dependencies to fix a broken -build, and reports what it did. It doesn't stop and wait for permission to delete -a corrupt `.pyc`. +**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 class 2, +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 class 3. ### D4. Git, branches and PRs -- **The harness owns branches.** Plans and models never create or switch - branches. - - **The workspace is never on the default branch:** when a work item starts, the - harness puts it on a branch of its own. - - **Milestones are started and finished by control tools** (L13): - `start_milestone(name)` makes the harness cut the milestone's branch; +- **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; the name is the model's, and the - harness doesn't check it against the plan. - - **Mistakes are made harmless,** not trusted away: finishing without a start - cuts the branch then; starting while another milestone is in progress is - refused; work done outside a milestone is still on a harness branch. -- **Wait for the merge between milestones.** The next milestone starts only after - the previous milestone's PR has merged, read from Gitea; until then the work item - waits on the operator and the agent works on something else (L1). Stacked PRs - are a possible later addition (Q). - - **A PR closed without merging** makes the item ready with that fact; the agent - asks the operator what next (revise, re-plan or abandon) rather than waiting - for a merge that won't come. - - **A review comment** on the PR makes the item ready, so the review loop (D5) - runs even while the item is parked. -- **Parking commits and pushes the item's branch,** so another work item on the - same project starts from a fresh checkout (D3) and the parked one resumes from - its branch. -- **Only the milestone's own changes are published:** what the agent changed, - never build artifacts (standard excludes in the clone, and committing only what - the work produced). Anything left out is reported. -- **"Changes to publish" are counted from where the milestone started,** never - from a possibly stale remote branch. -- **Pushes are harness operations on the build VM.** The harness runs `git push` - there, handing the push key over stdin to a short-lived `ssh-agent` for that one - push (L12); the model's commands never receive it. Nothing is synced through the - agent host: Gitea is the only copy that has to survive. -- **Pull requests are opened from the agent's container** with its Gitea API token, - which never reaches a build VM. -- **Publishing is idempotent** (L8): an open PR for the branch is adopted and - updated, never duplicated. -- **A milestone that completes without changes is done, not failed,** when it - wasn't expected to change code. The agent reports its result and moves on. An - unexpected "no changes" is raised with the operator. -- **The agent never owns or administers a repository.** Whoever creates a repo can - turn its protection off, so repositories are created and protected by the operator - (later `nisse-ops`), and `dev-agent` is a collaborator with write access. -- **The project's default branch is protected,** and this is verified, not - assumed: the agent's own token must be refused when pushing to it, and when - merging without whatever approval the trust dial requires (L3). A test asserts - both, so relaxing protection fails loudly. + 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. +- **Parking pushes the item's branch,** so another item on the same project starts + fresh and this one resumes from its branch. +- **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 key 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 -- **The review loop:** operator comments on a PR (the conversation and inline - line comments) are instructions; the agent responds with commits to the same PR - and replies in the thread where the comment was made. -- **Intake is exactly as in L8:** cursors, at-most-once taking, at-least-once - answering, redelivery after a crash. -- **Merging follows the trust dial** (L3). Today the operator merges. As confidence - grows, the dial may let the agent merge its own PRs once checks pass and the - required approvals are recorded; the branch protection must then require - an operator approval the agent's account can't satisfy, so the merge rule is - enforced by Gitea, not by the agent. -- **Finishing a work item:** the last PR closes its issue on merge ("Closes #N"; - earlier milestone PRs say "Part of #N"). When no merge closes it (abandoned, or a - task that changed no code), the agent closes the issue with a note. Either way it - first checks what is left open. +- **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. +- **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. `nisse-dev`'s action classes +### D6. Action classes | Class | Examples | |---|---| -| 1 | Reading the repo, running tests and builds in the workspace or a test container, querying Gitea | -| 2 (in scope) | Editing the workspace, committing to milestone branches, pushing them, opening and updating PRs, commenting on its issues and PRs, filing issues the operator asked for or agreed to (D1), cleaning caches, installing project dependencies, packages and toolchains on the project's build VM | -| 3 | Merging to the default branch (unless the trust dial allows), deleting remote branches or others' work, changing repository settings, anything outside the work item's repo or its project's build VM | - -Its scope source is the assigned issue and what was agreed in its conversation -(L3); `ARCHITECTURE.md` and the build plan inform it but don't widen it. +| 1 | Reading the repository, running builds and tests on the build VM, querying Gitea | +| 2 (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) | +| 3 | 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** runs as a container on the agent host (X1), holding its state, its -secrets and its Gitea API token. It reaches LiteLLM, Gitea, Matrix, and SSH to its -build VMs, and nothing else. It never runs a command a model chose (L13). +**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 the work happens: -- **A real Docker daemon,** because projects build and run containers. The VM is - the trust boundary for unattended command execution. -- **Sealed:** egress allows package sources (language registries and OS package - mirrors) and Gitea's git endpoint; there is no ingress except SSH from the agent - host. Denial covers RFC1918 (except those specific addresses), the agent host, - and the tailnet's `100.64.0.0/10`, so a build can't reach the NAS, Proxmox, the - gateways or the agent's state. -- **A project may add egress its tests need,** for example LiteLLM and a Matrix test - homeserver for the `nisse` project itself. Additions are declared in the - project's configuration and reviewed like any allowlist (X2). -- **No Tailscale** on build VMs; it would route around the firewall. -- **On their own VLAN,** with test services on predictable ports (e.g. - 18000–18099), so opening access later is a firewall change. -- **Test containers on an internal Docker network with no route out.** -- **A browser** (a shared Playwright tool, headless) **reaches only the internal - test network,** to exercise web apps the agent built and reproduce bugs. No SSO. +**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 whatever + the project 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** (L12), e.g. `nisse/litellm-key`, -`nisse/gitea-test-token`. A build VM's commands receive only their own project's -secrets, and each secret's configuration says whether the model's commands may see -it (a test credential the project's tests need) or only harness operations use it -(the push key). Anything the model's commands receive must be scoped for testing, -never able to touch production. +**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 key). Anything the model's +commands can see must be scoped for testing, never production. -**Provisioning.** Build VMs are created and destroyed by Terraform from the -template. Eventually `nisse-dev` asks `nisse-ops` for them, as an agent-to-agent -request (L1); until then the operator provisions them. +**Provisioning:** build VMs come from a template via Terraform; eventually at +`nisse-dev`'s request to `nisse-ops` (O5), until then by the operator. --- ## Part O — `nisse-ops` -Operates and maintains infrastructure across both sites, driven by tickets and -duties, with stricter controls because live changes have no review step before -they take effect. +Operates infrastructure across both sites, driven by tickets and duties, with +stricter controls because live changes have no review step before they take effect. ### O1. How it differs | Axis | `nisse-dev` | `nisse-ops` | |---|---|---| -| Undo | discard a branch, revert a snapshot | often none (`zfs destroy`, a firewall lockout) | -| Scope source | the assigned issue and its conversation | approved step sequences (O2) | -| Default for class 3 | discuss | discuss, and exact-step approval | -| Work shape | milestones → PRs | short reactive tasks and scheduled checks → live changes | -| Driver of work | issues assigned in Gitea, conversation in Matrix | Vikunja tickets, alerts, duties, the operator | -| Placement | sealed build VMs, own VLAN | Tailscale, credentials, broad reach | +| Undo | discard a branch | often none (`zfs destroy`, a firewall lockout) | +| Scope | the assigned issue and its conversation | approved step sequences (O2) | +| Class 3 | discuss | discuss, and exact-step approval | +| Work shape | milestones → PRs | short reactive tasks → live changes | +| Intake | Gitea issues, Matrix | Vikunja tickets, alerts, duties, Matrix | +| Reach | sealed build VMs | Tailscale, credentials, both sites | -The two are separate deployments over the same library; one instance playing both -roles would collapse the isolation. +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 first,** and approval covers -exactly the steps written. This is `nisse-ops`' scope source for the classifier -(L4) and a hard wall in its own right. +**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 instances are mounted by default, so looking around needs no - plan. -- **A write plan is a list of literal steps,** each naming the tool and target, the - full arguments (a config change carries its exact diff), an optional - **precondition** checked against the live system just before the step, an optional - **expected outcome** checked just after, and a retry allowance (default one). - Prose may explain the plan; it is never scope. -- **The approval message is rendered by code from the steps,** flagging at the top - anything irreversible, anything touching a protected target, and anything that - takes down the agent host (which runs every agent, `nisse-ops` included). -- **Execution is matched against the steps.** Read-only calls are always allowed. - An elevated call runs only if it matches the next unfinished step exactly - (arguments normalised against the tool's schema). Anything else halts and asks: - a call not in the plan, changed arguments, steps out of order, a completed step - run again. A failed precondition or expected outcome halts without retry. -- **Approvals expire.** A plan not started within the expiry window must be approved - again. -- **Policy is small, conservative, and data:** a versioned file declaring each MCP - instance's privilege, protected targets (the NAS, gateways, the agent host and - tool VMs), and each elevated tool's target argument. Anything unclassified is - treated as elevated and protected. Server-declared tool hints can make a tool - stricter, never looser. A model may propose entries for a new server as a PR; no - classification happens at run time. + 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, to add when real friction shows the shape:** bounded matching (any call -within an approved target and action, for reversible tools, one tool at a time), -and standing approvals (a plan template with a trigger, rate limit and expiry, for -a routine fix approved by hand often enough). +**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. `nisse-ops`' action classes +### O3. Action classes | Class | Examples | |---|---| -| 1 | Metrics, logs, status, read-only MCP calls, reading tickets and the world model | -| 2 (in scope) | Approved steps with a known undo; updating its own world model and tickets | -| 3 | Every other infrastructure write; browser actions on admin UIs (each confirmed, since a click isn't a stable literal); **anything on network gear**, the most conservatively handled action in the system, since a bad change can sever the path used to fix it | +| 1 | Metrics, logs, status, read-only MCP calls, tickets, the world model | +| 2 (in scope) | Approved steps with a known undo; its own world model and tickets | +| 3 | 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** (X3) for Proxmox and UniFi at both sites, one instance per - service, site and privilege: the primary path to infrastructure. -- **Browser** (a shared Playwright tool) for admin UIs where no API exists; real APIs - are preferred. For network gear it is a fallback only. -- **SSO** as `svc-nisse-ops`, whose blast radius equals its Authelia scope. -- **Event intake** as structured JSON (directly or via Alertmanager), never through - a Matrix bridge. The agent posts curated updates to `#ops`. +- **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. Tickets and duties -- **A Vikunja ticket is a work item's external record** (durable, with status and - history); **Matrix is the live conversation about it.** -- **Scheduled checks and alert intake are duties** (L1). Tickets they generate are - deduplicated findings, and like any auto-generated ticket can only lead to a plan - the operator approves. -- **Flow:** ticket filed → agent picks it up → discuss over Matrix → status updates → - closed with the result. -- **Vikunja comments are an approval source** for plans on their own tickets - (`approve rev-N`). Live prompts stay in Matrix, with outcomes written back. -- **Action-triggering tickets are operator-scoped:** a ticket is an instruction - channel into an agent that changes infrastructure. -- **Requests from other agents** (L1), such as `nisse-dev` asking for a build VM, - arrive as tickets and are picked up like duty findings: the agent may look and - propose, but any write needs a step sequence the operator approves (O2). Until - standing approvals exist, the operator approves each provisioning request. +- **A Vikunja ticket is a work item's record;** Matrix is the live conversation. + Flow: filed → picked up → discussed → status updates → closed with the result. +- **Tickets that trigger action are operator-scoped.** Tickets from duties (findings) + and from other agents' requests (L1, e.g. `nisse-dev` asking for a build VM) may + be picked up and planned, but any write needs a step sequence the operator + approves. Until standing approvals exist, that means every provisioning request. +- **Vikunja comments approve plans on their own ticket** (`approve rev-N`). ### O6. Deployment -- **A container on the agent host** (X1), with its MCP servers beside it (X3). -- **Its own tool VM,** long-lived, for network-management tools and the commands it - runs (L13). It holds Tailscale and the reach those commands need at both sites; - like any tool VM it can be rebuilt from its template. -- **Its egress allowlist** includes the Proxmox and UniFi endpoints at both sites, - for its MCP servers. -- **Accepts provisioning requests** from other agents (L1): creating and - destroying build VMs for `nisse-dev` through Terraform, under its own rules (O2). -- **Updated in place,** with restarts gated on an idle flag (X4). +- **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 model (all agents) +## Part X — Deployment (all agents) ### X1. The agent host and tool VMs -**One agent host,** a stable, long-lived VM. -- **Each agent runs on it as a container,** with its own volume (state, transcripts, - memory), its own secrets (L12) and its own network. No agent can reach another's - state, credentials or processes. -- **Its MCP servers run beside it** as containers (X3). -- **It never runs a command a model chose** (L13). That is what makes it safe for - agents to share it: nothing on it executes untrusted input. +**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 agent has the tool VMs its work needs: -- `nisse-dev`: one build VM per project, collected when idle and re-created on - demand (D3, D7); -- `nisse-ops`: its own tool VM, for network-management tools (O6); +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. -Tool VMs are **disposable** (P5): each is built from a template plus recorded -installs, and nothing on one has to survive. +**Per-agent attribution:** everything an agent holds is issued to its own identity, +so every upstream action traces to one agent. -**Per-agent attribution:** everything an agent holds is issued to its own -identity, so every upstream action traces to one agent. +### X2. Egress -### X2. Egress policy - -- **Default-deny allowlists,** declared as versioned artifacts next to the agent's - definition, each entry justified by the component that needs it. -- **For each agent container,** the union of what the agent and its MCP servers - reach, enforced by its Docker network and the agent host's firewall. The UniFi - firewall sees the agent host as one address, so these allowlists rest on the - host; that is acceptable because nothing on the host executes untrusted input. -- **For each tool VM,** enforced at the VM, its Docker network and the UniFi - firewall on its VLAN. Network-layer enforcement survives compromise of the VM, - which matters here because this is where untrusted input meets command execution. -- **Tool VMs never reach the agent host,** except as replies on the SSH sessions it - opens. -- **The tailnet is `100.64.0.0/10`,** not RFC1918; deny it explicitly wherever it - isn't needed. +- **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,** as containers beside it on the agent host. A shared - process would mean a shared credential, losing attribution in the upstream API's - own audit log. Duplication is nearly free for stateless servers. -- **The one exception to "execution happens on tool VMs"** (L13): a server runs - fixed code against an API, and the model supplies only arguments. -- **One instance per (service, site, privilege).** The site is part of the server's - identity, so a wrong-site action needs a tool that isn't mounted, not just a wrong - argument. Read-only and elevated instances have separate credentials, each from - its own machine account. -- **Per-task tool profiles** mount only the servers a work item's scope needs. -- **Rejected: search-then-dispatch meta-tools** over large generated catalogues: - extra round trips for a local driver, no discovery of capabilities not already - sought, opaque arguments, and a dynamic tool list that defeats prompt caching. +- **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. Containers, images, VMs and restarts +### X4. Images, VMs and restarts -- **Stateless images, named volumes** for state, on the agent host. -- **Delivery:** Gitea Actions → Gitea's OCI registry → Watchtower pulls. -- **Gate the restart, not the pull:** an idle flag; Watchtower never restarts an - agent mid-work. Unplanned interruptions are handled by recovery (L8). -- **Third-party MCP server images are digest-pinned** and updated deliberately, - never by Watchtower: they hold root-equivalent credentials. -- **Tool VMs come from templates,** provisioned by Terraform (`bpg/proxmox` and - cloud-init). Installs that keep recurring are baked into the template. -- **Survive reboots:** VMs and Docker start on boot, containers restart unless - stopped, and the agent host's state dataset has ZFS snapshots (scheduled, and on - each approval). Tool VMs are not snapshotted; they are rebuilt. +- **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 (llama- -swap on an RTX 4090 host), Vikunja, Vaultwarden, LLDAP with Authelia, and Syncthing -for the operator's copy of the knowledge vault (L10). Each agent gets its own -account, scoped token and room in each service it uses; nothing is shared between -agents. +All self-hosted: Matrix (Synapse), Gitea, LiteLLM, the local inference host, +Vikunja, 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 action classifier's model and placement.** A cheap frontier model, or the - local model with a different prompt; how its verdicts are calibrated against - operator overrides. -- **Recognising questions.** An explicit marker, or classifying free text; either - way a misread must default to asking (L5). -- **How far the local driver goes.** Which kinds of work it handles well and where - a frontier driver is needed, measured on real tasks, not assumed (P7). -- **One local model for every agent.** The inference host keeps one model resident; - different models per agent would reload weights on every switch. Preferred: one - model good enough for every local role. Fallback: agents take turns holding the - host. Measured before deciding. -- **Vision.** Needed for 3D design work (render and look) and video-file - management; not for ordinary code. Tools returning images, image content in the - log and renderers, and image tokens in the context budget. Measured when that work - starts. -- **More than one active work item per agent.** Park-and-switch first; concurrency - costs per-item serialisation, a scheduler for the shared inference host, and - per-item intent ownership. Build VMs per project (D3) already separate workspaces - across projects, and threads (L5) already separate conversations. -- **When a work item must have an external record.** Dev work on a repository and - ticket-driven ops work already do (D1, O5); whether pure conversations must. -- **Fallback browser access to network gear:** standing, or handed off per task. -- **Relocating stateful MCP servers** off the agent host, one at a time, if one - ever justifies losing per-agent attribution. -- **Where secrets come from.** Each agent's set lives on the agent host (L12); whether - it is filled by hand, synced from Vaultwarden, or written by Terraform. -- **How `nisse-ops`' MCP servers reach the second site** from the agent host - (Tailscale on the host, with other agents' networks denied the tailnet, or a - subnet route), since Tailscale on the host serves every container on it. -- **Stacked PRs.** Starting the next milestone before the previous PR merges, - with the harness rebasing and retargeting. Considered after experience with - waiting for merges (D4). -- **How long idle before a build VM is collected,** and whether collection needs the - operator's word. - ---- - -## Appendix — What changed from the Build Specification - -For readers who knew the former `docs/Architecture-Build-Spec.md` (in git -history): - -- **Threat model and control model (P2–P4).** Checks are built around mistakes, - injection and crashes, in three layers with an action classifier, rather than a - rigid state machine gating every phase. -- **The agent loop (L2)** replaces the per-agent state machines (old A4, B1). What - stays fixed is only what authority and durability need. -- **Approvals as scoped permissions (L3),** with scope from the assigned issue and - its conversation, labels to steer, and an operator-set trust dial. Revision - binding is kept and extended (task-wide revisions, stamping on arrival). -- **The local model drives everything (P7, L9);** frontier models are helpers. The - old "planning always escalates" phase policy is dropped. -- **Context and memory (L6)** is new: transcript scoping, attribution, orchestrator - facts, scope memory, agent memory, and how memory is written and used. -- **The dev workflow (D1, D2):** issues assigned to the agent as intake, labels to - steer, documents kept current through PRs, and build plans as the agent's working - documents rather than gates; a fresh workspace per task and toolchain preflight - (D3), and git hygiene learned from the live runs (D4). -- **Conversation (L5)** is a first-class requirement, not a command interpreter. -- **The knowledge vault (L10)** replaces Outline: Markdown on the agent host, shared - between agents by mounts, synced to the operator by Syncthing. Revised in PR #90. -- **Deployment (X1, L13),** revised in PR #90: one agent host with each agent as a - container, and disposable tool VMs (a build VM per project for `nisse-dev`) where - every model-chosen command runs over SSH, replacing a VM per agent. -- **Kept, largely as they were:** the work model, durability and reconciliation, - LiteLLM and the canonical log, delegation, prompt injection, - identity, and `nisse-ops`' approved step sequences. -- **Moved to the Development Plan** (`docs/Development-Plan.md`): build order, - supporting-service status, and interim arrangements. +- **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. +- **Whether pure conversations need an external record** (dev work on a repository + and ops tickets already have one). +- **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. diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md index 5d11810..3a6b4ea 100644 --- a/docs/Development-Plan.md +++ b/docs/Development-Plan.md @@ -309,3 +309,57 @@ All 19 open issues, as of 2026-09-30: | #79 ending projects and milestones | **Superseded** | M2.5 | | #80 build artifacts in PRs | Milestone | M1.4 | | #81 no-change milestones | **Superseded** | M2.5 | + +## 6. Implementation notes + +Mechanism detail moved out of `docs/Architecture.md` in PR #90, which keeps only +decisions and their reasons. Keyed by the architecture section each note +implements; a milestone touching that section uses the note, and changes it here +if the implementation differs. + +- **L1 switching:** the grace period before switching away from an item waiting + on the operator defaults to 10 minutes. When the driver chooses the next ready + item and the choice fails or is invalid, fall back to the oldest. +- **L3 revision stamping:** in Matrix, a reply is stamped with the revision whose + message had been sent when it arrived; on a Gitea issue or a Vikunja ticket, + with the latest proposal comment older than the reply. +- **L5 transport:** the `Transport` interface is send, ask and receive. Intake + backoff is exponential, capped at a few minutes. +- **L6 fitting order:** first shorten old tool results to their first and last + lines, then drop the oldest events with a note saying so; never the system + prompt, the current plan or the most recent turns. View labels: + `[orchestrator]`, `[operator, Matrix]`, `[operator, comment on PR #N]`, + `[helper report]`, `[executor]` narration. +- **L7 rendering:** content is stored as bytes or local paths and uploaded or + inlined at render time, never as provider handles. Token budgeting and + prompt-cache breakpoints are render-time concerns, never stored. Compaction + summaries are first-class events so a model change never inherits opaquely + compacted history. A reasoning "session" is a delegation, including one resumed + after a crash. +- **L8 state store:** SQLite in WAL mode, each change committed and fsynced before + the loop continues. A delegation's cut is clean when its last event is a + completed result, its branch is at the expected commit, its caps are unbreached + and external truth matches the log. "Anything else" offers continue, re-plan or + abandon. +- **L9 local models:** request `tool_choice` only of models whose server declares + it can enforce it; otherwise the loop enforces the contract (an explicit + `finish`, prose treated as a violation). Briefings use the capability profile, + never a model name or "weaker model". +- **L10 vault:** atomic writes are write-then-rename. Obsidian's settings folder + (and `.git`, if history is kept) are not synced. A failed write is retried, or + recorded and reconciled later. +- **L12:** secrets reach a command by environment or stdin. Headless agents get an + Authelia single-factor exception scoped by source address, group and target. + Logs are shipped to the host journal, not kept in containers. +- **D3 preflight:** a project's needs are read from its files (`Cargo.toml`, + `pyproject.toml`, `package.json`) or a declaration in the repository. +- **D4 publishing:** standard excludes are configured in each clone, and only what + the work produced is committed. A push hands the key over stdin to a + short-lived `ssh-agent` on the build VM, for that one push. +- **D7:** test services use ports 18000–18099, so opening access later is a + firewall change. The inference host is behind LiteLLM, so it is not a second + egress exception. +- **X4:** the agent host's state dataset is ZFS, snapshotted on a schedule and on + each approval. Tool VM templates use Terraform's `bpg/proxmox` provider with + cloud-init. Containers restart unless stopped. +- **X5:** the local inference host is llama-swap on an RTX 4090 machine. -- 2.54.0 From 4ca8e1f550fb9720c566e1b012fdee8422b730e5 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:52:18 +0000 Subject: [PATCH 09/12] Drop Vikunja: Gitea issues are the one tracker for every agent Ops work lives in one infrastructure repository for both sites, with site labels; intake, labels and dependencies work as for nisse-dev, and agent-to-agent requests are cross-repo issues. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 66 ++++++++++++++++++++++------------------ docs/Development-Plan.md | 4 +-- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 3b8d743..5aaea3d 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -138,15 +138,16 @@ Everything in this part is used by every agent. | **Scope** | What a work item operates on; context that outlives it | A repository | A site and service | - **Sources of work items:** duties, the operator (always), and other agents. A - request from another agent arrives through the receiving agent's normal intake - and carries no authority of its own: the receiver applies its own rules. + 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. -- **An external record is linked when there is one** (a Gitea issue, a Vikunja - ticket); external truth wins on divergence (L8). +- **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. @@ -159,10 +160,11 @@ Everything in this part is used by every agent. | **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, or another - agent's ticket. 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. +- **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 @@ -226,8 +228,8 @@ agreement: a restatement awaiting a go-ahead (D1), a design (D2), a step sequenc model's description (L11). - **Approvals expire** where the world can change underneath them (O2). -**Approval sources:** Matrix (L5), Gitea comments (D1) and Vikunja comments (O5), -each checked against its own operator allowlist. The first valid approval wins and +**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 @@ -293,8 +295,8 @@ 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. -**Other channels:** Gitea and Vikunja carry work and records, read as structured -JSON (never through a chat bridge). Comments from allowlisted operators are +**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 @@ -457,7 +459,7 @@ browses it in Obsidian. 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 tickets (L1). The operator points to a note in conversation + 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. @@ -710,13 +712,14 @@ see it or only harness operations use it (the push key). 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` (O5), until then by the operator. +`nisse-dev`'s request to `nisse-ops` (an issue in the infrastructure repository, +O5), until then by the operator. --- ## Part O — `nisse-ops` -Operates infrastructure across both sites, driven by tickets and duties, with +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 @@ -727,7 +730,7 @@ stricter controls because live changes have no review step before they take effe | Scope | the assigned issue and its conversation | approved step sequences (O2) | | Class 3 | discuss | discuss, and exact-step approval | | Work shape | milestones → PRs | short reactive tasks → live changes | -| Intake | Gitea issues, Matrix | Vikunja tickets, alerts, duties, Matrix | +| 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 @@ -766,8 +769,8 @@ template with a trigger, rate limit and expiry, for a routine fix). | Class | Examples | |---|---| -| 1 | Metrics, logs, status, read-only MCP calls, tickets, the world model | -| 2 (in scope) | Approved steps with a known undo; its own world model and tickets | +| 1 | Metrics, logs, status, read-only MCP calls, issues, the world model | +| 2 (in scope) | Approved steps with a known undo; its own world model; commenting on and filing issues in the infrastructure repository | | 3 | 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 @@ -778,15 +781,20 @@ template with a trigger, rate limit and expiry, for a routine fix). - **Event intake** as structured JSON (directly or via Alertmanager); curated updates go to `#ops`. -### O5. Tickets and duties +### O5. Issues and duties -- **A Vikunja ticket is a work item's record;** Matrix is the live conversation. - Flow: filed → picked up → discussed → status updates → closed with the result. -- **Tickets that trigger action are operator-scoped.** Tickets from duties (findings) - and from other agents' requests (L1, e.g. `nisse-dev` asking for a build VM) may - be picked up and planned, but any write needs a step sequence the operator - approves. Until standing approvals exist, that means every provisioning request. -- **Vikunja comments approve plans on their own ticket** (`approve rev-N`). +- **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 @@ -855,7 +863,7 @@ so every upstream action traces to one agent. ### X5. Supporting services All self-hosted: Matrix (Synapse), Gitea, LiteLLM, the local inference host, -Vikunja, Vaultwarden, LLDAP with Authelia, and Syncthing for the operator's copy of +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. @@ -872,8 +880,8 @@ shared between agents. - **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. -- **Whether pure conversations need an external record** (dev work on a repository - and ops tickets already have one). +- **Whether pure conversations need an external record** (work on a repository + already has its issue). - **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. diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md index 3a6b4ea..e104578 100644 --- a/docs/Development-Plan.md +++ b/docs/Development-Plan.md @@ -321,8 +321,8 @@ if the implementation differs. on the operator defaults to 10 minutes. When the driver chooses the next ready item and the choice fails or is invalid, fall back to the oldest. - **L3 revision stamping:** in Matrix, a reply is stamped with the revision whose - message had been sent when it arrived; on a Gitea issue or a Vikunja ticket, - with the latest proposal comment older than the reply. + message had been sent when it arrived; on a Gitea issue, with the latest + proposal comment older than the reply. - **L5 transport:** the `Transport` interface is send, ask and receive. Intake backoff is exponential, capped at a few minutes. - **L6 fitting order:** first shorten old tool results to their first and last -- 2.54.0 From 5839091d659202cd03c668ceb9611ee62df1bb7a Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:53:38 +0000 Subject: [PATCH 10/12] Define projects as configuration entries (D8); note the agent host's recovery Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 5aaea3d..bf84891 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -576,7 +576,7 @@ mean, how it fits, its first step. Labels steer what happens next: than about one PR (D2), work through it as milestones (D4). - **A new project:** a conversation, however vague. If it becomes a project, the agent lists what it needs provisioned (a repository with protection and its own - write access, a build VM, the project's configuration), which the operator + write access, a build VM, and its project entry, D8), which the operator provides (later `nisse-ops`, O6). It then opens a tracking issue ("implement the build plan") as the record, and the architecture lands in one or more PRs, then the plan's milestones. @@ -697,10 +697,11 @@ 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 whatever - the project 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. +- **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 @@ -715,6 +716,23 @@ commands can see must be scoped for testing, never production. `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` @@ -891,3 +909,7 @@ shared between agents. - **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. +- **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. -- 2.54.0 From 8c38444b88864de7f91e450647fa4e00b3cc0090 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 19:24:54 +0000 Subject: [PATCH 11/12] Close five gaps the loop model found - abandon closes the item's open PRs - a PR the operator closed is an answer, never reopened by a retry - a review fix for a PR that merged meanwhile goes out as a new PR - redoing a milestone after a closed PR gets a new branch and PR - a record issue carries its work item's ID from creation Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index bf84891..ef5d77a 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -199,8 +199,8 @@ again after a restart), permissions (L3), and work in progress as intents (L8). 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 with a - note and aborting its unresolved intents. +- **`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 @@ -379,7 +379,8 @@ migration hook. - **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). + 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. @@ -397,8 +398,9 @@ migration hook. 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 are checked by a formal model (`specs/`, Quint) over -interleavings of messages, comments, outcomes and crashes. Each counterexample is +**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 @@ -551,8 +553,10 @@ Work arrives as Gitea issues or conversation, and leaves as reviewed PRs. - **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:** the agent opens an issue, - assigned to itself and linked to the item. Intake ignores issues already linked, - so nothing is picked up twice. This is the only time the agent assigns itself. + 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 @@ -654,7 +658,8 @@ Installing on any other host is class 3. 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. + 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 branch,** so another item on the same project starts fresh and this one resumes from its branch. - **Publish only the work:** what the agent changed, counted from where the @@ -677,6 +682,9 @@ Installing on any other host is class 3. - **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. -- 2.54.0 From 2187c24d0de0facae26ca06bf37a443d332d6912 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 19:33:17 +0000 Subject: [PATCH 12/12] Address review comments on the architecture Name the action classes harmless, contained and dangerous instead of numbering them; say what escalation doesn't depend on; say when a work item's Matrix thread starts and how threads route commands; add open questions for the state store's engine, ops issuing credentials, and releases. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 68 ++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index ef5d77a..7e4628a 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -66,14 +66,16 @@ 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 | |---|---|---|---| -| **1. No side effects** | Changes nothing outside a disposable sandbox | Reading, querying, searching, running tests in a throwaway environment | Always allowed | -| **2. 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 | -| **3. 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 | +| **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 -class 2 in one work item and class 3 in another. **Move actions down the classes +contained in one work item and dangerous in another. **Make actions less dangerous wherever it is cheap** (P5). ### P4. Three layers of control @@ -82,7 +84,7 @@ wherever it is cheap** (P5). 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:** class-1 actions, and class-2 actions a static rule +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. @@ -117,9 +119,10 @@ naming a milestone (D4). 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 never depends on self-assessment alone.** The driver is prompted to - ask for help when unsure, and observable triggers (repeated failures, caps) - escalate too, since small models are poorly calibrated. +- **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. @@ -210,7 +213,7 @@ again after a restart), permissions (L3), and work in progress as intents (L8). - **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 class-3 action not named by an approved step +- **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. @@ -261,12 +264,19 @@ executes nothing, so it runs on the agent host. 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. -- A reply in a thread goes to that item, even while it is parked. -- Commands in a thread apply to that item. At top level, `stop` applies to the - active item and `status` to the agent; any other top-level message starts a new - conversation in its own thread, and the agent offers to continue an existing item - if it clearly belongs there. +**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. - **Any conversation about work is a work item from its first message,** however vague ("I'm having trouble with X…"). It may end with a project, an issue, or just an answer. @@ -638,10 +648,10 @@ the project needs and installs any gap **before any planning**; toolchain versio 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 class 2, +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 class 3. +Installing on any other host is dangerous. ### D4. Git, branches and PRs @@ -693,9 +703,9 @@ Installing on any other host is class 3. | Class | Examples | |---|---| -| 1 | Reading the repository, running builds and tests on the build VM, querying Gitea | -| 2 (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) | -| 3 | 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 | +| 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 @@ -754,7 +764,7 @@ stricter controls because live changes have no review step before they take effe |---|---|---| | Undo | discard a branch | often none (`zfs destroy`, a firewall lockout) | | Scope | the assigned issue and its conversation | approved step sequences (O2) | -| Class 3 | discuss | discuss, and exact-step approval | +| 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 | @@ -795,9 +805,9 @@ template with a trigger, rate limit and expiry, for a routine fix). | Class | Examples | |---|---| -| 1 | Metrics, logs, status, read-only MCP calls, issues, the world model | -| 2 (in scope) | Approved steps with a known undo; its own world model; commenting on and filing issues in the infrastructure repository | -| 3 | 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 | +| 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 @@ -917,6 +927,16 @@ shared between agents. - **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 -- 2.54.0