Skip to content

Commit c9c7f82

Browse files
committed
chore: add pre-commit config and fix terminology violations
Add .pre-commit-config.yaml matching edictum core setup: - ruff (lint + format) - check-terminology (enforce .docs-style-guide.md) Terminology script adapted for console codebase: - allowlist assignment rule `rule_id` (different concept from contract rules) - allowlist network/SSRF "blocked" (infrastructure term, not verdict) - scan .ts/.tsx files in dashboard/src/ Fix 8 terminology violations: - "blocked" → "denied" in UI labels, test fixtures, comments - "shadow mode" → "observe mode" in contract tooltips - "Blocked endpoint" → "Denied endpoint" in contract template
1 parent 941b05b commit c9c7f82

File tree

10 files changed

+263
-8
lines changed

10 files changed

+263
-8
lines changed

.docs-style-guide.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Edictum Docs Style & Terminology Guide
2+
3+
This is the binding reference for all documentation writers. Every page must use these terms consistently.
4+
5+
## Canonical Terms (USE THESE, NOT THE ALTERNATIVES)
6+
7+
| Concept | Canonical Term | DO NOT USE |
8+
|---------|---------------|------------|
9+
| The YAML constructs that define rules | **contract** / **contracts** | policies, rules, guards, checks |
10+
| What Edictum does to tool calls | **enforces contracts** | governs, guards, protects, secures |
11+
| When a contract blocks a call | **denied** / **deny** | blocked, rejected, prevented, stopped |
12+
| When a contract allows a call | **allowed** / **allow** | passed, approved, permitted |
13+
| The runtime check sequence | **pipeline** | engine, evaluator, processor, middleware |
14+
| The sequence: preconditions -> execute -> postconditions -> audit | **pipeline** (describe the steps, don't rename them) | workflow, chain, flow |
15+
| What agents do that Edictum checks | **tool call** / **tool calls** | function call, action, operation, invocation |
16+
| The thin framework-specific integration layer | **adapter** / **adapters** | integration, plugin, connector, driver |
17+
| Shadow-testing without blocking | **observe mode** | shadow mode, dry run, passive mode, monitor mode |
18+
| The identity context on a tool call | **principal** | user, identity, caller, actor |
19+
| The structured output from postconditions | **finding** / **findings** | result, detection, alert, violation |
20+
| The YAML file containing contracts | **contract bundle** | policy file, rule file, config |
21+
| What Edictum IS | **runtime contract enforcement for agent tool calls** | governance framework, safety library, guardrails |
22+
23+
## The One-Liner
24+
25+
Use this exact framing (or close paraphrase) when describing what Edictum is:
26+
27+
> Edictum enforces contracts on AI agent tool calls -- preconditions before execution, sandbox allowlists for file paths and commands, postconditions after, session limits across turns, and a full audit trail. Contracts are YAML. Enforcement is deterministic. The agent cannot bypass it.
28+
29+
## The Core Metaphor
30+
31+
Edictum sits at the **decision-to-action seam**. The agent decides to call a tool. Before that call executes, Edictum checks it against contracts. This is a hard boundary, not a suggestion.
32+
33+
DO NOT use metaphors like: gatekeeper, guardian, shield, firewall, sentinel, watchdog.
34+
DO use: "hard boundary," "enforcement point," "the check between decision and action."
35+
36+
## Writing Rules
37+
38+
1. **Lead with the problem, then the solution.** Not "Edictum has X" but "Agents do Y bad thing. Edictum prevents this by..."
39+
2. **Show, don't describe.** Every concept page: working example within the first screen.
40+
3. **No marketing language.** No "powerful," "seamless," "revolutionary," "robust," "elegant." Just say what it does.
41+
4. **Short paragraphs.** 2-3 sentences max.
42+
5. **Code examples must be copy-pasteable.** If it doesn't work when pasted, delete it.
43+
6. **Deterministic, not probabilistic.** Emphasize that contracts are code, not suggestions. The LLM cannot talk its way past a contract.
44+
45+
## Pipeline Description (USE THIS CONSISTENTLY)
46+
47+
When describing what happens on every tool call:
48+
49+
1. Agent decides to call a tool
50+
2. Edictum evaluates **preconditions** against the call's arguments and principal
51+
3. If any precondition fails: **deny** (call never executes)
52+
4. Edictum evaluates **sandbox contracts** against allowlist boundaries (file paths, commands, domains)
53+
5. If the call falls outside any sandbox boundary: **deny** or **pending_approval** (depending on `outside` setting)
54+
6. If all pass: tool executes
55+
7. Edictum evaluates **postconditions** against the tool's output
56+
8. Postcondition failures produce **findings**. With `effect: warn`, the tool result is unchanged. With `effect: redact` or `effect: deny`, the pipeline modifies the output for READ/PURE tools (WRITE/IRREVERSIBLE tools fall back to warn)
57+
9. **Audit event** is emitted for every evaluation (allowed, denied, or observed)
58+
59+
Session limits (max calls, per-tool caps, attempt limits) are checked as part of steps 2-5.
60+
61+
## What Edictum is NOT (be honest about these)
62+
63+
- NOT prompt engineering or input guardrails (those filter what goes INTO the LLM)
64+
- NOT output content filtering (that filters what comes OUT of the LLM)
65+
- NOT an authentication/authorization system (it accepts a Principal but doesn't authenticate)
66+
- NOT ML-based detection (contracts are deterministic pattern matching)
67+
- NOT a proxy or network-level tool (it's an in-process library)
68+
69+
## Page Structure Pattern
70+
71+
Every page should follow:
72+
73+
1. **Opening**: 1-2 sentences stating the problem this page addresses
74+
2. **Example**: Working code/YAML within the first visible screen
75+
3. **When to use this**: Concrete scenarios where this feature applies — real situations a user would recognize (e.g., "Your fintech agent needs to limit daily transaction approvals" not "This feature provides configurable limits"). Include:
76+
- 2-4 concrete scenarios with brief descriptions
77+
- Which user persona benefits (developer debugging vs. platform team in production)
78+
- How this relates to other Edictum features (e.g., "Use callbacks for immediate reactions, OTel for historical dashboards")
79+
4. **Explanation**: How it works, why it matters
80+
5. **Reference**: Full details, edge cases, configuration
81+
6. **Next steps**: Links to related pages
82+
83+
## Cross-Reference Conventions
84+
85+
- Link to concepts pages for explanations: `[contracts](../concepts/contracts.md)`
86+
- Link to reference pages for syntax: `[YAML reference](../contracts/yaml-reference.md)`
87+
- Link to adapter pages by name: `[LangChain adapter](../adapters/langchain.md)`
88+
- Always use relative paths for internal links

.pre-commit-config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: 89c421dff2e1026ba12cdb9ebd731f4a83aa8021 # v0.8.6
4+
hooks:
5+
- id: ruff
6+
args: [--fix]
7+
- id: ruff-format
8+
9+
- repo: local
10+
hooks:
11+
- id: check-terminology
12+
name: check-terminology
13+
entry: python scripts/check-terminology.py
14+
language: python
15+
types_or: [python, markdown, yaml, json, ts, tsx]
16+
pass_filenames: true

dashboard/src/pages/contracts/contract-detail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function ContractDetail({ contract, coverage }: ContractDetailProps) {
9292
)}
9393
{contract.not_allows?.domains && (
9494
<>
95-
<span className="font-medium text-muted-foreground">blocked</span>
95+
<span className="font-medium text-muted-foreground">denied</span>
9696
<span className="text-red-600 dark:text-red-400">{contract.not_allows.domains.join(", ")}</span>
9797
</>
9898
)}

dashboard/src/pages/contracts/contract-tooltips.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const EFFECT_TOOLTIPS: Record<string, { title: string; description: strin
6363
},
6464
observe: {
6565
title: "observe",
66-
description: "Logs the event without blocking. Used for monitoring in shadow mode.",
66+
description: "Logs the event without denying. Used for monitoring in observe mode.",
6767
href: `${DOCS}/contracts/effects#observe`,
6868
},
6969
}

dashboard/src/pages/contracts/templates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ contracts:
4343
- 'metadata\\.google\\.internal'
4444
then:
4545
effect: deny
46-
message: "Blocked endpoint: {args.url}"
46+
message: "Denied endpoint: {args.url}"
4747
tags: [security, ssrf]
4848
4949
- id: pii-in-output

scripts/check-terminology.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
"""Pre-commit hook: enforce .docs-style-guide.md terminology.
3+
4+
Scans staged files for banned terms and reports violations.
5+
Exit 0 = clean, exit 1 = violations found.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import re
11+
import sys
12+
from pathlib import Path
13+
14+
# Banned patterns: (regex, replacement hint, description)
15+
BANNED_PATTERNS: list[tuple[re.Pattern, str, str]] = [
16+
(re.compile(r"\bshadow mode\b", re.IGNORECASE), "observe mode", "banned phrase"),
17+
(re.compile(r"\bper-rule\b", re.IGNORECASE), "per-contract", "banned phrase"),
18+
(re.compile(r"\bRuleResult\b"), "ContractResult", "old class name"),
19+
(re.compile(r"\brule_id\b"), "contract_id", "old field name"),
20+
(re.compile(r"\brule_type\b"), "contract_type", "old field name"),
21+
(re.compile(r'"\brules_evaluated\b"'), '"contracts_evaluated"', "old field name"),
22+
(re.compile(r"\bby rule\b", re.IGNORECASE), "by contract", "banned phrase"),
23+
(re.compile(r"\bRules evaluated\b"), "Contracts evaluated", "banned CLI string"),
24+
(re.compile(r"\ball rules passed\b", re.IGNORECASE), "all contracts passed", "banned CLI string"),
25+
]
26+
27+
# "blocked" needs special handling — allow infrastructure/network uses
28+
BLOCKED_PATTERN = re.compile(r"\bblocked\b", re.IGNORECASE)
29+
BLOCKED_ALLOWLIST = {
30+
# builtins.py loop variable: "for blocked in commands:"
31+
"for blocked in commands",
32+
"cmd == blocked",
33+
"cmd.startswith(blocked",
34+
# f-string references to the loop variable
35+
"{blocked}",
36+
# Network/SSRF security — "blocked address", "blocked network" is infra terminology
37+
"blocked address",
38+
"blocked network",
39+
"blocked:", # e.g. "Request to X blocked: resolves to Y"
40+
}
41+
42+
# Files/dirs to skip
43+
SKIP_PATHS = {
44+
".docs-style-guide.md",
45+
"scripts/check-terminology.py",
46+
"CLAUDE.md", # references banned terms when defining the enforcement rules
47+
}
48+
SKIP_DIRS = {
49+
".git",
50+
"__pycache__",
51+
".ruff_cache",
52+
"node_modules",
53+
"dashboard/dist",
54+
}
55+
56+
# Files where `rule_id` is legitimate — assignment rules are a different concept
57+
# from contract rules. `rule_id` here means "assignment rule ID", not "contract rule ID".
58+
RULE_ID_ALLOWLIST_PATHS = {
59+
"assignment_rules",
60+
"assignment_service",
61+
"agent_registrations",
62+
"test_agent_assignment",
63+
"agents", # dashboard API type includes assignment rule_id in agent response
64+
}
65+
66+
# Only check these extensions
67+
CHECK_EXTENSIONS = {".py", ".md", ".yaml", ".yml", ".json", ".ts", ".tsx"}
68+
69+
70+
def should_skip(path: Path) -> bool:
71+
path_str = str(path)
72+
if path_str in SKIP_PATHS:
73+
return True
74+
for skip_dir in SKIP_DIRS:
75+
if path_str.startswith(skip_dir):
76+
return True
77+
if path.suffix not in CHECK_EXTENSIONS:
78+
return True
79+
return False
80+
81+
82+
def check_file(path: Path) -> list[str]:
83+
violations: list[str] = []
84+
try:
85+
lines = path.read_text().splitlines()
86+
except (OSError, UnicodeDecodeError):
87+
return []
88+
89+
is_changelog = path.name == "CHANGELOG.md"
90+
91+
for i, line in enumerate(lines, 1):
92+
# Skip CHANGELOG lines that document renames (backtick-quoted old names)
93+
if is_changelog and "`" in line and "→" in line:
94+
continue
95+
96+
# Check banned patterns
97+
for pattern, fix, desc in BANNED_PATTERNS:
98+
if pattern.search(line):
99+
# Allow rule_id in assignment rule files (different concept)
100+
if fix == "contract_id" and any(
101+
a in path.stem for a in RULE_ID_ALLOWLIST_PATHS
102+
):
103+
continue
104+
violations.append(f" {path}:{i}: {desc} — use '{fix}' instead")
105+
violations.append(f" {line.strip()}")
106+
107+
# Check "blocked" with allowlist
108+
if BLOCKED_PATTERN.search(line):
109+
line_stripped = line.strip()
110+
if not any(allowed in line_stripped for allowed in BLOCKED_ALLOWLIST):
111+
violations.append(f" {path}:{i}: 'blocked' — use 'denied' instead")
112+
violations.append(f" {line_stripped}")
113+
114+
return violations
115+
116+
117+
def main() -> int:
118+
# If args are passed, check those files (pre-commit passes staged files)
119+
# Otherwise, scan src/, tests/, dashboard/src/
120+
if len(sys.argv) > 1:
121+
files = [Path(f) for f in sys.argv[1:]]
122+
else:
123+
files = []
124+
for directory in ["src", "tests", "dashboard/src"]:
125+
d = Path(directory)
126+
if d.exists():
127+
files.extend(d.rglob("*"))
128+
for extra in ["CHANGELOG.md", "README.md"]:
129+
p = Path(extra)
130+
if p.exists():
131+
files.append(p)
132+
133+
all_violations: list[str] = []
134+
for f in files:
135+
if not f.is_file() or should_skip(f):
136+
continue
137+
violations = check_file(f)
138+
all_violations.extend(violations)
139+
140+
if all_violations:
141+
print("Terminology violations found (see .docs-style-guide.md):\n")
142+
for v in all_violations:
143+
print(v)
144+
print(f"\n{len(all_violations) // 2} violation(s) found.")
145+
return 1
146+
147+
return 0
148+
149+
150+
if __name__ == "__main__":
151+
sys.exit(main())

src/edictum_server/ai/system_prompt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
equals: production
7979
then:
8080
effect: deny
81-
message: "Blocked: cannot email competitor domains in production"
81+
message: "Denied: cannot email competitor domains in production"
8282
```
8383
8484
### Pre-contract: Block all exec() calls

tests/test_adversarial/test_s1_session_bypass.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async def test_tampered_session_payload(
119119
except Exception:
120120
# Server raised an unhandled error -- still means no access granted.
121121
# This is an implementation issue (should catch JSONDecodeError in
122-
# authenticate), but from a security perspective the attacker is blocked.
122+
# authenticate), but from a security perspective the attacker is denied.
123123
pass
124124

125125

tests/test_audit_fixes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _make_event(call_id: str = "call-1", **overrides: object) -> dict:
2929
"verdict": "deny",
3030
"mode": "enforce",
3131
"timestamp": "2026-02-18T12:00:00Z",
32-
"payload": {"reason": "blocked"},
32+
"payload": {"reason": "denied"},
3333
}
3434
base.update(overrides)
3535
return base
@@ -249,7 +249,7 @@ async def test_stats_handles_null_decision_name(
249249
_make_event("no-decision", payload={"reason": "no contract"}, timestamp=now),
250250
_make_event(
251251
"with-decision",
252-
payload={"decision_name": "test-contract", "reason": "blocked"},
252+
payload={"decision_name": "test-contract", "reason": "denied"},
253253
timestamp=now,
254254
),
255255
]

tests/test_events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def _make_event(call_id: str = "call-1") -> dict:
1313
"verdict": "deny",
1414
"mode": "enforce",
1515
"timestamp": "2026-02-18T12:00:00Z",
16-
"payload": {"reason": "blocked"},
16+
"payload": {"reason": "denied"},
1717
}
1818

1919

0 commit comments

Comments
 (0)