From 2352a84004346af5926456c6d5a6722be4abbe16 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Tue, 8 Sep 2026 19:36:39 +0000 Subject: [PATCH] Map A17 onto Outline's actual primitives, and record the MCP decision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/Architecture-Build-Spec.md | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/Architecture-Build-Spec.md b/docs/Architecture-Build-Spec.md index 54e8b71..9c43c9a 100644 --- a/docs/Architecture-Build-Spec.md +++ b/docs/Architecture-Build-Spec.md @@ -622,6 +622,56 @@ rests on. **Deferred:** the operator-writable procedures collection. The interface is defined here; the content and the C2 binding list arrive with the policy taxonomy (Part H). +**Concrete mapping onto Outline.** Observed in the running instance: a **collection** +carries per-user permissions with three tiers — visible-to (read), read-write, and +manager. Permissions are collection-scoped, not per-document. +- Therefore **one collection per writing principal**, with read membership granted to + everyone else who needs it: `nisse-ops-world` (write: `nisse-ops`), + `nisse-dev-world` (write: `nisse-dev`), `procedures` (write: operator). +- **What this enforces, precisely:** no *other agent* can write a given agent's world + model — the collision that would otherwise be silent and unattributable. It does not + enforce operator-versus-agent exclusion, because the operator holds manager on their + own wiki and can always grant themselves write. That half is procedural: the + break-glass routine stops the agents first, so concurrent writers do not exist while + editing. +- **Assign per user, not per group.** Outline groups appear to be internal to Outline + rather than federated with LLDAP (A8), so using them would put membership in a second + place that nothing syncs. With three principals the manual mapping is trivial and + obvious; revisit only if the cast grows. +- **Each agent's API token must belong to that agent's own Outline user.** Outline API + keys are per-user, so the token *is* the identity every permission above evaluates + against. A token minted from the operator's account — or an admin's — silently voids + the entire boundary while appearing to work. +- **Identify collections and documents by ID, from config and state.** Collection IDs + come from env (`NISSE_OUTLINE_*_COLLECTION`), never discovered from "whatever the + token can write": a permission change must not silently redefine what an agent + considers its own. Document IDs are recorded in the state store (A2) when created and + used for updates, which also makes writing idempotent — a retry after a crash updates + the document instead of creating a second one. +- **Revision reads, not conditional writes.** Because break-glass stops the agents, + compare-and-swap is unnecessary; what is needed is the ability to *observe* that a + document changed since the agent last read it, feeding the A3 divergence check. + Whether Outline exposes a revision identifier or only a modified timestamp is + unverified — either satisfies this. + +#### Considered and deferred: Outline's MCP server + +Outline can expose search, read, create and edit over MCP (remote, Streamable HTTP, +enabled workspace-wide). **Not adopted for the retrieval path**, because A17's +enforcement depends on this library sitting *between* the model and the store: staleness +marking, snapshotting into the canonical log, binding procedures to action classes, and +gating a failed confirm-first read all happen to a retrieval before the model sees it. +MCP's purpose is the opposite — direct model access — and a procedure the model may +decline to fetch does not constrain it (A12). + +It remains a reasonable candidate for the *discretionary* search tool later, and unlike +the vendor agent SDKs A13 rejects, MCP is a neutral standard rather than lock-in: one +client would serve many servers if others are ever added. Before adopting it, verify +that it honours the key owner's **collection** permissions rather than mere workspace +membership, and that the workspace-level toggle does not force broader exposure than +intended. + + --- ## PART B — THE DEV AGENT / `nisse-dev` (build first) -- 2.54.0