Add Nisse build specification
Seeds nisse-core as the home for system-level design documentation. The spec covers the shared library (Part A), both agents (Parts B and C), and the supporting services, build order, and open decisions. nisse-core holds this because the document describes the shared design rather than either agent's implementation; the repo gains code later, at the extraction step in the build order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
be04c28542
1 file changed
+802
@@ -0,0 +1,802 @@
|
|||||||
|
# Nisse — Build Specification
|
||||||
|
|
||||||
|
A build-oriented spec for two self-hosted AI agents sharing a common library. This
|
||||||
|
document states **requirements per component** so major pieces can be broken out and
|
||||||
|
built independently. It is deliberately lighter on the supporting services (Matrix,
|
||||||
|
Gitea, Vikunja, Outline, LiteLLM) and heaviest on the **agent library** and the
|
||||||
|
**two agents**, which is where the real design work lives.
|
||||||
|
|
||||||
|
For deeper rationale behind any decision here, see the companion `Architecture.md`.
|
||||||
|
|
||||||
|
**Document history.** The Tiered Inference amendment has been folded into this
|
||||||
|
document and no longer exists separately: its A-series components appear as A10–A16,
|
||||||
|
its network and credential revisions in A5 and B3, and its revised build order as
|
||||||
|
Part G. The inference-hardware question it raised has since been resolved in
|
||||||
|
principle (Part H: a 4090-class host).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Naming
|
||||||
|
|
||||||
|
The project is **Nisse**, after the Nordic farm spirit that keeps the place running
|
||||||
|
overnight and expects to be treated well in return. Three artifacts:
|
||||||
|
|
||||||
|
| Artifact | Name | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| Shared library | `nisse-core` | The spine both agents sit on (Part A) |
|
||||||
|
| Dev Agent | `nisse-dev` | Plans and implements software projects (Part B) |
|
||||||
|
| Sysadmin Agent | `nisse-ops` | Operates infrastructure (Part C) |
|
||||||
|
|
||||||
|
Throughout this document, **Dev Agent** and **Sysadmin Agent** name the *roles* in
|
||||||
|
conceptual discussion; `nisse-dev` and `nisse-ops` name the *artifacts* — repos,
|
||||||
|
container images, VM hostnames, Matrix identities, and service accounts. Use the
|
||||||
|
same string across all of those: the VM, the container, the Caddy host, the Matrix
|
||||||
|
account, and the Bitwarden machine account for a given agent all share one name, so
|
||||||
|
that grepping the infrastructure for `nisse-ops` finds everything.
|
||||||
|
|
||||||
|
Published under a personal GitHub account rather than an org; `github.com/nisse` is
|
||||||
|
squatted and GitHub does not reclaim inactive usernames. The repo names carry the
|
||||||
|
identity. `nisse-core` is free on PyPI if packages are ever published.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0.1 What We're Building
|
||||||
|
|
||||||
|
Two agents:
|
||||||
|
|
||||||
|
1. **Dev Agent** — `nisse-dev` (build first) — plans and implements software
|
||||||
|
projects, produces Gitea PRs for human review, reacts to review comments and
|
||||||
|
commits.
|
||||||
|
2. **Sysadmin Agent** — `nisse-ops` (follow-on) — operates and maintains
|
||||||
|
infrastructure, driven by tickets, gated on high-consequence actions.
|
||||||
|
|
||||||
|
Both are thin applications over a shared **`nisse-core`** library. The library is
|
||||||
|
*extracted from the Dev Agent after it works* — not designed up front for two
|
||||||
|
consumers that don't exist yet. Build the Dev Agent as if the library were internal,
|
||||||
|
then lift the seams that proved real.
|
||||||
|
|
||||||
|
Whether the three artifacts live in one repository or three is **an open decision**
|
||||||
|
(Part H). It changes what the extraction step in the build order concretely means.
|
||||||
|
|
||||||
|
### Guiding principles (apply to everything below)
|
||||||
|
|
||||||
|
- **Human-gated autonomy.** Agents work independently once told to start, but never
|
||||||
|
cross key boundaries (start implementation, apply infrastructure change) without
|
||||||
|
explicit sign-off.
|
||||||
|
- **No per-command approval.** Within an approved unit of work, execution is
|
||||||
|
unattended. Control lives at the *boundaries*, not per action.
|
||||||
|
- **Isolation matched to blast radius.** Dev Agent is disposable and sealed; Sysadmin
|
||||||
|
Agent is trusted with live-infrastructure reach. They never share a running
|
||||||
|
instance.
|
||||||
|
- **Stateless image, stateful volumes.** Containers are replaceable; durable state
|
||||||
|
lives on named volumes and must survive both redeploys and unexpected reboots.
|
||||||
|
- **Structured channels for machines, prose channels for humans.** Never route
|
||||||
|
machine event intake through human-facing formatters (chat bridges).
|
||||||
|
- **Own the stack.** Everything self-hostable stays inside the network where practical.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART A — THE AGENT LIBRARY (`nisse-core`)
|
||||||
|
|
||||||
|
The shared spine. Every component here is consumed by both agents. This is the part
|
||||||
|
to get right.
|
||||||
|
|
||||||
|
### A1. Transport
|
||||||
|
|
||||||
|
**Purpose:** all human⇄agent conversation — commands in, questions/approvals/updates
|
||||||
|
out.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- A `Transport` interface the orchestrator depends on; **no platform SDK imported
|
||||||
|
directly** anywhere else. Methods (at least): `send_message`,
|
||||||
|
`send_approval_request`, `ask_question`, `on_message` (inbound callback/stream).
|
||||||
|
- **`MatrixTransport`** is the first and primary implementation. Self-hosted
|
||||||
|
homeserver (Synapse or Conduit) keeps the control plane in-network. Must support
|
||||||
|
threaded conversations (for plan discussion) and per-agent identities.
|
||||||
|
- Interface must make adding `TelegramTransport` / `SlackTransport` later a matter of
|
||||||
|
one new class + config, no orchestrator changes.
|
||||||
|
- Both agents run as **distinct identities** (`@nisse-dev`, `@nisse-ops`) in separate
|
||||||
|
rooms on the same homeserver.
|
||||||
|
|
||||||
|
Explicitly *not* required: rich message formatting, custom clients. Existing Matrix
|
||||||
|
clients (Element) are the UI.
|
||||||
|
|
||||||
|
**Matrix is strictly a UI channel.** Although the homeserver is self-hosted and
|
||||||
|
therefore not "remote," room history must not become load-bearing context. The
|
||||||
|
canonical log (A10) is the sole source of truth.
|
||||||
|
|
||||||
|
### A2. State Store and Durability *(critical path — build first)*
|
||||||
|
|
||||||
|
**Purpose:** durable, crash-safe persistence of everything that must outlive a
|
||||||
|
process restart. This is what makes the whole system safe to redeploy and reboot; it
|
||||||
|
blocks everything else.
|
||||||
|
|
||||||
|
Must persist: active project/task, per-project state-machine position, approvals
|
||||||
|
granted, pending questions, session continuity, (dev) git clone locations.
|
||||||
|
|
||||||
|
Disposable (never on the state volume): agent code, build artifacts/test containers,
|
||||||
|
shipped-out logs.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- **Crash-atomic writes.** SQLite in **WAL mode** (default choice) or atomic
|
||||||
|
file writes (`write-temp → fsync → rename`). A power cut mid-write must never
|
||||||
|
produce torn state.
|
||||||
|
- **Checkpoint after every state transition**, with `fsync`. No "flush later" —
|
||||||
|
in-memory state that hasn't hit disk is assumed lost.
|
||||||
|
- **Durable intent log.** Before a unit of work starts, record intent, so a restart
|
||||||
|
knows what was in flight.
|
||||||
|
- **Idempotent transitions reconciled against external truth.** On any operation with
|
||||||
|
an external side effect (open PR, push branch), restart must detect the effect
|
||||||
|
already happened (query Gitea/git) rather than repeat it.
|
||||||
|
- **`schema_version`** field from the first commit, with a migration hook, so a newer
|
||||||
|
image can read older state.
|
||||||
|
|
||||||
|
### A3. Boot-Time Reconciliation
|
||||||
|
|
||||||
|
**Purpose:** turn an ambiguous crash into a safe, known state.
|
||||||
|
|
||||||
|
Every startup (clean or post-crash), before accepting operator commands:
|
||||||
|
1. Open state store (WAL recovery is automatic).
|
||||||
|
2. Run any pending schema migration.
|
||||||
|
3. Read intent log; look for a transition that began but never committed.
|
||||||
|
4. Reconcile against external truth (Gitea/git for the dev agent; relevant services
|
||||||
|
for the sysadmin agent).
|
||||||
|
5. **Interrupted mid-delegation (A13) — apply the deterministic resume rule.**
|
||||||
|
**Auto-resume** the delegation — re-render its transcript from the canonical log
|
||||||
|
and continue the loop, posting a notice (not a question) to transport — when the
|
||||||
|
cut is clean: the last durable event is a completed `tool_result` or assistant
|
||||||
|
turn, the delegation branch sits at the expected commit, no cap is breached, and
|
||||||
|
external truth matches the log. **Gate to `AWAIT_INPUT`** when the last event is a
|
||||||
|
`tool_call` with no recorded result whose effect cannot be verified by inspection
|
||||||
|
(tree diff for edits, Gitea query for pushes), or on any branch/log mismatch. The
|
||||||
|
judgment is an observable check, never model self-assessment — the same reasoning
|
||||||
|
as A12.
|
||||||
|
6. Any other interruption → move to **`AWAIT_INPUT`** and post to transport
|
||||||
|
(e.g. "restarted mid-milestone-2, branch at commit abc123, continue or re-plan?").
|
||||||
|
Never blind-resume.
|
||||||
|
7. Otherwise resume the last committed state.
|
||||||
|
|
||||||
|
### A4. Control Loop, State Machine, and Gating Primitives
|
||||||
|
|
||||||
|
**Purpose:** the generic engine that drives an agent through gated work. Each agent
|
||||||
|
supplies its own concrete states; the primitives are shared.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- A **state machine** framework: named states, transitions, and human gates. Each
|
||||||
|
gate blocks until an explicit operator action arrives via transport.
|
||||||
|
- **Gating is structural, not advisory.** The mechanism (used by both agents):
|
||||||
|
- Expose a **`submit_plan(type, content)`** tool to the model. Calling it posts the
|
||||||
|
plan for approval and **ends the agent's turn.** The agent cannot self-advance.
|
||||||
|
- Expose an **`ask_human(question)`** tool — non-terminal: pause, post question,
|
||||||
|
block until answered, resume.
|
||||||
|
- **Do not** expose any "proceed" / "start" tool. Advancement past a gate happens
|
||||||
|
only when the orchestrator receives operator approval and hands over the next
|
||||||
|
task. The agent has no path to approve itself.
|
||||||
|
- **Approval sources.** A gate waits on an approval event from any registered
|
||||||
|
**`ApprovalSource`** — the Matrix transport (A1), the Gitea comment poller (B2), or
|
||||||
|
the Vikunja comment stream (C4). First valid approval wins and is consumed; the
|
||||||
|
gate records which channel satisfied it, and the orchestrator echoes the outcome to
|
||||||
|
the other channels so the records converge. Each source authenticates the approver
|
||||||
|
against a per-channel operator allowlist (Matrix MXID, Gitea username, Vikunja
|
||||||
|
username).
|
||||||
|
- **Approvals bind to a plan revision, never to "the current gate."** Every submitted
|
||||||
|
plan revision carries a monotonically-increasing tag (`rev-N`). An approval naming
|
||||||
|
a stale revision is rejected with a notice, never applied to a newer one — this is
|
||||||
|
what makes multi-channel approval race-free.
|
||||||
|
- **One consistent approval UI across all channels.** Approval is always the typed
|
||||||
|
word `approve`, attached to a specific revision: in Matrix, a reply to the plan
|
||||||
|
message (the reply relation carries the binding; a bare `approve` in the room binds
|
||||||
|
to the latest revision only when unambiguous, otherwise the agent asks); in Gitea
|
||||||
|
and Vikunja, a comment `approve rev-N`, with the tag printed in the plan comment
|
||||||
|
for copy-paste. Any other reply/comment is `reject <feedback>`. Reactions/emoji are
|
||||||
|
never an approval path. Approvals are consumed at first sight; later edits to an
|
||||||
|
approval comment are ignored and logged.
|
||||||
|
- **Autonomy inside an approved unit of work:** all tool/command use auto-approved
|
||||||
|
(Claude Agent SDK `bypassPermissions` or an allow-all `canUseTool`). Escalation to
|
||||||
|
the human happens *only* via `ask_human`.
|
||||||
|
- The loop must survive restart mid-work (relies on A2/A3).
|
||||||
|
|
||||||
|
### A5. LLM Access *(tiered)*
|
||||||
|
|
||||||
|
**Purpose:** reach both a locally-hosted model and frontier models through one
|
||||||
|
interface, with the frontier vendor swappable by config.
|
||||||
|
|
||||||
|
A locally-hosted model drives the control loop and performs mechanical work; frontier
|
||||||
|
models are engaged for work that exceeds local capability. Two requirements drive the
|
||||||
|
tiering:
|
||||||
|
|
||||||
|
1. **No remotely-held state.** Conversation and task state lives on the agent VM. No
|
||||||
|
provider holds anything the system depends on to resume.
|
||||||
|
2. **Vendor fluidity.** The frontier tier should be swappable — Anthropic, OpenAI,
|
||||||
|
Google, or a vendor that does not exist yet — as a configuration change, not a
|
||||||
|
refactor.
|
||||||
|
|
||||||
|
These two are only in tension if portability is required *within* a session. Making
|
||||||
|
frontier engagements **ephemeral and self-contained** (A13) allows vendor-specific
|
||||||
|
session data to be used freely inside a delegation while keeping the durable record
|
||||||
|
vendor-neutral.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- **One client interface**, `complete(canonical_log, tools, model) -> canonical_events`,
|
||||||
|
present at the **first real LLM call** — not introduced as a later cutover. The same
|
||||||
|
reasoning that produced the `Transport` abstraction.
|
||||||
|
- **LiteLLM in front of everything, from the start.** The agent VM gets exactly one
|
||||||
|
LLM destination; LiteLLM fans out to the local inference host and to frontier
|
||||||
|
vendors behind it. This also resolves the RFC1918 conflict in B3 — a LAN-resident
|
||||||
|
inference host would otherwise need a second hole punched in the egress allowlist.
|
||||||
|
- **Passthrough for the vendor driving a given session** (`/v1/messages` for
|
||||||
|
Anthropic), so vendor-specific features are not lost on the path where they matter —
|
||||||
|
delegations (A13) always take this path. **Normalized routing for everything else**;
|
||||||
|
the canonical log (A10) reconciles the difference.
|
||||||
|
- **Fallback chains configured at LiteLLM.** Rate limits and outages differ per
|
||||||
|
vendor; a frontier failure should not kill a task.
|
||||||
|
- **No vendor credentials on the agent VM.** LiteLLM holds every key, sourced from
|
||||||
|
Bitwarden machine accounts (A7); the agent holds one LiteLLM token. Adding a vendor
|
||||||
|
does not touch the agent VM, and a compromised agent leaks no provider keys.
|
||||||
|
- Config from env/`.env`, **nothing hardcoded**: base URL, auth token, and the role
|
||||||
|
mapping (A11). Changing any of it is an env change + restart, nothing more.
|
||||||
|
- **Budget caps and spend alerts live on the LiteLLM virtual key**, not in agent code.
|
||||||
|
|
||||||
|
### A6. Browser Tool
|
||||||
|
|
||||||
|
**Purpose:** drive web UIs. Shared implementation; each agent points it at a
|
||||||
|
different network with different trust.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- **Playwright** (headless Chromium), exposed to the model as tool actions:
|
||||||
|
navigate, click, type, read DOM, screenshot. Screenshot + accessibility-tree loop
|
||||||
|
so the model can operate UIs it wasn't trained on.
|
||||||
|
- Runs **inside the agent's VM**. Network reachability is set by the agent's
|
||||||
|
deployment, not the tool.
|
||||||
|
- Must support authenticated sessions (for the sysadmin agent's admin UIs) while
|
||||||
|
being usable without auth (dev agent's test apps).
|
||||||
|
|
||||||
|
### A7. Secrets Client
|
||||||
|
|
||||||
|
**Purpose:** fetch credentials at runtime instead of baking them into images.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Client for **Bitwarden/Vaultwarden** via **machine account / Secrets Manager** (or
|
||||||
|
`bw` CLI + API key). Not the personal vault.
|
||||||
|
- Reads only from a **dedicated, minimally-scoped machine account** per agent.
|
||||||
|
- A single **bootstrap token** (the machine-account credential) is the one secret
|
||||||
|
that lives on the box: tight file perms, never in image or repo.
|
||||||
|
- (Deferred, not first cut: dynamic/short-lived secrets via Vault/OpenBao. Leave the
|
||||||
|
interface able to grow toward this; don't build it yet.)
|
||||||
|
|
||||||
|
### A8. SSO Identity
|
||||||
|
|
||||||
|
**Purpose:** each agent authenticates as itself.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Each agent is a dedicated **LLDAP** principal (`svc-nisse-dev`, `svc-nisse-ops`) in
|
||||||
|
a tightly-scoped **Authelia** group. Never the operator's account.
|
||||||
|
- Support an **unattended-auth path**: because interactive MFA is impossible for a
|
||||||
|
headless agent, allow a narrow **IP-scoped, group-scoped, target-scoped**
|
||||||
|
single-factor exception. Keep it specific on all three axes.
|
||||||
|
|
||||||
|
### A9. Logging
|
||||||
|
|
||||||
|
**Purpose:** observability without bloating state.
|
||||||
|
|
||||||
|
Requirements: structured logs to journald / external log stack, **not** kept
|
||||||
|
in-container. Every gate crossing, tool call, and escalation is logged.
|
||||||
|
|
||||||
|
### A10. Canonical Transcript Schema
|
||||||
|
|
||||||
|
The durable record is a **canonical event log** in SQLite, not a provider-shaped
|
||||||
|
`messages` array. Provider request bodies are rendered at call time from this log.
|
||||||
|
|
||||||
|
| Event | Payload |
|
||||||
|
|---|---|
|
||||||
|
| `user_message` | text, source (Matrix event ID) |
|
||||||
|
| `assistant_text` | text, model that produced it |
|
||||||
|
| `tool_call` | canonical call ID, tool name, arguments |
|
||||||
|
| `tool_result` | canonical call ID, result, success/failure |
|
||||||
|
| `plan_submitted` | plan body, authoring model |
|
||||||
|
| `gate_opened` | gate type, reason |
|
||||||
|
| `delegation_started` | brief, target role, resolved model, caps |
|
||||||
|
| `delegation_completed` | handback note, artifacts, tokens, duration, exit status |
|
||||||
|
| `compaction` | summary, producing model, range of events replaced |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- **Canonical tool-call IDs are minted locally.** Provider IDs are mapped at the
|
||||||
|
boundary and never used as a join key. Providers differ in both ID format and in
|
||||||
|
where tool calls sit structurally (content blocks vs. a separate array on the
|
||||||
|
assistant message).
|
||||||
|
- **No provider-side handles for content.** If a Files API is used, store the bytes or
|
||||||
|
a content-addressed local path; upload or inline at render time. A provider file ID
|
||||||
|
must never be the only reference to something.
|
||||||
|
- **Vendor-opaque reasoning artifacts are stored, tagged with the producing model, and
|
||||||
|
never replayed outside the session that produced them.** This covers Anthropic
|
||||||
|
signed thinking blocks, OpenAI encrypted reasoning items, and Gemini thought
|
||||||
|
signatures. Inside a delegation, replay per that vendor's rules. On delegation exit,
|
||||||
|
the artifacts become archival only. A "session" here is the **delegation**, not the
|
||||||
|
process lifetime: a delegation resumed after a crash (A3) is the same session and
|
||||||
|
may replay its own artifacts.
|
||||||
|
- **Token budgeting is a render-time concern.** Context window and tokenizer are
|
||||||
|
properties of the model being called. Compute budget when building the request, not
|
||||||
|
when writing the log.
|
||||||
|
- **Compaction summaries are first-class log events** with the producing model
|
||||||
|
recorded, so a tier change does not silently inherit opaquely-compacted history.
|
||||||
|
- **Prompt cache breakpoints are placed by the renderer and never stored.** A tier
|
||||||
|
change means a cold cache — a cost event, not a correctness event.
|
||||||
|
|
||||||
|
### A11. Role Configuration
|
||||||
|
|
||||||
|
Model identity is expressed as **roles**, resolved to concrete model strings in config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
roles:
|
||||||
|
executor: local/<model> # drives the loop, mechanical work
|
||||||
|
planner: <frontier> # authors plans
|
||||||
|
delegate: <frontier> # scoped sub-task execution
|
||||||
|
consultant: <frontier> # advisory, no repo access
|
||||||
|
```
|
||||||
|
|
||||||
|
"Best model this week" is a config edit. **The resolved mapping is pinned at task
|
||||||
|
start** and recorded in the log, so a mid-task config change cannot split a task
|
||||||
|
across two planners.
|
||||||
|
|
||||||
|
### A12. Escalation Triggers
|
||||||
|
|
||||||
|
The local model does **not** decide when it needs help. Small models are poorly
|
||||||
|
calibrated about their own competence and will escalate either constantly or never.
|
||||||
|
Triggers are observable from outside the model:
|
||||||
|
|
||||||
|
1. **Phase policy.** Planning and architecture escalate unconditionally. Mechanical
|
||||||
|
edits stay local.
|
||||||
|
2. **Failure count.** Local attempts; tests fail twice; escalate with the failures
|
||||||
|
attached to the brief.
|
||||||
|
|
||||||
|
A model-invoked `ask_expert` (A14) exists in addition to these, never as a
|
||||||
|
replacement, and carries a per-task budget so a confused executor cannot spend twenty
|
||||||
|
consults circling.
|
||||||
|
|
||||||
|
### A13. Delegation
|
||||||
|
|
||||||
|
Escalation is **delegation**: a frontier model receives a scoped sub-task, works
|
||||||
|
autonomously with full tool access, and hands back. It is not a consult, and the local
|
||||||
|
model does not interpret or re-derive the frontier model's reasoning.
|
||||||
|
|
||||||
|
**Harness — one, not one per vendor.** Do not adopt each vendor's agent SDK to obtain
|
||||||
|
their best agentic behavior; that recreates the lock-in this design avoids, relocated.
|
||||||
|
One loop, one cap regime, one handback contract.
|
||||||
|
|
||||||
|
**Normalization lives in the A10 renderer, not on the wire.** Vendors disagree
|
||||||
|
structurally about tool calling (content blocks vs. `tool_calls` arrays, tool-result
|
||||||
|
placement, streaming deltas) and about reasoning artifacts (signed thinking blocks,
|
||||||
|
encrypted reasoning items, thought signatures) — and a normalizing middlebox is lossy
|
||||||
|
exactly where a long-running delegation lives. So:
|
||||||
|
|
||||||
|
- **Delegations run over LiteLLM's passthrough route** (`/v1/messages` for Anthropic,
|
||||||
|
per A5), with the A10 renderer producing the vendor-native request body. The
|
||||||
|
renderer is a per-vendor adapter by definition — it already has to know each wire
|
||||||
|
shape to render the canonical log.
|
||||||
|
- **LiteLLM's normalized route is reserved for calls where fidelity is cheap:**
|
||||||
|
`ask_expert` (A14), fallback chains, and trialing a new vendor before its renderer
|
||||||
|
adapter exists.
|
||||||
|
- **Adding a vendor** therefore means writing one renderer adapter — or provisionally
|
||||||
|
accepting LiteLLM's normalization for that vendor and validating it against the A16
|
||||||
|
eval set before trusting it with delegations. A day-one build needs exactly one
|
||||||
|
adapter: the vendor actually in use.
|
||||||
|
|
||||||
|
**Scoping and authority.** Delegation sits **inside** the plan gate, not around it. A
|
||||||
|
frontier sub-agent that acts freely would bypass `submit_plan` (A4), which is the
|
||||||
|
structural control the design rests on.
|
||||||
|
|
||||||
|
- **Planning is itself a delegation** that produces a plan and stops. Human approves.
|
||||||
|
- **Execution delegations are scoped to a sub-task the approved plan already
|
||||||
|
contains.**
|
||||||
|
|
||||||
|
The frontier model is smart twice; neither time is it authorized to expand the work.
|
||||||
|
|
||||||
|
Because plans may be executed by a materially weaker model, the planner system prompt
|
||||||
|
must require **explicit, low-inference plans** — the executor cannot reconstruct
|
||||||
|
intent.
|
||||||
|
|
||||||
|
**Isolation and caps.**
|
||||||
|
- **Branch at delegation start.** Abort discards the branch. This gives delegations the
|
||||||
|
same atomicity property that boot-time reconciliation (A3) gives interrupted state
|
||||||
|
transitions.
|
||||||
|
- **Hard caps on turns, tokens, and wall clock.**
|
||||||
|
- **Checkpoint delegation events as they arrive.** Every `tool_call` / `tool_result` /
|
||||||
|
assistant turn inside a delegation is durably written to the canonical log when it
|
||||||
|
happens (per A2), never batched at handback. This is what makes a delegation
|
||||||
|
resumable after a crash — the deterministic resume rule lives in A3.
|
||||||
|
- **Defined failure return.** On cap breach or explicit failure, the local loop gates
|
||||||
|
to `AWAIT_INPUT` rather than resuming on top of a half-edited tree.
|
||||||
|
|
||||||
|
**Handback.** The final delegation action is a **structured handback note written for a
|
||||||
|
weaker reader**, not a human-facing summary:
|
||||||
|
|
||||||
|
- what changed
|
||||||
|
- what is now true that was not before
|
||||||
|
- what to do next
|
||||||
|
|
||||||
|
That note plus the resulting file state is the entire interface back to the local loop.
|
||||||
|
The delegation's internal transcript is stored alongside the composite event,
|
||||||
|
vendor-tagged, and never replayed into anything.
|
||||||
|
|
||||||
|
#### Rejected alternative: ask-and-apply
|
||||||
|
|
||||||
|
An alternative was considered in which the local model asks a frontier model for advice
|
||||||
|
and applies the answer itself. **Rejected.**
|
||||||
|
|
||||||
|
Its principal advantage was blast radius — the frontier model never touches the repo,
|
||||||
|
and everything flows through a gated local loop. That was explicitly not a concern
|
||||||
|
here.
|
||||||
|
|
||||||
|
Without it, the comparison is lopsided:
|
||||||
|
|
||||||
|
- Delegation lets the frontier model read the code, run the tests, and see the
|
||||||
|
failures. Ask-and-apply forces it to reason blind from whatever the local model
|
||||||
|
thought to include in the brief, and small models write poor briefs.
|
||||||
|
- The cost advantage is illusory: one delegation that succeeds beats three consult
|
||||||
|
round-trips that do not.
|
||||||
|
- Any variant where the local model *interprets* rather than applies verbatim
|
||||||
|
reintroduces the capability gap downstream of the expensive call.
|
||||||
|
|
||||||
|
The diff-application machinery, verbatim-fidelity rules, and local-model-as-verifier
|
||||||
|
design are all dropped.
|
||||||
|
|
||||||
|
### A14. `ask_expert` (retained, minimal)
|
||||||
|
|
||||||
|
One narrow case survives delegation: **questions requiring no repo access.** "Which of
|
||||||
|
these two approaches is better," "why does this library behave this way," "is this API
|
||||||
|
deprecated."
|
||||||
|
|
||||||
|
- Single stateless call, text answer.
|
||||||
|
- No diff application, no fidelity concerns, no session state.
|
||||||
|
- Reuses the delegation brief format.
|
||||||
|
- Structured output contracted **at the prompt level, not the API level** — vendors
|
||||||
|
differ on JSON mode and schema enforcement. Parse, validate against a local schema,
|
||||||
|
retry once.
|
||||||
|
|
||||||
|
Expected implementation size: small. Do not let it grow into a second escalation
|
||||||
|
subsystem.
|
||||||
|
|
||||||
|
### A15. Local Model Accommodations
|
||||||
|
|
||||||
|
- **Constrained decoding for tool-call JSON.**
|
||||||
|
- **Reduced tool schema for the executor role:** fewer tools, flatter arguments, no
|
||||||
|
deeply nested optionals. Maintain **two tool manifests keyed on role**, rendered at
|
||||||
|
request time like everything else.
|
||||||
|
|
||||||
|
### A16. Delegation Evaluation Capture
|
||||||
|
|
||||||
|
Capture a **consult/delegation eval set** from day one. Each `delegation_started` /
|
||||||
|
`delegation_completed` pair (A10) is already a clean input-output record; the
|
||||||
|
additional cost is marking outcomes as good or bad.
|
||||||
|
|
||||||
|
Without this, "best model this week" is decided by benchmark chatter, and swapping the
|
||||||
|
frontier tier becomes something to avoid because the effect is unmeasurable. With it, a
|
||||||
|
new model is a config change and a re-run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART B — THE DEV AGENT / `nisse-dev` (build first)
|
||||||
|
|
||||||
|
A thin app over `nisse-core`. Plans and implements software projects; produces PRs.
|
||||||
|
|
||||||
|
### B1. Workflow (state machine)
|
||||||
|
|
||||||
|
```
|
||||||
|
IDLE
|
||||||
|
→ [use <project>] clone repo, begin architecture planning
|
||||||
|
PLANNING_ARCH
|
||||||
|
→ agent drafts architecture + implementation plan → submit_plan
|
||||||
|
AWAIT_ARCH_SIGNOFF ← human gate (approve / reject+feedback)
|
||||||
|
→
|
||||||
|
MILESTONE_PLANNING
|
||||||
|
→ agent drafts plan for a specific milestone → submit_plan
|
||||||
|
AWAIT_MILESTONE_SIGNOFF ← human gate
|
||||||
|
→ [start milestone <n>]
|
||||||
|
EXECUTING autonomous; no per-command approval
|
||||||
|
→ may enter AWAIT_INPUT (ask_human) any time it's unsure
|
||||||
|
PR_OPEN
|
||||||
|
→ open PR; react to review comments and commits (review loop)
|
||||||
|
→ on acceptance → next milestone, or project complete
|
||||||
|
```
|
||||||
|
|
||||||
|
### B2. Requirements
|
||||||
|
|
||||||
|
- **One active project at a time.** Operator selects it.
|
||||||
|
- **Operator commands** (over transport): `use <project>`, `approve`,
|
||||||
|
`reject <feedback>`, `start milestone <n>`, `status`, plus free text during
|
||||||
|
planning (forwarded to the agent as answers).
|
||||||
|
- **Plan gating** per A4: architecture plan and each milestone plan require sign-off
|
||||||
|
before the next phase. Enforced structurally. Plans and approvals are externalized
|
||||||
|
to Gitea issue comments (Part H, decided); Matrix and Gitea are both approval
|
||||||
|
channels, with revision binding and the uniform `approve` UI per A4.
|
||||||
|
- **Autonomous execution** within a started milestone: builds, runs, tests without
|
||||||
|
per-command approval; asks questions any time via `ask_human`.
|
||||||
|
- **Gitea integration:**
|
||||||
|
- *Outbound:* `git` + Gitea API to push branches and open PRs, via a dedicated
|
||||||
|
Gitea **service account** with a scoped token. Record any runnable test service's
|
||||||
|
URL/port in the PR description.
|
||||||
|
- *Inbound:* react to PR comments, commits, pushes. **Poll** the Gitea API
|
||||||
|
(15–30s) rather than take webhooks, to keep the VM outbound-only. Intake is
|
||||||
|
structured JSON — never via a chat bridge.
|
||||||
|
- *On Gitea unavailability:* the poller backs off (exponential, capped at a few
|
||||||
|
minutes) and retries indefinitely, logging the outage. Polling is pure intake —
|
||||||
|
an outage delays reactions but never fails a task or forces a state transition.
|
||||||
|
Outbound Gitea operations that fail mid-flight are already covered by the
|
||||||
|
intent log and boot-time reconciliation (A2/A3).
|
||||||
|
- **Browser (A6)** scoped to the **internal test network only** — used to exercise
|
||||||
|
web apps it built and reproduce bugs. **No SSO needed** for this.
|
||||||
|
|
||||||
|
### B3. Deployment & Isolation
|
||||||
|
|
||||||
|
- **Dedicated VM** (Proxmox), not a bare container — the agent builds and runs Docker
|
||||||
|
containers, so it needs its own real Docker daemon. (Docker-in-a-container via
|
||||||
|
socket-mount ≈ host root; via dind = nested-storage pain. The VM boundary is the
|
||||||
|
correct trust boundary for unattended arbitrary command execution.)
|
||||||
|
- **Sealed to start:** egress allow-list (LiteLLM, Gitea, package registries), **no
|
||||||
|
ingress**. Egress denial must include **RFC1918 except the specific LiteLLM/Gitea
|
||||||
|
IPs** — sealed from the internet must also mean no reach to NAS/Proxmox/UDM.
|
||||||
|
- **The LAN-resident inference host is not a second exception.** It sits behind
|
||||||
|
LiteLLM (A5), so the allowlist keeps exactly one LLM destination regardless of how
|
||||||
|
many models or vendors are in play.
|
||||||
|
- **No Tailscale** on this VM (it routes around firewall policy). Manage via Proxmox
|
||||||
|
console / jump host.
|
||||||
|
- **Own VLAN from day one** (even a VLAN of one) and test services on **predictable
|
||||||
|
ports** (e.g. 18000–18099), so opening access later is a firewall edit, not a
|
||||||
|
rebuild. (Sealed for now; may relax later.)
|
||||||
|
- Test containers on a dedicated **internal Docker network with no route out**.
|
||||||
|
- Build-daemon storage on a **separate mount** from the state volume, so
|
||||||
|
`docker system prune` between milestones can't touch agent state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART C — THE SYSADMIN AGENT / `nisse-ops` (follow-on)
|
||||||
|
|
||||||
|
A thin app over the extracted `nisse-core`. Operates infrastructure; driven by
|
||||||
|
tickets; gated on consequences. Built after the Dev Agent works.
|
||||||
|
|
||||||
|
### C1. How it differs (not reducible to config)
|
||||||
|
|
||||||
|
| Axis | Dev Agent | Sysadmin Agent |
|
||||||
|
|---|---|---|
|
||||||
|
| Rollback | revert snapshot / discard branch | none for `zfs destroy` or a firewall lockout |
|
||||||
|
| Trust model | allow-all in sandbox | tiered **allow / deny / confirm** policy |
|
||||||
|
| Work shape | long milestones → a PR | short reactive tasks + scheduled checks → live changes |
|
||||||
|
| Driver | Gitea projects | **Vikunja tickets** |
|
||||||
|
| Placement | sealed, own VLAN, no Tailscale | Tailscale, credentials, broad reach |
|
||||||
|
|
||||||
|
Running both roles in one instance would collapse isolation — build them as separate
|
||||||
|
deployments over the same library.
|
||||||
|
|
||||||
|
### C2. Permission Policy Engine *(the defining addition)*
|
||||||
|
|
||||||
|
Tiered policy, evaluated on every action:
|
||||||
|
- **Read-only telemetry** (metrics, logs, status) — free.
|
||||||
|
- **Reversible service actions** (restart a container) — allowed.
|
||||||
|
- **High-consequence actions** (storage, networking, auth) — **confirm-first** over
|
||||||
|
transport before executing.
|
||||||
|
|
||||||
|
This is the sysadmin analog of the dev agent's plan gate: the human boundary sits at
|
||||||
|
irreversible / high-blast-radius actions.
|
||||||
|
|
||||||
|
Special cases fold into the tiers:
|
||||||
|
- **Browser actions against admin UIs default to confirm-first.** Changes to
|
||||||
|
**network gear (UniFi) are the most conservatively gated action in the system** — a
|
||||||
|
bad remote UI click can sever the path used to fix it.
|
||||||
|
- **Action-triggering tickets are operator-scoped** (C4). Auto-generated tickets
|
||||||
|
(e.g. from a monitoring alert) route through confirm-first, never drive unattended.
|
||||||
|
|
||||||
|
### C3. Interfaces
|
||||||
|
|
||||||
|
- **Browser (A6):** into admin UIs. **Prefer real APIs where they exist** (Proxmox,
|
||||||
|
TrueNAS good; UniFi partial); use the browser only for genuinely UI-only gaps —
|
||||||
|
it's the least stable interface.
|
||||||
|
- **SSO (A8):** logs into admin UIs as `svc-nisse-ops`. This account is a high-value
|
||||||
|
credential; its blast radius equals its Authelia scope, so scope it minimally.
|
||||||
|
- **Secrets (A7):** UI/SSO logins and service API tokens from Bitwarden.
|
||||||
|
- **Event intake:** monitoring feeds the orchestrator as **structured JSON**
|
||||||
|
(directly or via Alertmanager), never through a Matrix bridge. The agent posts
|
||||||
|
curated, state-aware updates to `#ops` ("Gitea 502ing, restarted, back up").
|
||||||
|
|
||||||
|
### C4. Ticket-Driven Control (Vikunja)
|
||||||
|
|
||||||
|
The sysadmin analog of "dev agent driven by Gitea."
|
||||||
|
- **Vikunja ticket = task of record** (durable, has status/queue/history). **Matrix =
|
||||||
|
live conversation about it** (questions, confirm-first prompts, progress).
|
||||||
|
- **Flow:** ticket filed → agent picks up → discuss/confirm over Matrix → update
|
||||||
|
status as it progresses → close with result.
|
||||||
|
- **Mechanics:** Vikunja REST API + webhooks (same two-direction, JSON-only pattern
|
||||||
|
as Gitea); dedicated service account, scoped token from Bitwarden. Verify the
|
||||||
|
installed version's webhooks fire on comment creation; if not, poll comments as
|
||||||
|
B2 polls Gitea.
|
||||||
|
- **Approvals:** Vikunja is an `ApprovalSource` (A4) for plan gates on its own
|
||||||
|
tickets, via `approve rev-N` comments. Live **confirm-first** prompts (C2) stay
|
||||||
|
Matrix-primary — polling latency makes a ticket comment a poor interactive
|
||||||
|
channel — with the outcome written back to the ticket as the record.
|
||||||
|
- **Trust:** a driving ticket is an instruction channel into an infrastructure-
|
||||||
|
changing agent → action-triggering ticket creation is operator-scoped (C2).
|
||||||
|
|
||||||
|
### C5. Deployment & Isolation
|
||||||
|
|
||||||
|
- Longer-lived, **stateful VM** (not disposable).
|
||||||
|
- Has **Tailscale** + scoped credentials to reach infrastructure across both sites.
|
||||||
|
- Updated **in place** via the container pipeline, **restart gated on an idle flag**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART D — NOTES / KNOWLEDGE (Outline)
|
||||||
|
|
||||||
|
Shared knowledge store for both agents (sysadmin-primary). Not a message bus — see
|
||||||
|
note below.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- **Read — runbooks as context.** Procedures written in Outline are fetched before
|
||||||
|
acting, constraining the agent toward *your* procedures over improvised ones.
|
||||||
|
- **Write — durable record.** Agent writes change logs / incident notes / findings
|
||||||
|
back to Outline: searchable, off-box, and an extra externalized record that
|
||||||
|
survives volume loss.
|
||||||
|
- Access via Outline **REST API**, dedicated service account, token from Bitwarden.
|
||||||
|
|
||||||
|
**Deliberately rejected:** using a shared notes store (Outline *or* an Obsidian vault)
|
||||||
|
as an **agent-to-agent message bus**. It's unstructured, race-prone (last-write-wins,
|
||||||
|
no delivery semantics), and a shared read-write store both agents touch would
|
||||||
|
undermine the dev/sysadmin isolation boundary. Agent coordination, if ever needed,
|
||||||
|
goes through the **human**, **Matrix**, or a **Vikunja ticket** handed from one agent
|
||||||
|
to another — or a real message queue if genuine multi-agent async ever arrives.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART E — SUPPORTING SERVICES (installed)
|
||||||
|
|
||||||
|
**Status: all supporting services are installed and running.** What remains for each
|
||||||
|
is per-agent *configuration* (service accounts, scoped tokens, rooms), created as
|
||||||
|
each agent is built — not standing up the service itself.
|
||||||
|
|
||||||
|
- **Matrix homeserver** (Synapse/Conduit): ✅ installed. *Remaining per-agent:* create
|
||||||
|
the two bot identities (`@nisse-dev`, `@nisse-ops`), one room per agent + `#ops`.
|
||||||
|
- **Gitea:** ✅ installed. *Remaining per-agent:* `nisse-dev` service account + scoped
|
||||||
|
token; enable native-JSON webhooks or confirm API polling access.
|
||||||
|
- **LiteLLM proxy:** ✅ installed, and **on the path from the first LLM call** (A5) —
|
||||||
|
it is the agent VM's single LLM destination, providing per-agent virtual keys with
|
||||||
|
budget caps, central logging, vendor swap, fallback routing, and the Anthropic
|
||||||
|
passthrough. *Remaining per-agent:* mint a virtual key; configure routes for the
|
||||||
|
local inference host and each frontier vendor, with keys sourced from Bitwarden.
|
||||||
|
- **Local inference host:** 🔜 **decided in principle, hardware pending** — a
|
||||||
|
4090-class server (Part H). Interim: a Jetson Nano running Ollama validates the
|
||||||
|
plumbing, and the `executor` role maps to a Haiku-class frontier model via LiteLLM
|
||||||
|
until the server arrives. Everything in A5, A12, and A15 assumes the eventual host
|
||||||
|
sits on the LAN behind LiteLLM.
|
||||||
|
- **Vikunja:** ✅ installed. *Remaining (sysadmin):* service account + scoped token;
|
||||||
|
webhook/poll config (Part C4).
|
||||||
|
- **Outline:** ✅ installed. *Remaining per-agent:* service account + API token
|
||||||
|
(Part D).
|
||||||
|
- **Bitwarden/Vaultwarden:** ✅ installed. *Remaining per-agent:* dedicated
|
||||||
|
minimally-scoped machine account + bootstrap token (A7).
|
||||||
|
- **LLDAP + Authelia:** ✅ installed. *Remaining per-agent:* create `svc-nisse-dev` /
|
||||||
|
`svc-nisse-ops` principals, scoped groups, and the narrow unattended-auth exception
|
||||||
|
(A8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART F — DEPLOYMENT MODEL (both agents)
|
||||||
|
|
||||||
|
- **Containers + persistent volumes.** Stateless image; named volumes carry state
|
||||||
|
across redeploys (and allow optional persistence between deployments).
|
||||||
|
- **Image delivery:** Gitea Actions → Gitea OCI registry → Watchtower for the *pull*.
|
||||||
|
- **Gate the restart, not the pull:** an idle/"safe-to-restart" flag; Watchtower must
|
||||||
|
never restart mid-work. *Unplanned* interruptions (reboot/crash) are handled by the
|
||||||
|
durability layers (A2/A3), not by restart timing.
|
||||||
|
- **Survive reboots:** VM + Docker service set to start on boot; `restart:
|
||||||
|
unless-stopped`; ZFS snapshots of the state dataset (scheduled + on each approval).
|
||||||
|
- **Terraform + cloud-init:** deferred. When adopted, `bpg/proxmox` + cloud-init,
|
||||||
|
VM definitions as code, clean `destroy`/`apply`; the dev agent becomes a candidate
|
||||||
|
for immutable rebuilds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART G — BUILD ORDER
|
||||||
|
|
||||||
|
1. **State store + durability (A2/A3)** — critical path; everything sits on it.
|
||||||
|
2. **Transport (A1, Matrix) + control loop/gating (A4)** with a **stub agent** that
|
||||||
|
just echoes plans — get the approval loop feeling right before any real LLM.
|
||||||
|
3. **LiteLLM + LLM client interface (A5).** The interface must exist at the **first
|
||||||
|
real LLM call**, not as a later cutover. Mint a virtual key with a budget cap;
|
||||||
|
verify prompt caching survives the passthrough.
|
||||||
|
4. **Canonical log schema and renderer (A10).** Before any transcript accumulates in a
|
||||||
|
provider-shaped form that would have to be migrated later.
|
||||||
|
5. **Local executor loop (A11/A12/A15)** — role resolution pinned at task start, phase
|
||||||
|
policy, failure-count escalation, reduced tool manifest, constrained decoding.
|
||||||
|
6. **Gitea layer** — polling intake, PR creation, comment/commit handling.
|
||||||
|
7. **Delegation harness + plan-gate integration (A13/A14).** Begin capturing the eval
|
||||||
|
set here (A16); the records are free once the composite events exist.
|
||||||
|
8. **Browser tool (A6)** for `nisse-dev` — test apps on the sealed network.
|
||||||
|
9. First **genuinely unattended milestone** (budget caps + ZFS rollback earn their keep
|
||||||
|
here). Crash-atomic writes and reconcile-on-boot must be in place first.
|
||||||
|
10. **Extract `nisse-core`**; build the **Sysadmin Agent** (`nisse-ops`): policy
|
||||||
|
engine (C2), SSO + scoped browser (A8/A6), secrets (A7), Vikunja control (C4),
|
||||||
|
Outline runbooks (D), scheduled checks, alert intake.
|
||||||
|
- Under a monorepo, extraction is a **package split** within one tree: move code
|
||||||
|
behind a package boundary, no release machinery required.
|
||||||
|
- Under three repos, it is a **repo split plus a version-pinning contract**:
|
||||||
|
`nisse-core` gets tagged releases, and `nisse-dev` pins a version rather than
|
||||||
|
tracking `main`, or every core change becomes a coordinated two-repo bump.
|
||||||
|
This is the step the open repo-layout decision (Part H) actually bites on.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PART H — OPEN DECISIONS
|
||||||
|
|
||||||
|
- **Inference hardware — resolved in principle, no longer blocking.** Target: a
|
||||||
|
**4090-class host (24GB VRAM)** running a **24–32B open model** at 4–5-bit
|
||||||
|
quantization (candidates: Devstral Small 24B, Qwen3-32B / Qwen3-30B-A3B,
|
||||||
|
GPT-OSS-20B). Models of this class are competent at tool calling and mechanical
|
||||||
|
edits, which puts the system in "tiering genuinely load-bearing" territory rather
|
||||||
|
than "local model as dispatcher." Consequences:
|
||||||
|
- **Context, not weights, is the VRAM pressure.** A 32B at 4-bit leaves roughly
|
||||||
|
5GB for KV cache, and agent loops render long contexts. Model size vs. context
|
||||||
|
length vs. KV-cache quantization is an empirical trade — tune it, and the
|
||||||
|
thresholds that depend on local capability (A12 failure count, A14 consult
|
||||||
|
budget, A15 manifest size), at build-order step 5 on the real hardware.
|
||||||
|
- **Interim posture:** until the server exists, the `executor` role points at a
|
||||||
|
cheap frontier model (Haiku-class) through LiteLLM — a config entry under A11,
|
||||||
|
nothing structural, so the build order does not stall. The Jetson Nano validates
|
||||||
|
plumbing (Ollama → LiteLLM → agent) only; nothing learned from models it can run
|
||||||
|
transfers to threshold tuning.
|
||||||
|
- If the 24GB class proves insufficient in practice, additional hardware is the
|
||||||
|
fallback — the role indirection (A11) makes that swap invisible to the agents.
|
||||||
|
|
||||||
|
The rest resolve when reached, not now.
|
||||||
|
|
||||||
|
- **Repository layout:** one monorepo with three packages vs. three separate repos.
|
||||||
|
Three repos match the build order and give each artifact its own CI and release
|
||||||
|
cadence, at the cost of version-pinning `nisse-core` from Gitea and coordinating
|
||||||
|
bumps every time the shared interface moves. A monorepo is less friction while
|
||||||
|
`nisse-core` is still being extracted from a working `nisse-dev` and its seams are
|
||||||
|
still moving. Splitting later is easy; merging later is not. Decide by step 10.
|
||||||
|
- **Sysadmin browser access to network gear:** standing vs. per-task hand-off. The
|
||||||
|
lock-yourself-out risk argues for per-task.
|
||||||
|
- **Policy action taxonomy (C2):** the concrete list of which operations are
|
||||||
|
allow vs. confirm. Best decided with the agent half-built in front of you.
|
||||||
|
- **Approved-plan externalization — decided: Gitea-backed from the start.** Plans are
|
||||||
|
posted as issue comments on the project repo; the approval comment (author,
|
||||||
|
timestamp, revision tag) is the durable off-box record, authenticated by Gitea's
|
||||||
|
own auth. The state store keeps only a pointer ("gate 3 satisfied by comment 8841"),
|
||||||
|
so boot-time reconciliation (A3) can re-derive gate state from Gitea even after
|
||||||
|
state-volume loss — external truth wins (A2). Approval mechanics and revision
|
||||||
|
binding are specified in A4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
nisse-core/ # shared library (extracted after nisse-dev works)
|
||||||
|
transport/ # matrix | telegram | slack (A1)
|
||||||
|
state/ # persistence, reconcile, migration (A2/A3)
|
||||||
|
control/ # loop, state machine, gating, ask_human (A4)
|
||||||
|
llm/ # client interface, role resolution (A5)
|
||||||
|
log/ # canonical event log + renderer (A10)
|
||||||
|
delegation/ # delegation harness, ask_expert (C1/C3)
|
||||||
|
browser/ # Playwright tool (A6)
|
||||||
|
secrets/ # Bitwarden machine-account client (A7)
|
||||||
|
sso/ # LLDAP identity + auth path (A8)
|
||||||
|
logging/ # structured operational logs (A9)
|
||||||
|
|
||||||
|
nisse-dev/ # app: sealed VM (Part B)
|
||||||
|
workflow (arch/milestone gating), Gitea PR layer, allow-all sandbox
|
||||||
|
browser → internal test network only
|
||||||
|
|
||||||
|
nisse-ops/ # app: separate host, Tailscale (Part C)
|
||||||
|
policy engine, tiered confirmation, scheduled checks
|
||||||
|
Vikunja ticket intake, Outline runbooks/records
|
||||||
|
browser → admin UIs (API-first, UI fallback), confirm-first gating
|
||||||
|
|
||||||
|
infra/ # (future) Terraform + cloud-init (Part F)
|
||||||
|
nisse-dev.tf | nisse-ops.tf | templates/
|
||||||
|
```
|
||||||
|
|
||||||
|
Shown as three sibling trees. Under a monorepo they become three packages under one
|
||||||
|
root; under three repos, three checkouts. Either way the module boundaries are the
|
||||||
|
same, which is the point of deciding this late (Part H).
|
||||||
|
|
||||||
|
`log/` carries the canonical event log and its renderer (A10); `delegation/` carries
|
||||||
|
the delegation harness and `ask_expert` (A13/A14).
|
||||||
Reference in new issue
Block a user