With no production instance yet, there is no reason to carry two ways
of configuring the agent. Every environment variable that duplicated a
config key is gone; the environment now supplies only the secrets the
file references by ${…}, plus NISSE_CONFIG and NISSE_ENV to find the
files.
- AgentConfig loads from a file (load, from_file) or a parsed tree
(from_tree, for tests); a missing file is an error naming the example
- The from_env constructors on LiteLLMClient, AnthropicPassthroughClient,
GiteaClient, MatrixConfig and RoleMap are removed, with the env
reading for max output tokens; clients take their settings from config
- The live integration tests share a committed agent config,
tests/integration/nisse-test.yaml, aimed at test resources, and one
conftest that loads it and builds clients. NISSE_LIVE_PLANNER_MODEL
becomes its planner role; NISSE_CAPABILITY_MODELS stays as a per-run
switch
- A missing secret now fails the live tests instead of skipping them
- CI passes only the three tokens
- .env.example is secrets only; spec A5 says config comes from the file
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The thresholds Part H says to tune on the real inference hardware were
hardcoded, so every adjustment would have been a PR. They now live in a
`tuning:` section of a YAML config file, which also replaces flat
environment variables as the home for structure (#32).
- The file (NISSE_CONFIG, default ./nisse-dev.yaml) carries structure
and holds no secrets: credentials are ${VAR} references resolved from
the environment, and a reference to an unset variable is an error
- The environment still wins: every existing NISSE_* variable overrides
its key, and the file is optional, so .env and CI work unchanged
- Validation stays fail-fast and reports every problem at once; unknown
keys are errors, so a misspelt knob fails at startup instead of
silently doing nothing
- The config file joins the state store and .env in the check that
refuses to start when they sit inside the workspace clone
- Tuning covers the executor's turn and failure limits, the consult
budget, the delegation caps, the command timeout and the tool output
limit, which Workspace now takes as a setting; the defaults are the
values the agent already ran with
- llm.max_output_tokens reaches both LLM clients from the config
docs/Tuning-Guide.md explains the knobs for someone learning local
model tuning: how weights, quantization and the KV cache spend VRAM,
what each knob does and what goes wrong at either extreme, and an order
to tune in. nisse-dev.example.yaml documents every key and is checked
by a test so it cannot drift.
Closes#32
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Switching as soon as the active item posts a plan or question means the
operator sees the request and the switch notice seconds apart. A prompt
approval then waits until the other item waits or closes, which can be
a whole milestone. A configurable grace period (default 10 minutes)
lets a watching operator answer before the agent moves on, and still
switches when they are away.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0.2 previously allowed an autonomous switch only while the active item
waits on the operator. Ready work should not sit behind an idle agent
either, so closing an item (completed or abandoned) is now the second
case. This also defines "ready": the agent can act on the item without
the operator, because it has not started or because intake arrived
while it was parked that answers what it was waiting for. A9's
work_item.switch allows a closed or null `from`, and B2 no longer says
only the operator selects the active project.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Letting the model choose which ready item to switch to adds a model
call that belongs to no work item. Duty runs before a finding already
had the same gap. A10 now defines an agent-level scope for these calls,
kept apart from work item transcripts in both directions. The switch
call sees only code-rendered summaries of the ready items, runs as the
executor, and falls back to the oldest item if it fails. A9 gains a
work_item.switch event, and A18 accepts that an injected alert can at
worst reorder work.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
C2's per-tool allow/deny/confirm taxonomy would have to be written in
advance for a tool catalog that will grow large, and every mistake in it
is friction or a hole. An ops write plan is now a list of literal steps
(tool, target, arguments, optional precondition and expected outcome,
retry allowance), and one approval covers exactly that sequence:
- Code renders the approval message from the steps, flagging
irreversible steps, protected targets, and steps that take down the
agent's own host
- A matcher checks every call: read-only is free, an elevated call must
exactly match the next approved step (arguments normalized against the
tool's schema), and anything else halts to AWAIT_INPUT
- Approvals expire if the plan does not start in time
- Classification shrinks to a versioned policy file: instance privilege,
protected targets, and each elevated tool's target argument, all
failing safe; MCP annotations may only tighten
- Bounded matching and standing approvals are deferred, with reasons
Supporting changes:
- A3: an approved sequence resumes after the agent's own restart only on
a verified expected outcome for the step in flight
- A4: an operator kill switch, `stop`, for every agent
- 0.2: the agent may switch items on its own while the active one waits
on the operator; parking unmounts elevated instances; a Matrix reply
to a parked item binds to that item
- A17: step preconditions replace the staleness bounds, the log-checked
verification precondition and the boot-time re-fetch
- A18 (new): prompt injection, designed around limiting what a
compromised model can do rather than detecting injection
- Part H records the decision and parks the deferred items
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Project" was carrying several things at once: the repo, the effort
through arch plan and milestones, and the transcript's scope. The
Sysadmin Agent needs the same distinctions in a different shape, with
standing duties that produce many efforts over one scope. New section
0.2 names them:
- Duty: a standing responsibility that produces work items, and owns
deduplication of its findings
- Work item: the unit of state, identified by a locally minted id
(A9's task_id), optionally linked to an external record
- Scope: what a work item operates on, shared across work items
- Park-and-switch: one active work item, switching only at safe
points, nothing lost while parked; the schema does not assume one
0.1 records this as a bounded exception to extract-after-it-works:
identity is designed ahead, behaviour is not. A2, A4, A9, A10, B1/B2
and C4 now speak in these terms; the transcript is scoped per work
item, and a milestone no longer mints a task_id. Part H parks
concurrency with its costs, and leaves open when a work item must
have an external record.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part F is restructured around rules that now apply to every agent:
- F1: every agent gets its own VM, for inter-agent isolation and
per-agent credential attribution
- F2: a declared, per-instance default-deny egress allowlist that
covers MCP servers' reach, with the tailnet's 100.64.0.0/10 called
out
- F3: MCP servers per agent VM and per (service, site, privilege),
plus the rejected routed-meta-tools alternative
- F4: the existing image pipeline, with third-party MCP images
digest-pinned and excluded from Watchtower
Consequences land where they belong: site identifiers in Part 0, a
machine account per MCP server instance in A7, B3 and C5 recast as
instances of F1/F2, plan approval required for every write in C2, MCP
as the primary infrastructure path in C3, the network-gear browser
item narrowed to fallback access, and stateful-server relocation
parked in Part H.
The standalone amendment is removed now that its content lives here,
as with the Tiered Inference amendment; it remains in the previous
commit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Records a design session on MCP server deployment as a standalone
amendment, ahead of folding it into the spec:
- Every agent gets its own VM, justified by inter-agent isolation and
per-agent credential attribution; Docker-in-Docker stays as an extra
Dev Agent reason rather than the rule's foundation
- Egress is a declared, per-agent-instance default-deny allowlist,
covering what MCP servers reach on the agent's behalf, with the
tailnet's 100.64.0.0/10 called out as outside RFC1918
- MCP servers run per agent VM, one instance per (service, site,
privilege), with site in the server identity rather than a tool
argument
- Third-party MCP images are digest-pinned and excluded from Watchtower
- Routed meta-tools rejected; stateful-server relocation parked
Two consequences settled with the operator while drafting: each MCP
server instance gets its own Vaultwarden machine account (A7), and any
write, reversible or not, requires an approved plan (C2).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Observed rather than assumed, after checking the running instance:
collections carry per-user permissions in three tiers and permissions
are collection-scoped, not per-document. So one collection per writing
principal, with read membership for everyone else.
- States precisely what that enforces: no other agent can write a
given agent's world model. It does not enforce operator-versus-agent
exclusion, since the operator holds manager on their own wiki; that
half is procedural, via the break-glass routine that stops the
agents first. Two different strengths, so the spec no longer implies
one guarantee covers both
- Per-user grants, not groups: Outline groups are not federated with
LLDAP, so using them would put membership somewhere nothing syncs.
Three principals make the manual mapping trivial
- Outline API keys are per-user, so each agent's token must belong to
that agent's own Outline user. A token minted from an admin account
voids every boundary above while appearing to work — the same shape
as the Gitea permission surprise
- Collections identified by ID from config, documents by ID recorded
in the state store, which also makes writes idempotent across a
retry
- The conditional-write requirement softens to a revision *read*:
break-glass stops the agents, so compare-and-swap is unnecessary and
only divergence detection is needed for A3
Also records Outline's MCP server as considered and deferred. A17's
enforcement depends on this library sitting between the model and the
store; MCP's purpose is direct model access, and a procedure the model
may decline to fetch does not constrain it. Still a candidate for the
discretionary search tool, with two things to verify first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two loose ends in the A17 amendment, both found by asking whether the
build order needed changing:
- A17 requires retrieved content to be snapshotted into the canonical
log, but A10's event table — the authoritative list — had no row for
it, so an implementer reading A10 would never know to add one.
knowledge_retrieved now carries the text as read, the document ID,
its revision, and when it was read
- Part G step 10 already scheduled Outline, so the ordering does not
change; it now names A17 rather than only Part D, and says which
half arrives first — the world model, with operator procedures
waiting on the C2 taxonomy they bind to
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Part D said what Outline is for but never how to build against it —
the only external integration with no A-series component, while
Transport, LLM, Browser, Secrets, SSO and Logging all have one. A17
supplies the contract.
Settled here:
- Sharing is read access, never write access. Every document has
exactly one writer, so concurrent writers never exist and Part D's
last-write-wins objection disposes of itself. Default shared
membership is operator + nisse-ops; nisse-dev gets private space
only, since B3 seals it from the LAN and a shared network map is a
map of what it is sealed from
- Agent-owned knowledge is always a cache, never authority: rely on
it to know what exists and where to look, verify a specific fact
against the live system before acting on it
- Enforcement is structural, because a prompt is not enforcement. A
model cannot be stopped from believing what it reads, so the
enforceable goal is narrower — an unverified belief must not become
an irreversible action — and four mechanisms carry it, leaning
hardest on surfacing provenance at the human gate that C2 already
requires
- Never an instruction channel and never an approval channel, stated
both here and in A4 where approvals are specified
- Corrections flow through transport rather than direct edits, which
is better provenance: a hand edit is invisible to the agent's
record, an instruction is authenticated, logged, and reconcilable.
Break-glass reuses Part F's idle flag
- Retrieved content is snapshotted into the canonical log, since A10
forbids a mutable provider handle being the only reference — with
the A3 consequence that boot must re-fetch snapshots for resumed
tasks
- Outage posture is asymmetric: write failures never block, but a
failed read of a procedure bound to a confirm-first action gates,
because proceeding is acting unconstrained on exactly the operation
someone wrote a procedure for
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
B2 specified the service account and its scoped token but never said
the project repo's default branch must be protected — which turns out
to be the load-bearing part of the PR gate.
In Gitea, write access includes merging pull requests, and repository
ownership does not restrict merging either. So an agent with the
access it needs to push branches can also merge its own PR, and the
review gate holds only because the agent's code does not call the
merge endpoint. That is the advisory guarantee A4 exists to rule out.
Verified rather than assumed: before protection was configured, the
agent's own token merged its own PR (HTTP 200). With protection it is
refused (405), as is a direct push to the default branch. The
amendment says to assert both, so relaxing the protection later fails
a test instead of silently removing the gate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every durable mutation emits one JSON-lines event, emitted by the
state store itself immediately after the commit returns, with a fixed
envelope (ts, agent, event, task_id correlation ID, cause) and a
dotted event taxonomy covering transitions, intents, gates, boot
reconciliation, and migrations. Logs stay diagnostic-only, never
load-bearing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part H's repository-layout item is resolved: one repo, three packages.
Naming (S0), scope (S0.1), the step-10 extraction, and the repository
layout section are updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>