Governance controls¶
ZettelForge applies four layered controls every time content enters memory: a content size limit, a prompt injection check, optional PII detection, and a write-time anomaly gate. All four are configured under the governance block in config.yaml.
Control order at remember() time¶
When you call mm.remember(content), the controls run in this sequence:
- Content size check — rejects content that exceeds
governance.limits.max_content_length. - Prompt injection block — rejects content that contains high-confidence instruction-injection patterns.
- PII detection (disabled by default) — logs, redacts, or blocks content containing personally identifiable information.
- Memory anomaly gate (audit mode by default) — scores the candidate note against recent trusted notes; blocks or quarantines when the anomaly score exceeds the calibration threshold.
An exception in any step halts storage. The content never reaches the backend store unless all enabled controls pass.
Config reference¶
All keys live under the governance top-level block in config.yaml.
Top-level keys¶
| Key | Default | Description |
|---|---|---|
enabled |
true |
Master switch for governance controls. Setting to false disables structural validation; PII and defense have their own switches. |
min_content_length |
1 |
Minimum byte length for stored content. Content shorter than this raises GovernanceViolationError. |
Environment override: none (use config file).
governance.limits — operation limits¶
DoS mitigation. Values of 0 disable the corresponding limit.
| Key | Default | Env var | Description |
|---|---|---|---|
max_content_length |
52428800 (50 MB) |
ZETTELFORGE_LIMITS_MAX_CONTENT_LENGTH |
Maximum byte length of content passed to remember(). Content exceeding this raises GovernanceViolationError. |
recall_timeout_seconds |
30.0 |
ZETTELFORGE_LIMITS_RECALL_TIMEOUT |
Wall-clock timeout for recall() queries. 0 means no timeout. |
governance.pii — PII detection¶
Disabled by default. Requires pip install zettelforge[pii] (Microsoft Presidio + spaCy). When the package is not installed, ZettelForge logs a warning at startup and continues without PII enforcement — no core functionality breaks.
| Key | Default | Env var | Description |
|---|---|---|---|
enabled |
false |
ZETTELFORGE_PII_ENABLED |
Enable PII detection before remember(). |
action |
"log" |
ZETTELFORGE_PII_ACTION |
What to do when PII is detected: log (warn, pass through), redact (replace with placeholder), block (raise before storage). |
redact_placeholder |
"[REDACTED]" |
— | Replacement text for redacted PII spans. Used only when action=redact. |
entities |
[] (all) |
— | Presidio entity types to detect. Empty list detects all supported types minus the CTI allowlist. |
language |
"en" |
— | Language code passed to Presidio. |
nlp_model |
"en_core_web_sm" |
— | spaCy model used by Presidio. Downloaded automatically on first use. |
CTI allowlist. IP addresses (IP_ADDRESS), URLs (URL), and domain names (DOMAIN_NAME) are excluded from PII detection by default. These are legitimate threat indicators, not personally identifiable information. The allowlist applies when entities is empty (detect-all mode). When you specify an explicit entities list, only the listed types are detected and the allowlist is not applied.
Actions:
log— Presidio detects PII, logs entity types and confidence scores (never the actual text), and passes content through to storage unchanged.redact— Detected PII spans are replaced with theredact_placeholderbefore storage.block— Any PII detection raisesGovernanceViolationErrorand content is not stored.
governance.memory_defense — write-time anomaly detection¶
A MemSAD-style anomaly gate that scores each candidate note against recent trusted notes before writing. Default mode is audit: anomalies are logged but writes proceed. Switch to block or quarantine once you have a clean calibration corpus.
| Key | Default | Env var | Description |
|---|---|---|---|
enabled |
true |
ZETTELFORGE_MEMORY_DEFENSE_ENABLED |
Enable the anomaly gate. |
mode |
"audit" |
ZETTELFORGE_MEMORY_DEFENSE_MODE |
audit logs anomalies; block raises MemoryAnomalyError; quarantine writes the note to a JSONL file and then raises. |
min_calibration_notes |
50 |
ZETTELFORGE_MEMORY_DEFENSE_MIN_CALIBRATION |
Minimum number of reference notes required to compute the calibration baseline. Below this count the gate passes all writes through with reason calibration_insufficient. |
max_reference_notes |
50 |
— | Maximum reference notes used per evaluation. Ordered by most recently created. |
kappa |
2.0 |
ZETTELFORGE_MEMORY_DEFENSE_KAPPA |
Sensitivity multiplier. Threshold = calibration mean + (kappa × calibration std dev). Lower kappa = more sensitive. |
lexical_weight |
0.25 |
— | Weight of the lexical divergence term in the composite score. |
ngram_size |
3 |
— | Character n-gram size for the lexical Jensen-Shannon divergence term. |
monitored_domains |
[] (all) |
— | CTI domains to monitor. Empty list monitors all domains. Notes with a domain not in this list pass through without evaluation. |
quarantine_path |
"" |
— | Path to the quarantine JSONL file. Default: <data_dir>/quarantine/memory_anomalies.jsonl. |
quarantine_raw_content |
true |
— | Include raw note content in quarantine records. Set to false to omit content from quarantine logs. |
Anomaly scoring¶
The gate computes a composite score for each candidate note:
memsad_score = 0.5 × max_cosine_similarity(candidate, refs)
+ 0.5 × mean_cosine_similarity(candidate, refs)
lexical_jsd = Jensen-Shannon divergence over character n-grams
(candidate vs. pooled reference corpus)
score = memsad_score + (lexical_weight × lexical_jsd)
The threshold is calibrated leave-one-out from the reference set:
calibration_scores = [score(note_i, all_others) for note_i in reference_notes]
threshold = mean(calibration_scores) + kappa × std_dev(calibration_scores)
A note is flagged when score > threshold. In audit mode the gate logs the decision and allows the write. In block or quarantine mode it raises MemoryAnomalyError.
Cosine similarity uses the 768-dimension fastembed embeddings stored in the note's embedding vector. Notes without a valid embedding vector bypass scoring with reason candidate_embedding_unavailable.
Short-circuit conditions¶
The gate returns action=audit, flagged=False without scoring when:
| Condition | Reason field |
|---|---|
enabled=false |
disabled |
Domain not in monitored_domains (list non-empty) |
domain_not_monitored |
| Candidate embedding vector missing or all-zero | candidate_embedding_unavailable |
Fewer than min_calibration_notes valid reference notes |
calibration_insufficient |
| Leave-one-out calibration produces no scores | calibration_unscorable |
Prompt injection detection¶
Every call to remember() runs require_no_prompt_injection(content) before content reaches the backend. Detection is deterministic regex-based matching — no LLM call. The check is always active when governance.enabled=true and has no separate config switch.
Seven pattern categories are detected, all at high severity:
| Category | What it matches |
|---|---|
direct_instruction_override |
"ignore/disregard/override/bypass … previous/system instructions" |
role_takeover |
"you are now / act as / pretend to be … system/admin/root" |
system_prompt_exfiltration |
"reveal/print/leak/dump … system/hidden prompt" |
secret_exfiltration |
"send/exfiltrate … api_key/token/secret/password" (within 80 chars) |
tool_instruction_smuggling |
"call/invoke/use/execute the tool/function/api/mcp/shell" |
retrieval_poisoning |
"when this note is retrieved …" or "always/only answer/respond/say" |
role_delimiter_smuggling |
Lines starting with system: / developer:, XML role tags, BEGIN_SYSTEM_PROMPT |
Any high-severity match raises PromptInjectionError, which GovernanceValidator converts to GovernanceViolationError. Content is not stored.
Exceptions¶
| Exception | Module | When raised |
|---|---|---|
GovernanceViolationError |
zettelforge.governance_validator |
Content size exceeded; structural validation failed; prompt injection detected; PII blocked (action=block). |
PromptInjectionError |
zettelforge.prompt_security |
High-confidence injection pattern detected. Converted to GovernanceViolationError by the validator. |
MemoryAnomalyError |
zettelforge.memory_defense |
Anomaly gate fires with mode=block or mode=quarantine. Contains a decision attribute with full MemoryAnomalyDecision fields. |
Minimal hardening config¶
Enable PII redaction and switch the anomaly gate to block mode once your store has 50+ calibration notes:
governance:
limits:
max_content_length: 1048576 # 1 MB per note
recall_timeout_seconds: 15.0
pii:
enabled: true
action: redact
memory_defense:
enabled: true
mode: block
kappa: 2.0
Or with environment variables:
export ZETTELFORGE_PII_ENABLED=true
export ZETTELFORGE_PII_ACTION=redact
export ZETTELFORGE_MEMORY_DEFENSE_MODE=block
export ZETTELFORGE_LIMITS_MAX_CONTENT_LENGTH=1048576
Do not switch memory_defense.mode to block before you have at least min_calibration_notes notes in the store. Before that threshold the gate always passes writes through and produces no calibration baseline.