Seiten der Dokumentation

Diese Seite gibt es bisher nur auf Englisch.

Groundnote threat model#

Status: living document, first version 2026-11-05; state columns checked against the code on 2026-11-05 and updated for the review findings on 2026-11-05 (commits named per row). Method: STRIDE per trust boundary, then one section per named attacker. Every control names where it is enforced and how it is tested, so a reviewer can check the claim instead of trusting the prose. Controls marked open are not built yet; they are listed so nobody mistakes the plan for the state.

What we protect#

Asset Why it matters Where it lives
Confirmed rules and their decision history The only part that cannot be rebuilt from sources: a person spent judgement on it data/scopes/<scope>.sqlite, tables rule, rule_version, decision
Source excerpts (mail text, transcript windows, attachment text) Personal data of third parties (customers, colleagues), often confidential business content same scope file, tables source, source_attachment
Scope separation A rule from a private scope must never surface in a business scope and vice versa one file per scope
Grants and approval identity Whoever can approve decides what an assistant will treat as true data/ops.sqlite, Access JWTs
Backups and exports Full copies of everything above, outside the server Litestream bucket or copy directory, export/<scope>/
Operator secrets Session secret, ops token, provider keys, backup credentials environment only

Trust boundaries#

 untrusted content            untrusted callers                 trusted (narrowly)
 ─────────────────            ─────────────────                 ───────────────────
 mail / meeting text ──▶ [B1 ingest] ──▶ scope files ◀── [B4 approval] ◀── person in browser
                                          ▲      ▲
 AI client (claude.ai, ─▶ [B2 /mcp] ──────┘      └──── [B5 ops, loopback] ◀── operator CLI
  Claude Code, tasks)
 service clients ──────▶ [B3 /v1 API]
 webhook senders ──────▶ [B6 webhooks: ID only, no content]
 search results ───────▶ [B7 output to a model]  ◀── the boundary most memory tools forget
 backups / exports ────▶ [B8 storage outside the host]
 dependencies, CI ─────▶ [B9 supply chain]
 query + statements ───▶ [B10 external judge: TypeSafe JEV, USA]  (stage c, off by default)

B7 is drawn on purpose: Groundnote does not run the model that reads its search results, but the text it returns becomes part of that model's context. A rule or excerpt is therefore an input to someone else's agent, and Groundnote is responsible for how it is framed.

STRIDE by boundary#

Boundary S T R I D E
B1 ingest Forged sender in mail headers Instructions in content try to become rules — Content of one scope stored in another (wrong feeder binding) Huge mails, attachment bombs Content tries to reach a tool
B2 /mcp Stolen OAuth token Proposal with fabricated quote Proposals without author Cross-scope search, enumeration of scope names Expensive queries, oversized inputs Model tries to approve
B3 /v1 Stolen service bearer Overwriting history via API — Same as B2 Same as B2 Service token used on approval
B4 approval Session riding (CSRF), clickjacking Decision on a stale version Decision without identity Rejected proposals leaking into search — Service token or MCP token accepted
B5 ops Remote caller pretending to be local — Grant changes without log — — Creating an approve grant for a service
B6 webhooks Forged notification — — Validation endpoint reflecting input Notification floods Notification carrying content
B7 output — Stored text steers the reading model — Rule text from a scope the reader may not see Context flooding Excerpt instructs the model to call write tools elsewhere
B8 backups — Restored file swapped between scopes — Plaintext copies in a bucket Silent backup failure —
B9 supply chain Typosquatted package Malicious release, compromised action Unsigned artefacts Dependency exfiltrates data — Install script runs on the build host
B10 external judge Provider endpoint impersonated Foreign text in a statement or query steers the score and suppresses a hit — Query and statement text leave the host (USA) Provider down or slow; monthly cap used up —

Attackers and controls#

A1 — attacker writes a mail or speaks in a meeting (prompt injection)#

The attacker controls text that Groundnote ingests: a customer mail, a forwarded thread, an attachment, a sentence in a meeting. Goals: (a) get an instruction stored as a confirmed rule, (b) make a reading model act on stored text later, (c) get content of another scope returned, (d) hide a real rule.

Groundnote's position is that no filter recognises all injections. The design therefore makes injected text powerless rather than trying to detect it. The detector exists, but only as a warning signal for the human reviewer.

# Control Enforced where Tested by State
PI-1 Content never becomes an instruction to Groundnote. No part of the server interprets stored text: no templating, no tool dispatch, no URL fetch based on content. Ingest stores bytes and metadata only. ingest and extraction modules red-team suite: every attack is stored and returned as inert data built (ingest stores data only)
PI-2 Extraction model has no tools and structured output only. Its output can at most become a proposal. extraction (phase 2) architecture test: extraction module imports no tool or network client except the model client open (phase 2)
PI-3 A proposal needs a verbatim quote found in the stored source (mail: normalised substring, at least 20 characters; meeting: inside ±120 s of the timestamp). A model that invents a rule cannot invent the quote. proposal path property tests; red-team cases "fabricated quote", "quote from another scope" built (quote check, slice 7)
PI-4 Only a person approves, only in the approval channel — or, since slice A, the deterministic policy inside the mandatory exceptions (section "Approval modes", AM1–AM9). No MCP tool can confirm, reject, revoke or review. The MCP module cannot import the decision module (import-graph test). Service tokens are rejected on approval. auth + routing + architecture test red-team cases "please approve", "tool call smuggled in quote" built for approval routes; import-graph test AT1–AT4 open until MCP exists
PI-5 Provenance per sentence. Every text Groundnote returns to a model is split into units, and each unit carries its origin: groundnote (server-generated, e.g. the summary line), human-confirmed (statement text a person approved), or untrusted:<source-ref> (quote, excerpt, attachment text, subject). src/security/untrusted.ts unit + red-team tests built, wired into every MCP result and the CLI output (F4, 26ed1e4)
PI-6 Untrusted text leaves Groundnote only inside an encoded, delimited block. The block has a random per-response boundary, the text is escaped so it cannot close the block or open a new one, control and bidi characters are made visible, and the block states that its content is data, not instructions. A forged closing marker inside the text is neutralised. src/security/untrusted.ts red-team "fake end marker", "fake system block", bidi and zero-width cases built, wired into every MCP result and the CLI output (F4, 26ed1e4)
PI-7 Injection signals as a warning, never as a gate. A detector flags typical patterns (role takeover, "ignore previous", fake system or tool syntax, hidden Unicode, exfiltration URLs, approval requests). The result is shown to the reviewer next to the proposal and attached to search results as signals. It never blocks ingest and never lowers the bar for approval; an unflagged text is not "safe". src/security/injection-signals.ts red-team suite asserts ≥ 90 % recall on the attack set and 0 false positives on the benign set; both numbers are tracked, not used as a guarantee module built, not shown on approval page (F7)
PI-8 No tool with external effect. Groundnote's MCP tools read and propose. There is no tool that sends, deletes, forwards or fetches URLs. Proactive outputs (phase 4) post fixed templates with references, never stored text as free prose, and strip links and images from untrusted text. MCP tool list; output module MCP tool-list snapshot test open (MCP not built); tool-list test AT4 planned
PI-9 Rejected proposals are invisible outside approval. A rejected injection cannot later be found by search and applied by a model. search filter + FTS removal on reject property test "rejected never in search" built (filter on every read path, reviewed)
PI-10 The human sees what the model saw. The approval page shows the verbatim quote, the source link, injection signals, and previously rejected similar proposals. approval UI UI test (open) open (approval UI)

Residual risk: a person approves a malicious rule whose quote is genuine (for example a customer writes "you must always ship via carrier X" and the reviewer agrees). Groundnote cannot prevent judgement errors; it makes them attributable (decision row with identity and time) and revocable (decision revoke keeps the history).

A2 — stolen token#

Token Impact if stolen Controls
MCP OAuth/JWT of a person Read the person's scopes, create proposals Short JWT lifetime from Access; aud pinned to the MCP application; scopes limited per grant; proposals are inert until a person approves; every call logged in search_log per scope with grant id; rate limit per grant
Approval session Approve or reject in the person's scopes Separate audience; session cookie HttpOnly; Secure; SameSite=Strict; Path=/approval, __Host- prefix; CSRF token bound to identity; idle and absolute timeout; decisions name the version seen (409 on stale)
Service bearer Read and propose in the service's scopes Stored as SHA-256 only; compared in constant time; disable per grant via ops; never valid on approval
Ops token Create grants and scopes Listener bound to loopback only; refuses non-loopback peers even behind a proxy header; separate secret

JWT verification must pin, in this order: algorithm allow-list (RS256 only; reject none, HS*, and any alg not in the list before looking up a key), key by kid from the configured JWKS only (no jku, x5u or embedded jwk headers), signature, iss equal to the configured team domain, aud containing exactly the audience of this channel, exp in the future and nbf/iat not in the future (clock skew at most 60 s), and an identity claim present. JWKS is cached with a maximum age and refetched on unknown kid at most once per minute, so an attacker cannot force a fetch per request.

A3 — someone reaches the server, a backup or an export#

Threat Control State
Disk or VM image copied Encryption at rest by the volume (LUKS, FileVault, encrypted cloud disk). See decision below. operator duty, documented
Other local users read files Data directory 0700, files 0600, created with a restrictive umask; the server refuses to start if the data directory is group- or world-accessible built (F2, 25231ea: data-dir-permissions refusal), see docs/security/data-at-rest.md
Backup bucket leaked Litestream 0.5.x has no client-side encryption (its age block is rejected at start since 0.5.1, per the Litestream configuration reference, retrieved 2026-11-05). A bucket therefore holds plaintext SQLite pages. Use a bucket with provider-side encryption and a bucket-scoped key, or copy mode with an encrypting backup tool (restic, borg). documented; the default for sensitive scopes should be copy + restic
Export directory leaked Export is plaintext Markdown by design (readable without the service). It inherits the data-directory permissions and must never be committed or published. documented
Scope file swapped after restore File knows its scope (meta.scope), server refuses a mismatch (contract 4.3) core rule 6
Empty file after a failed mount Full-loss lock (503 scope-locked) instead of "no rules" core rule 7

A4 — data flows to a client that should not get it (MCP exfiltration)#

Groundnote answers whoever holds a valid grant. Exfiltration risks are therefore about which data a valid caller receives and where the client sends it next.

  • Scope check on every call, including scope names derived from references (rule/sales-0042 belongs to sales). Unknown and forbidden scopes get the same 403, so scope names cannot be enumerated.
  • Cross-scope search only with an explicit flag and only if every scope is in the grant.
  • Minimal output. Search returns bundles with excerpts, never full source bodies; the full excerpt needs provenance on a single reference.
  • Per-scope provider allow-list (providers_allowed) for any path where content leaves the server (embeddings, reranker, extraction). An empty list means: nothing leaves.
  • Logging without content. search_log stores terms, stages and reference ids, plus a SHA-256 of the input, never the text.
  • Client side is out of our control. A claude.ai connector may combine Groundnote output with other tools. PI-6 and PI-8 limit what our text can make that client do; the operator decides which clients get a grant.

A5 — malicious contributor or compromised dependency (supply chain)#

Control Where State
Lockfile committed; CI installs with --frozen-lockfile; install scripts disabled except an explicit allow-list (pnpm.onlyBuiltDependencies) package.json, CI built (allowBuilds in pnpm-workspace.yaml)
Dependabot for npm and GitHub Actions, grouped weekly .github/dependabot.yml built
Actions pinned to commit SHAs, permissions: read-all by default, no pull_request_target with checkout of PR code workflows built
OpenSSF Scorecard on main .github/workflows/scorecard.yml built (this branch)
Signed releases: container image signed keyless with Sigstore cosign, SBOM (CycloneDX) attached, build provenance attestation release workflow (open until the first release) SBOM and provenance attestation built in release.yml; cosign signature open
Required review by a code owner for src/auth/**, src/security/**, src/decision/**, migrations and workflows CODEOWNERS + branch protection (repository setting, open) open (no CODEOWNERS, repository setting)
DCO sign-off on every commit CONTRIBUTING built (CONTRIBUTING)
Architecture tests guard the two invariants a malicious change would target first: MCP cannot reach decisions; no module outside src/storage opens a scope file test suite open (test/arch not built yet)

A6 — foreign text reaches the external judge (stage c, boundary B10)#

Stage c sends the query text and the statements of up to 50 candidate rules per scope to TypeSafe AI (JEV, USA). Two risks: (a) disclosure: the texts leave the host and the EU; (b) prompt injection: a mail that was ingested can land in a query (the caller searches with the mail text) or in a rule statement, and that text can act as an instruction to the judge, pushing the score of a real rule under the threshold and so suppressing a hit. The judge returns numbers only; it cannot write, approve or call tools, so the damage is a hidden or a promoted rule, not an action.

Control Where State
Stage c is off by default: it runs in a scope only with search.reranker.enabled, scopes[].reranker: true and typesafe in providers_allowed src/config/file.ts (rerankerScopes), src/search/rerank/stage-c.ts built
No rule ids or scope names go out; the questions are positional; the quote, speaker and source link are never sent src/search/rerank/jev-client.ts built
Proposals (unconfirmed text) never go to c; with include_proposals they stand behind the reranked heads in a+b order with rank.reranker: null src/search/search.ts built
The query sent is cut to 4000 characters; the response body is capped at 1 MiB jev-client.ts built
Monthly cap (monthly_cap_usd) in ai_usage; reached → stages.c: budget, no call; failed calls without usage are booked with an estimate; a usage write that fails is kept in memory and counts against the cap until it is written src/search/rerank/budget.ts, stage-c.ts built
TYPESAFE_API_KEY from the environment only, distinct from other secrets, never logged startup built
Failure or timeout of c → the result is that of a (or a+b); stage a stays the recall floor src/search/search.ts built

Residual risk. A confirmed rule whose statement carries an injected instruction, or a query built from a hostile mail, can still lower its own or another rule's score. Mitigation is the recall floor (a+b hits remain when c fails, but not when c succeeds with a low score) and that statements are confirmed by a person before they reach c. Switch c on only for scopes whose texts may leave the host. Open: the cap is instance-wide; one grant can use it up, after which c is budget for everyone until month end (a+b keeps working). A quota per grant is not built. Overshoot of the cap: the check reads the spent amount before the call and books after it (check-then-act), so parallel searches can pass the check together; the cap is exceeded by at most (parallel searches × cost of one call), each call holding at most 50 statements and a query of 4000 characters.

Abuse limits#

Limit Value Reason State
Request body 256 KiB on /v1 and /mcp, 10 MiB on POST /v1/sources (contract §6) memory, parser cost built
Search text 20 000 characters; terms ≤ 20, each ≤ 100 characters (contract §7, SearchRequest) FTS query cost built
limit ≤ 50 bundles contract built
Rate per grant: 60 requests/min on /mcp and /v1, 60 decisions/min and 6 exports/hour on approval stolen-token blast radius built (F1, b2aec29, src/http/rate-limit.ts)
Rate per address (known bypass) 5 login attempts/min per client address; at most 10 000 buckets, least recently used evicted in O(1), so an attacker with more than 10 000 addresses (or one IPv6 prefix) is not limited per address dev login guessing; the dev token is ≥ 32 chars, so the limit is a brake, not the defence accepted
FTS query built from tokens only; FTS5 operators and quotes in user input are escaped query injection into FTS5 syntax built (fuzz-tested)

Logging#

Never logged: request bodies, source text, quotes, tokens, cookies, Authorization and Cf-Access-Jwt-Assertion headers, provider keys. Logged: method, route template, status, duration, grant id, scope names, error codes. A test asserts that a request with a known secret and a known sentence leaves neither in the log output.

Supply chain note: stage b (2026-11-06)#

@huggingface/transformers is an optional dependency (exact version, loaded by dynamic import only in src/search/semantic/transformers-embedder.ts). It pulls sharp and with it libvips (LGPL-3.0). pnpm audit --prod now reports high advisories in libvips, libheif and librsvg. They are not reachable: Groundnote embeds text and never decodes images. The statement "pnpm audit clean" therefore holds only for an install without the optional package. Model files are pinned by revision and SHA-256 and loaded from disk only (env.allowRemoteModels = false); the service never downloads.

Right to erasure despite append-only#

See docs/security/data-at-rest.md, section "Erasure". Short form: redaction with proof is the default (a separate, audited erase path replaces the personal text with a tombstone and its hash); crypto-shredding per source is the option for operators whose backups outlive the erasure deadline.

Out of scope#

  • The security of the AI client and its other tools.
  • A malicious operator with root on the host.
  • Judgement errors of the approving person (mitigated by history and revoke, not prevented).

Findings against the Phase 1 contract (docs/architecture.md, 2026-11-05)#

Checked against the contract before code exists, so they are cheap to fix. Each is a proposal for the contract, not a change made here.

# Finding Proposal State
F1 No rate limit anywhere in the contract; the dev approval login has no attempt limit Per-grant limits from "Abuse limits" above; 429 rate-limited; login: 5 attempts per minute per address built, b2aec29 (limits, src/http/rate-limit.ts; 429 rate-limited)
F2 Data directory is only checked for writability Refuse start if data/ is group- or world-accessible; create files with umask 077 built, 25231ea (start refused with data-dir-permissions; data dir 0700 under umask 077)
F3 Litestream mode replicates plaintext pages (no client-side encryption in Litestream 0.5.x) State it in docs/backup.md; recommend copy + restic for scopes with personal data documented (docs/backup.md, 72dc318)
F4 MCP text output repeats quotes, excerpts and subjects as plain JSON strings Render the MCP text content through src/security/untrusted.ts (provenance per sentence, random boundary); keep structuredContent unchanged and add untrusted: true markers plus signals per head built, 26ed1e4 (src/security/model-output.ts; untrusted and signals in structuredContent)
F5 Approval cookie is gn_session with Path=/ Use the __Host- prefix (__Host-gn_session, Secure, Path=/, no Domain) whenever public_url is https built, b2aec29 (src/auth/session.ts: __Host-gn_session under https)
F6 No erasure path although sources hold third-party personal data Redaction with proof as in data-at-rest.md; one narrow trigger exemption, property-tested planned: docs/redaction.md (slices R1–R7), architecture.md section 15.2
F7 Injection signals are not shown on the approval page Show detectInjectionSignals results next to the quote; never block on them built, f351776 (src/approval/views.ts shows signals next to statement, quote and source)

Security review of the built core (2026-11-05, HEAD b303ec0)#

Two-stage review (finder, then an independent verifier per blocker). No blocker. Verified without finding: RS256-only allow-list before key lookup, no jku/x5u/jwk, iss/aud per channel/exp/nbf; bearer only on api; ops listener loopback-only with constant-time token compare; scope names cannot traverse paths; FTS input is quoted; rejected rules are filtered on every read path; logs and error bodies carry no content; pnpm audit had no findings at that review (state of 2026-11-05; see the supply-chain note below for the later finding).

Findings of the review; all are fixed (state per row):

# Where Finding Proposal State
R1 src/search/bundle.ts source, thread and attachment entries carry full mail and attachment text instead of excerpts (contradicts A4; response size grows with source size) cap excerpt (e.g. 500 characters around the quote); full text only via provenance on one reference built, 32b1f72 (EXCERPT_CHARS = 300, src/search/excerpt.ts)
R2 src/domain/schemas.ts (MailId) no length limit; bidi, zero-width and control characters allowed in an attacker-chosen id that is echoed outside any untrusted block enforce RFC 5322 msg-id (printable ASCII, ≤ 998 characters) built, c25d4fa (mail and thread ids restricted to RFC 5322 msg-id)
R3 src/domain/sources.ts (409 other-feeder) tells any proposer that another feeder already stored a message id answer like source-differs built, 03697a6 and ae15760 (one generic 409 source-conflict, D66)
R4 src/config/file.ts (team_domain) http:// accepted, JWKS fetched in clear text require https as for public_url built, 952781b (team-domain-not-https)
R5 src/auth/access-jwt.ts future iat accepted; JWKS cooldown 30 s (jose default) reject iat in the future beyond clock tolerance; set cooldown explicitly built, b2aec29 (future iat rejected, JWKS cooldown set explicitly)
R6 src/auth/access-jwt.ts toLowerCase() folds non-ASCII (Kelvin sign → k) in identity emails reject non-ASCII identity emails built, b2aec29 (identity emails must be printable ASCII)
R7 src/ops/cli-run.ts CLI sends the ops token to whatever listens on the loopback port Unix socket 0600 in the data directory built, 25231ea and 2a18d16 (ops on a Unix socket 0600, race closed; D69)
R8 src/search/feedback.ts feedback accepts a search id of another grant bind search_id to the grant built, cf05fe5 (feedback bound to the grant that ran the search)
R9 rendering for F4 a proposal's statement must render as untrusted, not as groundnote provenance follows status built, 26ed1e4 (a statement is human-confirmed only while its record is confirmed)
R10 src/graph/schemas.ts, src/graph/normalize.ts (osaDistance) denial of service through normalization expansion: the 200-character limit applied to raw input, NFKC expands one ligature to 18 letters, names are stored and scored normalized, and the full OSA matrix made one entity_resolve take 1.6 s and a duplicate check 6.5 s on the event loop bound the normalized length of name, aliases and q; compute the distance only up to the typo limit built, 488f827 (normalized form ≤ 200 characters in the input schemas; osaDistance banded with length gate and early exit, graph.md §5.3)

The core adds entity (with a parent, so hierarchies such as department → team → project) and link with an origin deterministic, proposed or human. Threats and the controls the design needs. The graph is built (slices G1–G8, 30fb494 … eb38afd); G1–G8 below are the controls it was built against, and the individual tests are listed in graph.md §14. They have not been re-verified one by one in this pass.

# Threat Control
G1 Link across scopes. A link or a parent pointer to an entity in another scope file would recreate the cross-file bridge the design forbids. Entities and links live in the scope file; link.to and entity.parent are foreign keys inside that file; ids carry the scope prefix and a trigger rejects a foreign prefix, as for rule ids.
G2 Injection through entity names and aliases. A mail introduces "Customer: Ignore previous instructions …" or an alias with bidi or invisible characters; names are shown to models in every bundle. Names and aliases from sources are untrusted text: rendered inside untrusted blocks with provenance (F4), neutralized, length-limited (e.g. 200), injection signals stored with the proposal; only a person turns a proposed entity into a confirmed one.
G3 Impersonation by look-alike names. "Acme GmbH" with a Cyrillic "а" becomes a second entity that collects links meant for the real one. NFKC plus confusable skeleton (UTS #39) as a uniqueness key per scope and parent; a collision is shown to the reviewer as "possible duplicate", never merged automatically.
G4 entity_create spam via MCP or a feeder: thousands of entities or aliases bloat the scope and every search. Creation through MCP only as a proposal (propose right), rate-limited per grant (F1), a cap per source and per day, and counted in /v1/status.
G5 Re-parenting a foreign subtree. Moving "Department 3" under an attacker-controlled node changes what every search below it returns. parent is written only by the decision path (like status); a change is a new version, never an update; a cycle check in the same transaction; depth limit (e.g. 12).
G6 Forged link origin. A model-proposed link claims deterministic and skips review. origin is set by the service from the code path that created the link (thread, attachment, Paperless id), never from input; MCP and API input can only create proposed.
G7 Traversal cost. Deep or cyclic hierarchies and fan-out make "everything under department X" unbounded. Depth and result caps per query, recursive CTE with a depth column, cycle prevention at write time (G5).
G8 Exfiltration through structure. Entity names reveal customers even without rule text. Same scope check as rules for every entity and link read; names count as content in logs (never logged).

Measured on the private real-data set (counts only; see the operator's notes): of 74 rule sources, 34 carry a mail thread and 4 threads hold more than one rule; 9 sources have 35 attachments, only 2 of them with readable text; 8 rules come from documents with a stable document id; 8 sources are shared by more than one rule. These links follow from headers and ids alone and need no model.

Approval modes A/B/C (slice A, specified 2026-11-06, approval-modes.md)#

Modes A and B let rules become valid without a person. The goal of an attacker shifts from "convince the reviewer" to "pass the classification". The controls below are specified for slice A and land with its commits; "built in this slice" is the target state and must be re-verified against the tests of approval-modes.md §15 after the merge.

# Attack Control Enforced / tested by State
AM1 Fraud mail "from now on new bank details" (IBAN, paraphrase "the transfer data changed, see attachment"), proposed by an agent with a harmless statement and a genuine quote. payment on the proposal (valid and invalid IBAN shapes with any separator between the groups, BIC, account numbers, amounts ≥ threshold, paraphrase keywords and stems such as zahlung, konto, bank gewechselt) is mandatory; every scan reads the skeleton (NFKC, Cyrillic and Greek look-alikes folded to Latin), accents are removed, and a mixed-script word, a combining mark or any letter without a Latin base is uncertain (fail closed); for an external sender the source text (subject, display sender, mail, attachments) also triggers external-sensitive; an unreadable attachment from outside is uncertain. All three wait for a person in every mode. classify(), resolve(); classify.test.ts "evasion of the payment scan", intake.test.ts, E2E approval-fraud.test.ts (incl. the _-grouped IBAN) built in this slice; separator, look-alike and stem bypasses of r1 closed in r2; the separator limit, U+2800 and IBANs in digit words of r2 closed in r3 (no limit, doubt digit-run, E2E "IBAN behind …")
AM2 Forged internal sender: From: an internal address without valid authentication. Internal only with a listed sender and dmarc=pass (with matching header.from) in the first Authentication-Results of a trusted authserv-id, stored by a service feeder; anything missing or unparsable is external. sender.ts; sender.test.ts (each failing condition), E2E built in this slice
AM3 DMARC spoofing via forged Authentication-Results written by the sender, or a domain without DMARC enforcement. Only the topmost header of an authserv-id in authserv_ids counts; the result pass is required, not the published policy. Operator duty: the receiving MTA prepends its own header and strips foreign ones with its authserv-id (Postfix/rspamd, Exchange Online and Google do). A person-grant feeder cannot vouch (D74). sender.ts; test "forged trusted header below the real one" built in this slice; MTA stripping is an operating step
AM4 Detector probing: an attacker with propose sends variants to learn which texts pass the injection detector. Outward the detector is only the reason review, never a signal id or pattern; class names hide behind mode; per-grant rate limit (60/min); every probe leaves a classification row with grant and detector ids for the operator. Residual: the binary oracle review / not review remains (accepted). intake.test.ts "detector hit in A", limits built in this slice
AM5 Daily-limit flooding: many trivial proposals use up max_auto_per_day, so later real proposals wait, or a flood of junk is adopted before anyone looks. The limit caps the damage per scope and day (default 20); beyond it everything waits with daily-limit (fail-safe); the exceptions page shows the banner and the process log approval-daily-limit; /approval/auto lists every adoption with a revoke form. intake.test.ts "daily limit" incl. the DST day built in this slice
AM6 Retract abuse by MCP (an injected model retracts rules) or by a stolen service token. Retract works only on rules whose newest confirm/review is the policy's; a person-confirmed or person-reviewed rule answers 409 not-auto-adopted (trigger); reason required; rule_retract cannot confirm, revoke or change; history stays; a retracted rule can be proposed again from the same source; a service token's retract shows in the rule page history and on /approval/auto (operator decision 2026-11-06, approval-modes.md §16.6). Residual: availability of auto-adopted rules (accepted, fail-safe direction). retract.test.ts, exceptions.test.ts "service token's retract in the audit", trigger test, AT1/AT4 built in this slice
AM7 Secret stored through a proposal (a key or password quoted into a rule, then served to every reader). Secret scan before anything is stored: 422 secret-detected, no rule, no classification, body and logs without the value, search_log.input_sha256 NULL; only a secret_finding row without text. The retract reason gets the same scan (422 secret-detected, nothing stored). intake.test.ts "secret in A", "compound credential keywords"; retract.test.ts "reason holding credentials" built in this slice; compound keywords (Adminpasswort: …) and arrow operators found in review of r1, closed in r2; PIN-Code: …, TAN-Nummer: …, →, plurals and soft hyphens of r2 closed in r3 (structural keyword, no operator; a keyword without a value is the mandatory exception access, D91)
AM8 Mode or guardrail switched off from the model side (an MCP call sets mode A or a class to auto). The mode exists only in groundnote.json; no tool, route or form writes it (AT4: no mode/approval/classes key in tool inputs); the validator refuses a mandatory exception as a class (AT7); adopt() is reachable only through intake() (AT2c, AT2d), and the schema refuses an auto decision without an auto classification of the same version. AT1b, AT2c, AT2d, AT4, AT7, migration trigger tests built in this slice
AM9 False rule adopted (misunderstanding, joke, outdated statement with a genuine quote) and applied by agents. Not prevented (the accepted price of decision 6); bounded: quote requirement in every mode, mandatory exceptions, marking in every answer and in the MCP text (never human-confirmed), retract, daily limit, complete log. Measure the retract rate of auto-adopted rules. bundle and MCP rendering tests built in this slice; residual accepted

Auf GitHub bearbeiten