diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4ec03d1c..a484ab92 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,114 +1,90 @@ # Edictum Architecture -> Runtime safety for AI agents. Stop agents before they break things. +> Developer agent behavior platform for Python. Rulesets, workflow gates, adapters, and decision logs around one deterministic pipeline. -## What It Does +## Shape Of The Repo -Edictum sits between an agent's decision to call a tool and actual execution. It enforces rules, hooks, audit trails, and operation limits. When a rule is violated, it tells the agent **why** so it can self-correct. +Edictum has two Python layers: -## Package Structure +- `src/edictum/` is the standalone core library. It loads rulesets, evaluates tool calls, tracks local session state, runs workflow gates, and emits decision logs. +- `src/edictum/server/` is the server SDK. It implements the same core protocols over HTTP so a Python agent can use a remote approval backend, remote rule source, remote session store, and remote log destination. -``` -src/edictum/ -├── __init__.py # Edictum class, guard.run(), exceptions, re-exports -├── envelope.py # ToolCall (frozen), SideEffect, ToolRegistry, BashClassifier -├── hooks.py # HookDecision (ALLOW/DENY) -├── rules.py # Decision, @precondition, @postcondition, @session_contract -├── limits.py # OperationLimits (attempt + execution + per-tool caps) -├── pipeline.py # CheckPipeline — single source of governance logic -├── session.py # Session (atomic counters via StorageBackend) -├── storage.py # StorageBackend protocol + MemoryBackend -├── audit.py # AuditEvent, RedactionPolicy, Stdout/File sinks -├── telemetry.py # OpenTelemetry (graceful no-op if absent) -├── builtins.py # deny_sensitive_reads() -├── types.py # Internal types (HookRegistration, ToolConfig) -└── adapters/ - ├── langchain.py # LangChain adapter (pre/post tool call hooks) - ├── crewai.py # CrewAI adapter (before/after hooks) - ├── agno.py # Agno adapter (wrap-around hook) - ├── semantic_kernel.py # Semantic Kernel adapter (filter pattern) - ├── openai_agents.py # OpenAI Agents SDK adapter (guardrails) - └── claude_agent_sdk.py # Claude Agent SDK adapter (hook dict) -``` +The server itself is a separate deployment. This repo ships the core library and the Python server client. -## The Flow +## Package Layout -Every tool call passes through: - -``` -Agent decides to call tool - │ - ▼ -Adapter creates ToolCall (deep-copied, classified) -Increments attempt_count (BEFORE governance) - │ - ▼ -Pipeline.pre_execute() — 5 steps: - 1. Attempt limit (>= max_attempts?) - 2. Before hooks (user-defined, can DENY) - 3. Checks (rule checks, can BLOCK) - 4. Session rules (cross-turn state, can BLOCK) - 5. Execution limits (>= max_tool_calls? per-tool?) - │ - ├── BLOCK → audit event → tell agent why → agent self-corrects - │ - └── ALLOW → tool executes - │ - ▼ - Pipeline.post_execute(): - 1. Postconditions (observe-only, warnings) - 2. After hooks - 3. Session record (exec count, consecutive failures) - │ - ▼ - Audit event (CALL_EXECUTED or CALL_FAILED) +```text +src/edictum/ +├── __init__.py +├── _guard.py # Edictum class and public construction paths +├── _runner.py # guard.run() execution path +├── _factory.py # from_yaml(), from_template(), from_multiple() +├── rules.py # Python rule decorators +├── pipeline.py # Deterministic pre/post evaluation pipeline +├── envelope.py # ToolCall model, tool registry, bash classifier +├── session.py # Local session counters and state helpers +├── approval.py # Human approval backends +├── audit.py # Decision log event model and log destinations +├── evaluation.py # Dry-run evaluation results +├── findings.py # Structured post-rule violations +├── workflow/ # WorkflowDefinition, WorkflowRuntime, loaders, evaluators +├── yaml_engine/ # Ruleset schema, loader, composer, compiler, templates +├── adapters/ +│ ├── langchain.py +│ ├── crewai.py +│ ├── agno.py +│ ├── semantic_kernel.py +│ ├── openai_agents.py +│ ├── claude_agent_sdk.py +│ ├── google_adk.py +│ └── nanobot.py +├── gate/ # Coding assistant hook runtime +├── server/ # HTTP-backed SDK implementations +├── skill/ # Skill scanning and risk analysis +└── telemetry.py / otel.py # Optional OpenTelemetry integration ``` -## Key Design Decisions +## Runtime Flow -**Pipeline owns ALL governance logic.** Adapters are thin translation layers. Adding a second adapter doesn't fork governance behavior. +1. A framework adapter, Gate hook, or direct `guard.run()` call creates a `ToolCall`. +2. `CheckPipeline.pre_execute()` evaluates before-hooks, pre rules, sandbox rules, session rules, approvals, and workflow stage rules. +3. If the decision is `block`, execution stops and a decision log event is emitted. If the decision is `ask`, execution pauses for approval. If the decision is `allow`, the tool runs. +4. `CheckPipeline.post_execute()` evaluates post rules, records workflow evidence, updates session counters, and emits the final decision log event. -**Two counter types:** -- `max_attempts` — caps ALL PreToolUse events (including blocked). Catches block loops. -- `max_tool_calls` — caps executions only (PostToolUse). Caps total work done. +The pipeline is the single source of truth. Adapters are translation layers. They should not contain separate rule logic. -**Postconditions are observe-only.** They emit warnings, never block. For pure/read tools: suggest retry. For write/irreversible: warn only. +## Workflow Module -**Observe mode** (`mode="observe"`): full pipeline runs, audit emits `CALL_WOULD_DENY`, but tool executes anyway. +The workflow runtime is separate from one-shot rules because it tracks process across calls: -**Zero runtime deps.** OpenTelemetry via optional `edictum[otel]`. +- `workflow/definition.py` validates `kind: Workflow` documents. +- `workflow/load.py` parses workflow YAML from files or strings. +- `workflow/runtime.py` manages stage state, entry gates, exit gates, command checks, and approvals. +- `workflow/runtime_eval.py` evaluates a live `ToolCall` against the active workflow stage. +- `workflow/evaluator_exec.py` provides the opt-in trusted `exec(...)` evaluator for stage conditions. -**Redaction at write time.** Destructive by design — no recovery. Sensitive keys, secret value patterns (OpenAI/AWS/JWT/GitHub/Slack), 32KB payload cap. +Workflow state is attached to the same session model the rules pipeline uses, so stage moves, approvals, evidence, and tool-call decisions stay aligned. -**BashClassifier is a heuristic, not a security boundary.** Conservative READ allowlist + shell operator detection. Defense in depth with `deny_sensitive_reads()`. +## Adapters -## Usage Modes +The Python SDK ships eight adapters: -**1. Framework-agnostic (`guard.run()`):** -```python -guard = Edictum(rules=[deny_sensitive_reads()]) -result = await guard.run("Bash", {"command": "ls"}, my_bash_fn) -``` - -**2. Framework adapters (6 supported):** - -All adapters are thin translation layers — governance logic stays in the pipeline. +- `LangChainAdapter` +- `CrewAIAdapter` +- `AgnoAdapter` +- `SemanticKernelAdapter` +- `OpenAIAgentsAdapter` +- `ClaudeAgentSDKAdapter` +- `GoogleADKAdapter` +- `NanobotAdapter` -```python -from edictum.adapters.langchain import LangChainAdapter -from edictum.adapters.crewai import CrewAIAdapter -from edictum.adapters.agno import AgnoAdapter -from edictum.adapters.semantic_kernel import SemanticKernelAdapter -from edictum.adapters.openai_agents import OpenAIAgentsAdapter -from edictum.adapters.claude_agent_sdk import ClaudeAgentSDKAdapter - -adapter = LangChainAdapter(guard, session_id="session-1") -``` +Each adapter maps its framework's tool-call lifecycle onto the same core pipeline. The adapter owns translation. The pipeline owns decisions. -## What This Is NOT +## Design Notes -- Not prompt injection defense -- Not content safety filtering -- Not network egress control -- Not a security boundary for Bash -- Not concurrency-safe across workers (MemoryBackend is single-process) +- Core stays standalone. No runtime network dependency is required for local enforcement. +- YAML rulesets and Python decorators both compile to the same runtime model. +- Workflow gates are explicit opt-in stateful process enforcement on top of per-call rules. +- Decision logs are structured and redact sensitive values before emission. +- OpenTelemetry is optional. If the dependency is missing, tracing is a no-op. +- The `gate/` package is the coding-assistant runtime layer, not a separate rules engine. diff --git a/README.md b/README.md index f10c95f2..478a5056 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,35 @@ - -# Edictum +# edictum + +> Declarative behavior rules for AI agents. Write rules in YAML. Enforce them at runtime. [![PyPI](https://img.shields.io/pypi/v/edictum?cacheSeconds=3600)](https://pypi.org/project/edictum/) +[![Tests](https://github.com/edictum-ai/edictum/actions/workflows/ci.yml/badge.svg)](https://github.com/edictum-ai/edictum/actions/workflows/ci.yml) [![License](https://img.shields.io/pypi/l/edictum?cacheSeconds=86400)](LICENSE) [![Python](https://img.shields.io/pypi/pyversions/edictum?cacheSeconds=86400)](https://pypi.org/project/edictum/) -[![CI](https://github.com/edictum-ai/edictum/actions/workflows/ci.yml/badge.svg)](https://github.com/edictum-ai/edictum/actions/workflows/ci.yml) -[![Downloads](https://img.shields.io/pypi/dm/edictum)](https://pypi.org/project/edictum/) -[![arXiv](https://img.shields.io/badge/arXiv-2602.16943-b31b1b.svg)](https://arxiv.org/abs/2602.16943) - -Runtime rule enforcement for AI agent tool calls. -**Prompts are suggestions. Rules are enforcement.** The LLM cannot talk its way past a rule. +## The Problem -**55us overhead** · **18 adapters across Python, TypeScript, Go** · **Zero runtime deps** · **Fail-closed by default** +`CLAUDE.md` is a wish list. The GAP benchmark found a 55-79% gap between what frontier agents refuse in text and what they still do through tool calls. Your agent says "I won't do that" and then does it anyway. -```bash -pip install edictum[yaml] -``` +Edictum intercepts the tool call, not the model's explanation. You write the rule once in YAML, then enforce it at runtime across frameworks. ## Quick Start -Deny first -- see enforcement before writing YAML: - -```python -from edictum import Edictum, EdictumDenied +Install YAML support: -guard = Edictum.from_template("file-agent") -result = guard.evaluate("read_file", {"path": ".env"}) -print(result.decision) # "block" -print(result.block_reasons[0]) # "Sensitive file '.env' blocked." +```bash +pip install edictum[yaml] ``` -Full path -- your rule, your enforcement: +Requires Python 3.11+. -```python -guard = Edictum.from_yaml("rules.yaml") +Core-only install stays zero-dependency: -try: - result = await guard.run("read_file", {"path": ".env"}, read_file) -except EdictumDenied as e: - print(e.reason) # "Sensitive file '.env' blocked." +```bash +pip install edictum ``` -`rules.yaml`: +1. Write a ruleset. ```yaml apiVersion: edictum/v1 @@ -54,202 +41,152 @@ defaults: rules: - id: block-sensitive-reads type: pre - tool: read_file + tool: Read when: - args.path: - contains_any: [".env", ".secret", "credentials", ".pem", "id_rsa"] + args.file_path: + contains_any: [".env", ".pem", "id_rsa", "credentials"] then: action: block - message: "Sensitive file '{args.path}' blocked." + message: "Sensitive file '{args.file_path}' is blocked." ``` -Rules are YAML. Enforcement is deterministic -- no LLM in the evaluation path, just pattern matching against tool names and arguments. The agent cannot bypass a matched rule. Rule errors, type mismatches, and missing fields all fail closed (block). Tool calls with no matching rules are allowed by default -- add a catch-all `tool: "*"` rule for block-by-default. - -## The Problem - -An agent says "I won't read sensitive files" -- then calls `read_file(".env")` and leaks your API keys. - -A DevOps agent recognizes a jailbreak attempt, writes "I should NOT comply" in its reasoning -- then reads four production database credentials in the next tool call. - -Prompt engineering doesn't fix this. You need enforcement at the tool-call layer. - -## Works With Your Framework - -| Framework | Adapter | Integration | -|-----------|---------|-------------| -| LangChain + LangGraph | `LangChainAdapter` | `as_tool_wrapper()` / `as_middleware()` | -| OpenAI Agents SDK | `OpenAIAgentsAdapter` | `as_guardrails()` | -| Claude Agent SDK | `ClaudeAgentSDKAdapter` | `to_hook_callables()` | -| CrewAI | `CrewAIAdapter` | `register()` | -| Agno | `AgnoAdapter` | `as_tool_hook()` | -| Semantic Kernel | `SemanticKernelAdapter` | `register()` | -| Google ADK | `GoogleADKAdapter` | `as_plugin()` / `as_agent_callbacks()` | -| Nanobot | `NanobotAdapter` | `wrap_registry()` | +2. Load it. ```python -# LangChain -from edictum.adapters.langchain import LangChainAdapter -adapter = LangChainAdapter(guard) -tool = adapter.as_tool_wrapper(tool) - -# OpenAI Agents SDK -from edictum.adapters.openai_agents import OpenAIAgentsAdapter -adapter = OpenAIAgentsAdapter(guard) -input_gr, output_gr = adapter.as_guardrails() - -# Claude Agent SDK -from edictum.adapters.claude_agent_sdk import ClaudeAgentSDKAdapter -adapter = ClaudeAgentSDKAdapter(guard) -hooks = adapter.to_hook_callables() - -# Google ADK -from edictum.adapters.google_adk import GoogleADKAdapter -adapter = GoogleADKAdapter(guard) -plugin = adapter.as_plugin() -``` - -See [Adapter docs](https://docs.edictum.ai/docs/adapters/overview) for all 8 frameworks. - -## What You Can Do - -**Rules** -- four types covering the full tool call lifecycle: - -- **Preconditions** block dangerous calls before execution -- **Postconditions** scan tool output -- warn, redact PII, or block -- **Session rules** cap total calls, per-tool calls, and retry attempts -- **Sandbox rules** allowlist file paths, commands, and domains - -**Principal-aware enforcement** -- role-gate tools with claims and `env.*` context. `set_principal()` for mid-session role changes. - -**Callbacks** -- block/allow lifecycle callbacks for logging, alerting, or approval workflows. - -**Test and validate:** - -- `guard.evaluate()` -- dry-run without executing the tool -- Load rules in tests and assert decisions directly from Python -- For CLI workflows, use the Go binary in [edictum-go](https://github.com/edictum-ai/edictum-go) -- that is the canonical Edictum CLI - -**Ship safely:** +from edictum import Edictum -- Observe mode -- log what would be blocked, then enforce -- Multi-file composition with deterministic merge -- Custom YAML operators and selectors -- For CLI-based diff/replay workflows, use the Go binary in [edictum-go](https://github.com/edictum-ai/edictum-go) - -**Audit and observability:** - -- Structured audit events on every evaluation -- OpenTelemetry spans and metrics -- Secret values auto-redacted in audit events -- File, stdout, and composite sinks +guard = Edictum.from_yaml("rules.yaml") +``` -## Built-in Templates +3. Wrap the tool call. ```python -guard = Edictum.from_template("file-agent") -# Blocks .env, .pem, credentials, id_rsa reads. Blocks rm -rf, chmod 777, destructive shell commands. +from edictum import EdictumDenied + -guard = Edictum.from_template("research-agent") -# Postcondition PII scanning on tool output. Session limits (100 calls, 20 per tool). +async def read_file(file_path: str) -> str: + with open(file_path) as f: + return f.read() -guard = Edictum.from_template("devops-agent") -# Role gates (only ops principal can deploy). Ticket ID required. Bash command safety. -guard = Edictum.from_template("nanobot-agent") -# Approval gates for exec/spawn/cron/MCP. Workspace path restrictions. Session limits. +try: + result = await guard.run( + "Read", + {"file_path": ".env"}, + read_file, + ) +except EdictumDenied as exc: + print(exc.reason) ``` -## Edictum Gate +## Workflow Gates -Pre-execution governance for coding assistants. Sits between the assistant and the OS, evaluating every tool call against rules. +Rules block one tool call at a time. Workflow gates enforce process across a session: what has been read, which stage is active, which commands are allowed, and where human approval is required. -```bash -pip install edictum[gate] +```yaml +apiVersion: edictum/v1 +kind: Workflow +metadata: + name: ship-feature + description: "Read, implement, verify, review" + version: "1.0" + +stages: + - id: read-context + description: "Read the spec" + tools: [Read] + exit: + - condition: file_read("specs/feature.md") + message: "Read the spec first" + + - id: implement + description: "Make the change" + entry: + - condition: stage_complete("read-context") + tools: [Edit, Write] + + - id: local-verify + description: "Run tests" + entry: + - condition: stage_complete("implement") + tools: [Bash] + checks: + - command_matches: "^pytest tests/ -q$" + message: "Only the test command is allowed here" + exit: + - condition: command_matches("^pytest tests/ -q$") + message: "Run the test command before moving on" + + - id: review + description: "Pause for approval" + entry: + - condition: stage_complete("local-verify") + approval: + message: "Human approval required before push" ``` -The Python package ships the Gate library and integrations. For command-line workflows, use the Go binary in [edictum-go](https://github.com/edictum-ai/edictum-go) -- that is the canonical Edictum CLI. +## Rules Engine -Supports Claude Code, Cursor, Copilot CLI, Gemini CLI, and OpenCode. Self-protection rules prevent the assistant from disabling governance. Optional sync to [Edictum Console](https://github.com/edictum-ai/edictum-console) for centralized audit. +The rules engine is deterministic, model-agnostic, and cheap enough to sit on every tool call. -See the [Gate guide](https://docs.edictum.ai/docs/guides/gate) for setup. - -## Edictum Console - -Optional self-hostable operations console for governed agents. Rule management, live hot-reload via SSE, human-in-the-loop approvals, audit event feeds, and fleet monitoring. +```yaml +apiVersion: edictum/v1 +kind: Ruleset +metadata: + name: command-rules +defaults: + mode: enforce +rules: + - id: block-force-push + type: pre + tool: Bash + when: + args.command: + matches_any: + - 'git\\s+push\\s+.*--force' + - 'git\\s+push\\s+-f\\b' + then: + action: block + message: "Force push is blocked." -```python -guard = await Edictum.from_server( - url="http://localhost:8000", - api_key="edk_production_...", - agent_id="my-agent", -) + - id: pii-in-output + type: post + tool: "*" + when: + output.text: + matches_any: + - '\\b\\d{3}-\\d{2}-\\d{4}\\b' + then: + action: warn + message: "PII pattern detected in output. Redact before using." ``` -See [edictum-console](https://github.com/edictum-ai/edictum-console) for deployment. - -## Research & Real-World Impact - -Edictum was evaluated across six regulated domains in the GAP benchmark (6 LLMs, 17,420 datapoints). +Edictum supports pre rules, post rules, session rules, sandbox rules, workflow gates, and `action: ask` approvals. -[Paper (arXiv:2602.16943)](https://arxiv.org/abs/2602.16943) +## Adapters -Used to audit [OpenClaw](https://github.com/OpenClaw)'s 36,000-skill registry -- found live C2 malware on first scan. +| Framework | Python adapter | Integration | +| --- | --- | --- | +| LangChain / LangGraph | `LangChainAdapter` | `as_middleware()`, `as_tool_wrapper()` | +| CrewAI | `CrewAIAdapter` | `register()` | +| Agno | `AgnoAdapter` | `as_tool_hook()` | +| Semantic Kernel | `SemanticKernelAdapter` | `register()` | +| OpenAI Agents SDK | `OpenAIAgentsAdapter` | `as_guardrails()` | +| Claude Agent SDK | `ClaudeAgentSDKAdapter` | `to_hook_callables()` | +| Google ADK | `GoogleADKAdapter` | `as_plugin()`, `as_agent_callbacks()` | +| Nanobot | `NanobotAdapter` | `wrap_registry()` | -For CLI-based scanning and other command-line workflows, use the Go binary in [edictum-go](https://github.com/edictum-ai/edictum-go). +## How It Works -## Install +Edictum sits at the tool-call boundary. Adapters translate framework-specific tool events into a shared `ToolCall`, the pipeline evaluates rules and workflow state with deterministic checks, and the decision is applied before or after execution. No LLM is involved in enforcement, and the same ruleset can follow the agent across frameworks. -Requires Python 3.11+. +## Research -```bash -pip install edictum # core (zero deps) -pip install edictum[yaml] # + YAML rule parsing -pip install edictum[otel] # + OpenTelemetry span emission -pip install edictum[gate] # + coding assistant governance library -pip install edictum[verified] # + Ed25519 bundle signature verification -pip install edictum[server] # + server SDK (connect to Edictum Console) -pip install edictum[all] # everything in this Python package -``` +Edictum is built on the GAP benchmark: 17,420 datapoints across 6 frontier models showing a 55-79% gap between text refusal and tool-call execution. Existing guardrails mostly inspect what the model says. Edictum focuses on what the agent does. -For CLI workflows, use the Go binary in [edictum-go](https://github.com/edictum-ai/edictum-go). - -## How It Compares - -| Approach | Scope | Runtime enforcement | Audit trail | -|---|---|---|---| -| Prompt/output guardrails | Input/output text | No -- advisory only | No | -| API gateways / MCP proxies | Network transport | Yes -- at the proxy | Partial | -| Security scanners | Post-hoc analysis | No -- detection only | Yes | -| Manual if-statements | Per-tool, ad hoc | Yes -- scattered logic | No | -| **Edictum** | **Tool call rules** | **Yes -- deterministic pipeline** | **Yes -- structured + redacted** | - -## Use Cases - -| Domain | What Edictum enforces | -|--------|----------------------| -| Coding agents | Secret protection, destructive command denial, write scope ([Gate guide](https://docs.edictum.ai/docs/guides/gate)) | -| Healthcare | Patient data access control, role-gated queries | -| Finance | PII redaction in query results, transaction limits | -| DevOps | Production deploy gates, ticket requirements, bash safety | -| Education | Student data protection, session limits per assignment | -| Legal | Privileged document access, audit trail for compliance | - -## Ecosystem - -| Repo | Language | What it does | -|------|----------|-------------| -| [edictum](https://github.com/edictum-ai/edictum) | Python | Core library -- this repo | -| [edictum-ts](https://github.com/edictum-ai/edictum-ts) | TypeScript | Core + adapters (Claude SDK, LangChain, OpenAI Agents, OpenClaw, Vercel AI) | -| [edictum-go](https://github.com/edictum-ai/edictum-go) | Go | Core + adapters (ADK Go, Anthropic, Eino, Genkit, LangChain Go) | -| [edictum-console](https://github.com/edictum-ai/edictum-console) | Python + React | Self-hostable ops console: HITL, audit, fleet monitoring | -| [edictum-schemas](https://github.com/edictum-ai/edictum-schemas) | JSON Schema | Rule bundle schema + cross-SDK conformance fixtures | -| [edictum-demo](https://github.com/edictum-ai/edictum-demo) | Python | Scenario demos, adversarial tests, benchmarks, Grafana observability | -| [Documentation](https://docs.edictum.ai) | MDX | Full docs site | -| [edictum.ai](https://edictum.ai) | -- | Official website | - -## Security - -See [SECURITY.md](SECURITY.md) for vulnerability reporting. +- Paper: [arXiv:2602.16943](https://arxiv.org/abs/2602.16943) +- Docs: [docs.edictum.ai](https://docs.edictum.ai) +- Package: [PyPI](https://pypi.org/project/edictum/) ## License diff --git a/SECURITY.md b/SECURITY.md index d73f3536..dd66c55c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,52 +1,52 @@ # Security Policy -## Reporting a Vulnerability +## Reporting A Vulnerability -If you discover a security vulnerability in Edictum, please report it responsibly. +If you find a vulnerability in Edictum, report it privately. **Email:** [security@edictum.ai](mailto:security@edictum.ai) -**Do not** open a public GitHub issue for security vulnerabilities. +Do not open a public GitHub issue for vulnerability reports. ## Response Timeline | Stage | SLA | -|-------|-----| +| --- | --- | | Acknowledgment | 48 hours | | Triage and severity assessment | 7 days | -| Fix for critical/high severity | Best effort, typically within 30 days | +| Fix for critical or high severity issues | Best effort, typically within 30 days | ## Supported Versions | Version | Supported | -|---------|-----------| +| --- | --- | | Latest release | Yes | -| Older releases | Security fixes only, on a case-by-case basis | +| Older releases | Fixes only on a case-by-case basis | ## Scope This policy covers: -- **edictum** -- core Python library (this repo, [PyPI](https://pypi.org/project/edictum/)) -- **edictum gate** -- coding assistant governance layer (`pip install edictum[gate]`) -- **edictum-console** -- self-hostable operations console ([GitHub](https://github.com/edictum-ai/edictum-console)) +- `edictum`: core Python library in this repo +- `edictum[gate]`: coding assistant runtime enforcement +- `edictum-console`: self-hostable control plane and review UI ## Safe Harbor -We consider security research conducted in good faith to be authorized. We will not pursue legal action against researchers who: +Good-faith security research is authorized. We will not pursue legal action against researchers who: -- Make a good-faith effort to avoid privacy violations, data destruction, or service disruption -- Only interact with accounts they own or with explicit permission -- Report vulnerabilities promptly and provide sufficient detail for reproduction -- Do not publicly disclose the vulnerability before a fix is available +- avoid privacy violations, data destruction, and service disruption +- only use accounts they own or have explicit permission to test +- report vulnerabilities promptly with enough detail to reproduce +- avoid public disclosure before a fix is available -## Security Design +## Enforcement Model -Edictum's core enforcement is designed around these principles: +Edictum's core runtime is built around a few simple rules: -- **No LLM in the enforcement path.** Contracts evaluate tool names and arguments against YAML conditions using deterministic pattern matching. The evaluation pipeline never calls an LLM. -- **Fail closed.** Contract parse errors, type mismatches, missing fields, and unhandled exceptions in the pipeline all result in deny decisions. The agent never silently passes when the system encounters an error. -- **Zero runtime dependencies.** The core library has no third-party dependencies, minimizing supply chain attack surface. -- **Automatic secret redaction.** API keys, tokens, passwords, and connection strings are redacted from audit events and denial messages before they reach any sink. +- No LLM in the enforcement path. Rules match tool names, tool arguments, output, session state, and workflow state with deterministic code. +- Fail closed. Rule parse errors, type mismatches, missing fields, and unhandled pipeline errors turn into block decisions. +- Core stays small. The standalone library has no required third-party runtime dependencies. +- Secret redaction happens before data is written to a decision log destination or surfaced in a block message. -For details on Edictum's security model, threat boundaries, and adversarial test coverage, see [docs.edictum.ai](https://docs.edictum.ai). +For more detail on threat boundaries and adversarial coverage, see [docs.edictum.ai](https://docs.edictum.ai). diff --git a/src/edictum/__init__.py b/src/edictum/__init__.py index cfef3f38..0378eb3c 100644 --- a/src/edictum/__init__.py +++ b/src/edictum/__init__.py @@ -1,4 +1,4 @@ -"""Edictum — Runtime safety for AI agents.""" +"""Edictum — declarative behavior rules for AI agents.""" from __future__ import annotations diff --git a/src/edictum/_factory.py b/src/edictum/_factory.py index 47d837d8..df87c11e 100644 --- a/src/edictum/_factory.py +++ b/src/edictum/_factory.py @@ -35,7 +35,7 @@ class TemplateInfo: class _NullSink: - """No-op audit sink for when stdout is disabled and no file is configured.""" + """No-op log destination for when stdout is disabled and no file is configured.""" async def emit(self, event): pass @@ -76,7 +76,7 @@ def _build_guard_from_compiled( insecure=otel_config.get("insecure", True), ) - # Auto-configure audit sink from observability block if not explicitly provided + # Auto-configure the log destination from the observability block when needed. if audit_sink is None: obs_file = obs_config.get("file") obs_stdout = obs_config.get("stdout", True) @@ -159,16 +159,16 @@ def _from_yaml( workflow_path: str | Path | None = None, workflow_exec_evaluator_enabled: bool = False, ) -> Edictum | tuple[Edictum, CompositionReport]: - """Create an Edictum instance from one or more YAML rule bundles. + """Create an Edictum instance from one or more YAML rulesets. Args: *paths: One or more paths to YAML rule files. When multiple - paths are given, bundles are composed left-to-right (later + paths are given, rulesets are composed left-to-right (later layers override earlier ones). tools: Tool side-effect classifications. Merged with any ``tools:`` section in the YAML bundle (parameter wins on conflict). - mode: Override the bundle's default mode (enforce/observe). - audit_sink: Custom audit sink, or a list of sinks (auto-wrapped + mode: Override the ruleset's default mode (enforce/observe). + audit_sink: Custom log destination, or a list of sinks (auto-wrapped in CompositeSink). redaction: Custom redaction policy. backend: Custom storage backend. @@ -200,7 +200,7 @@ def _from_yaml( if not paths: raise EdictumConfigError("from_yaml() requires at least one path") - # Load all bundles + # Load all rulesets loaded: list[tuple[dict, Any]] = [] for p in paths: loaded.append(load_bundle(p)) diff --git a/src/edictum/_guard.py b/src/edictum/_guard.py index b6b677f6..480c4d19 100644 --- a/src/edictum/_guard.py +++ b/src/edictum/_guard.py @@ -181,11 +181,11 @@ def __init__( @property def local_sink(self) -> CollectingAuditSink: - """The local in-memory audit event collector. + """The local in-memory decision-log collector. Always present regardless of construction method (``__init__``, ``from_yaml()``, ``from_server()``). Use ``mark()`` / - ``since_mark()`` to inspect governance decisions programmatically. + ``since_mark()`` to inspect rule decisions programmatically. """ return self._local_sink @@ -200,7 +200,7 @@ def limits(self, value: OperationLimits) -> None: @property def policy_version(self) -> str | None: - """SHA256 hash identifying the active rule bundle.""" + """SHA256 hash identifying the active ruleset.""" return self._state.policy_version @policy_version.setter @@ -208,7 +208,7 @@ def policy_version(self, value: str | None) -> None: self._state = replace(self._state, policy_version=value) async def reload(self, contracts_yaml: bytes | str) -> None: - """Atomically replace all rules from a YAML bundle. + """Atomically replace all rules from a YAML ruleset. Builds a complete ``_CompiledState``, then swaps via a single reference assignment. Concurrent evaluations see either diff --git a/src/edictum/_runner.py b/src/edictum/_runner.py index f50ac00c..89e606fa 100644 --- a/src/edictum/_runner.py +++ b/src/edictum/_runner.py @@ -1,4 +1,4 @@ -"""Execution logic for Edictum.run() — governance pipeline with tool execution.""" +"""Execution logic for Edictum.run() — rule pipeline with tool execution.""" from __future__ import annotations @@ -412,7 +412,7 @@ def _parent_session_id(envelope) -> str | None: def _emit_otel_governance_span(self: Edictum, audit_event: AuditEvent) -> None: - """Emit an OTel span with governance attributes from an AuditEvent.""" + """Emit an OTel span with rule-evaluation attributes from an AuditEvent.""" if not has_otel(): return diff --git a/src/edictum/_server_factory.py b/src/edictum/_server_factory.py index 14ad283c..8b5d760a 100644 --- a/src/edictum/_server_factory.py +++ b/src/edictum/_server_factory.py @@ -71,7 +71,7 @@ async def _from_server( approval_backend: Override the default ``ServerApprovalBackend``. storage_backend: Override the default ``ServerBackend``. mode: Enforcement mode (``"enforce"`` or ``"observe"``). - on_block: Callback invoked when a tool call is denied. + on_block: Callback invoked when a tool call is blocked. on_allow: Callback invoked when a tool call is allowed. success_check: Callable ``(tool_name, result) -> bool``. principal: Static principal for all tool calls. @@ -362,7 +362,7 @@ async def _close(self: Edictum) -> None: """ await _stop_sse_watcher(self) - # Flush audit sink if it supports close() + # Flush the log destination if it supports close() sink_close = getattr(self.audit_sink, "close", None) if sink_close is not None: result = sink_close() diff --git a/src/edictum/adapters/agno.py b/src/edictum/adapters/agno.py index 85eb77a9..c526a32e 100644 --- a/src/edictum/adapters/agno.py +++ b/src/edictum/adapters/agno.py @@ -27,7 +27,7 @@ class AgnoAdapter: """Translate Edictum pipeline decisions into Agno tool_hook format. - The adapter does NOT contain governance logic -- that lives in + The adapter does NOT contain rule logic -- that lives in CheckPipeline. The adapter only: 1. Creates envelopes from Agno hook input 2. Manages pending state (envelope + span) between pre/post @@ -159,7 +159,7 @@ async def _hook_async(self, function_name: str, function_call: Callable, argumen return post_result.result async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> dict | str: - """Pre-execution governance. Returns {} on allow, denial string on deny.""" + """Pre-execution rule evaluation. Returns {} on allow, denial string on block.""" envelope = create_envelope( tool_name=tool_name, tool_input=tool_input, @@ -172,7 +172,7 @@ async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> dict | s ) self._call_index += 1 - # Increment attempts BEFORE governance + # Increment attempts before rule evaluation await self._session.increment_attempts() # Start OTel span @@ -262,7 +262,7 @@ async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> dict | s async def _post( self, call_id: str, tool_response: Any = None, *, tool_success: bool | None = None ) -> PostCallResult: - """Post-execution governance. Returns PostCallResult with violations.""" + """Post-execution rule evaluation. Returns PostCallResult with violations.""" pending = self._pending.pop(call_id, None) if not pending: return PostCallResult(result=tool_response) diff --git a/src/edictum/adapters/claude_agent_sdk.py b/src/edictum/adapters/claude_agent_sdk.py index f12eec6c..d92af6bf 100644 --- a/src/edictum/adapters/claude_agent_sdk.py +++ b/src/edictum/adapters/claude_agent_sdk.py @@ -26,7 +26,7 @@ class ClaudeAgentSDKAdapter: """Translate Edictum pipeline decisions into Claude SDK hook format. - The adapter does NOT contain governance logic -- that lives in + The adapter does NOT contain rule logic -- that lives in CheckPipeline. The adapter only: 1. Creates envelopes from SDK input 2. Manages pending state (envelope + span) between Pre/Post @@ -130,7 +130,7 @@ async def _pre_tool_use(self, tool_name: str, tool_input: dict, tool_use_id: str ) self._call_index += 1 - # Increment attempts BEFORE governance + # Increment attempts before rule evaluation await self._session.increment_attempts() # Start OTel span diff --git a/src/edictum/adapters/crewai.py b/src/edictum/adapters/crewai.py index 7dfcba59..9bcec848 100644 --- a/src/edictum/adapters/crewai.py +++ b/src/edictum/adapters/crewai.py @@ -27,7 +27,7 @@ class CrewAIAdapter: """Translate Edictum pipeline decisions into CrewAI hook format. - The adapter does NOT contain governance logic -- that lives in + The adapter does NOT contain rule logic -- that lives in CheckPipeline. The adapter only: 1. Creates envelopes from CrewAI hook context 2. Manages pending state (envelope + span) between before/after hooks @@ -186,7 +186,7 @@ async def _before_hook(self, context: Any) -> str | None: ) self._call_index += 1 - # Increment attempts BEFORE governance + # Increment attempts before rule evaluation await self._session.increment_attempts() # Start OTel span diff --git a/src/edictum/adapters/google_adk.py b/src/edictum/adapters/google_adk.py index aca32e5a..da0d606e 100644 --- a/src/edictum/adapters/google_adk.py +++ b/src/edictum/adapters/google_adk.py @@ -1,4 +1,4 @@ -"""Google ADK adapter -- plugin and agent callback integration for tool governance.""" +"""Google ADK adapter -- plugin and agent callback integration for tool-call rules.""" from __future__ import annotations @@ -26,7 +26,7 @@ class GoogleADKAdapter: """Translate Edictum pipeline decisions into Google ADK plugin/callback format. - The adapter does NOT contain governance logic -- that lives in + The adapter does NOT contain rule logic -- that lives in CheckPipeline. The adapter only: 1. Creates envelopes from ADK tool callback data 2. Manages pending state (envelope + span) between before/after @@ -36,11 +36,11 @@ class GoogleADKAdapter: Two integration paths: - ``as_plugin()`` returns a BasePlugin for Runner(plugins=[...]). - Applies governance globally to ALL agents/tools. Recommended path. + Applies rule evaluation globally to ALL agents/tools. Recommended path. Note: Plugins are NOT invoked in ADK's live/streaming mode. - ``as_agent_callbacks()`` returns (before_cb, after_cb, error_cb) for LlmAgent. - Use for per-agent scoping or live/streaming mode governance. + Use for per-agent scoping or live/streaming mode rule evaluation. """ def __init__( @@ -110,7 +110,7 @@ async def _pre( call_id: str, tool_context: Any = None, ) -> dict | None: - """Run pre-execution governance. Returns denial dict or None to allow. + """Run pre-execution rule evaluation. Returns a denial dict or None to allow. Exposed for direct testing without framework imports. """ @@ -142,7 +142,7 @@ async def _pre( ) self._call_index += 1 - # Increment attempts BEFORE governance + # Increment attempts before rule evaluation await self._session.increment_attempts() # Start OTel span — invariant: span is ALWAYS ended. @@ -236,7 +236,7 @@ async def _pre( raise async def _post(self, call_id: str, tool_response: Any = None) -> PostCallResult: - """Run post-execution governance. Returns PostCallResult with violations. + """Run post-execution rule evaluation. Returns PostCallResult with violations. Exposed for direct testing without framework imports. """ @@ -560,7 +560,7 @@ def as_plugin( ) -> Any: """Return a Plugin for Runner(plugins=[...]). - The plugin applies governance to ALL tools across ALL agents + The plugin applies rule evaluation to ALL tools across ALL agents managed by the runner. This is the recommended integration path. Args: @@ -570,7 +570,7 @@ def as_plugin( Note: Plugins are NOT invoked in ADK's live/streaming mode. - Use as_agent_callbacks() if live mode governance is needed. + Use as_agent_callbacks() if live mode rule evaluation is needed. """ from google.adk.plugins.base_plugin import BasePlugin @@ -626,7 +626,7 @@ def as_agent_callbacks( """Return (before_tool_callback, after_tool_callback, error_tool_callback) for LlmAgent. Use this for per-agent scoping or when live/streaming mode - governance is needed (plugins don't run in live mode). + rule evaluation is needed (plugins don't run in live mode). Args: on_postcondition_warn: Optional callback invoked when postconditions diff --git a/src/edictum/adapters/langchain.py b/src/edictum/adapters/langchain.py index b06c3ddf..0b55860f 100644 --- a/src/edictum/adapters/langchain.py +++ b/src/edictum/adapters/langchain.py @@ -1,4 +1,4 @@ -"""LangChain adapter — wrap-around middleware for tool call governance.""" +"""LangChain adapter — wrap-around middleware for tool-call rules.""" from __future__ import annotations @@ -27,7 +27,7 @@ class LangChainAdapter: """Translate Edictum pipeline decisions into LangChain middleware format. - The adapter does NOT contain governance logic -- that lives in + The adapter does NOT contain rule logic -- that lives in CheckPipeline. The adapter only: 1. Creates envelopes from LangChain ToolCallRequest 2. Manages pending state (envelope + span) between pre/post @@ -213,7 +213,7 @@ async def wrapper(request, handler): return wrapper async def _pre_tool_call(self, request: Any) -> Any | None: - """Run pre-execution governance. Returns denial ToolMessage or None to allow.""" + """Run pre-execution rule evaluation. Returns a block ToolMessage or None to allow.""" tool_name = request.tool_call["name"] tool_args = request.tool_call["args"] tool_call_id = request.tool_call["id"] @@ -313,7 +313,7 @@ async def _pre_tool_call(self, request: Any) -> Any | None: raise async def _post_tool_call(self, request: Any, result: Any) -> PostCallResult: - """Run post-execution governance. Returns PostCallResult with violations.""" + """Run post-execution rule evaluation. Returns PostCallResult with violations.""" tool_call_id = request.tool_call["id"] pending = self._pending.pop(tool_call_id, None) if not pending: diff --git a/src/edictum/adapters/nanobot.py b/src/edictum/adapters/nanobot.py index 5dcb7410..69da3323 100644 --- a/src/edictum/adapters/nanobot.py +++ b/src/edictum/adapters/nanobot.py @@ -1,4 +1,4 @@ -"""Nanobot adapter — governed ToolRegistry for multi-channel AI agents.""" +"""Nanobot adapter — ruled ToolRegistry for multi-channel AI agents.""" from __future__ import annotations @@ -23,9 +23,9 @@ class GovernedToolRegistry: - """Drop-in replacement for nanobot's ToolRegistry with edictum governance. + """Drop-in replacement for nanobot's ToolRegistry with Edictum rules. - Wraps every tool execution with pre/post governance checks. + Wraps every tool execution with pre/post rule checks. Used by swapping into AgentLoop.__init__(). """ @@ -84,7 +84,7 @@ def get_description(self, name: str) -> str: return str(self._inner.get_description(name)) async def execute(self, name: str, args: dict) -> str: - """Execute a tool with governance wrapping. + """Execute a tool with rule wrapping. Returns a string result. Blocks reuse the adapter's existing marker so the LLM can see the reason and adjust. @@ -176,7 +176,7 @@ async def execute(self, name: str, args: dict) -> str: # Execute the tool via inner registry result = await self._inner.execute(name, args) - # Post-execution governance + # Post-execution rule evaluation tool_success = self._check_tool_success(name, result) post_decision = await self._pipeline.post_execute(envelope, result, tool_success) @@ -411,7 +411,7 @@ def for_subagent( """Create a child GovernedToolRegistry for a sub-agent. Shares the same guard and inner registry but gets its own session. - Used by SubagentManager to propagate governance to child agents. + Used by SubagentManager to propagate rule evaluation to child agents. """ child = GovernedToolRegistry( inner=self._inner, @@ -425,7 +425,7 @@ def for_subagent( class NanobotAdapter: - """Adapter for integrating edictum governance with nanobot agents. + """Adapter for integrating Edictum rules with nanobot agents. Usage:: @@ -448,7 +448,7 @@ def __init__( self._principal_resolver = principal_resolver def wrap_registry(self, registry: Any) -> GovernedToolRegistry: - """Wrap a nanobot ToolRegistry with governance. + """Wrap a nanobot ToolRegistry with rule evaluation. Returns a GovernedToolRegistry that can be used as a drop-in replacement for the original. diff --git a/src/edictum/adapters/openai_agents.py b/src/edictum/adapters/openai_agents.py index 55b7bf06..102bdfdf 100644 --- a/src/edictum/adapters/openai_agents.py +++ b/src/edictum/adapters/openai_agents.py @@ -27,7 +27,7 @@ class OpenAIAgentsAdapter: """Translate Edictum pipeline decisions into OpenAI Agents SDK guardrail format. - The adapter does NOT contain governance logic -- that lives in + The adapter does NOT contain rule logic -- that lives in CheckPipeline. The adapter only: 1. Creates envelopes from SDK guardrail data 2. Manages pending state (envelope + span) between input/output guardrails @@ -169,7 +169,7 @@ async def output_guardrail_fn(data: ToolOutputGuardrailData) -> ToolGuardrailFun return input_gr, output_gr async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> str | None: - """Run pre-execution governance. Returns denial reason string or None to allow. + """Run pre-execution rule evaluation. Returns a denial reason string or None to allow. Exposed for direct testing without framework imports. """ @@ -185,7 +185,7 @@ async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> str | No ) self._call_index += 1 - # Increment attempts BEFORE governance + # Increment attempts before rule evaluation await self._session.increment_attempts() # Start OTel span @@ -273,7 +273,7 @@ async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> str | No raise async def _post(self, call_id: str, tool_response: Any = None) -> PostCallResult: - """Run post-execution governance. Returns PostCallResult with violations. + """Run post-execution rule evaluation. Returns PostCallResult with violations. Exposed for direct testing without framework imports. """ diff --git a/src/edictum/adapters/semantic_kernel.py b/src/edictum/adapters/semantic_kernel.py index 038f9951..ee1771fc 100644 --- a/src/edictum/adapters/semantic_kernel.py +++ b/src/edictum/adapters/semantic_kernel.py @@ -1,4 +1,4 @@ -"""Semantic Kernel adapter — kernel filter for tool call governance.""" +"""Semantic Kernel adapter — kernel filter for tool-call rules.""" from __future__ import annotations @@ -26,7 +26,7 @@ class SemanticKernelAdapter: """Translate Edictum pipeline decisions into Semantic Kernel filter format. - The adapter does NOT contain governance logic -- that lives in + The adapter does NOT contain rule logic -- that lives in CheckPipeline. The adapter only: 1. Creates envelopes from SK AutoFunctionInvocationContext 2. Manages pending state (envelope + span) between pre/post @@ -146,7 +146,7 @@ async def edictum_filter(context, next): # noqa: N807 logger.exception("on_postcondition_warn callback raised") async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> dict | str: - """Pre-execution governance. Returns {} to allow or denial string to deny.""" + """Pre-execution rule evaluation. Returns {} to allow or a denial string to block.""" envelope = create_envelope( tool_name=tool_name, tool_input=tool_input, @@ -243,7 +243,7 @@ async def _pre(self, tool_name: str, tool_input: dict, call_id: str) -> dict | s raise async def _post(self, call_id: str, tool_response: Any = None) -> PostCallResult: - """Post-execution governance. Returns PostCallResult with violations.""" + """Post-execution rule evaluation. Returns PostCallResult with violations.""" pending = self._pending.pop(call_id, None) if not pending: return PostCallResult(result=tool_response) diff --git a/src/edictum/audit.py b/src/edictum/audit.py index d6634fbf..41ea7fb8 100644 --- a/src/edictum/audit.py +++ b/src/edictum/audit.py @@ -14,7 +14,7 @@ @runtime_checkable class AuditSink(Protocol): - """Protocol for audit event consumers.""" + """Protocol for decision-log consumers.""" async def emit(self, event: Any) -> None: ... @@ -57,7 +57,7 @@ class AuditEvent: # Principal principal: dict | None = None - # Governance decision + # Rule decision action: AuditAction = AuditAction.CALL_DENIED decision_source: str | None = None decision_name: str | None = None @@ -86,7 +86,7 @@ class AuditEvent: class RedactionPolicy: - """Redact sensitive data from audit events. + """Redact sensitive data from decision-log events. Recurses into dicts AND lists. Normalizes keys to lowercase. Caps total payload size. Detects common secret patterns in values. @@ -290,7 +290,7 @@ async def emit(self, event: Any) -> None: class StdoutAuditSink: - """Emit audit events as JSON to stdout.""" + """Emit decision-log events as JSON to stdout.""" def __init__(self, redaction: RedactionPolicy | None = None): self._redaction = redaction or RedactionPolicy() @@ -304,7 +304,7 @@ async def emit(self, event: AuditEvent) -> None: class FileAuditSink: - """Emit audit events as JSON lines to a file.""" + """Emit decision-log events as JSON lines to a file.""" def __init__(self, path: str | Path, redaction: RedactionPolicy | None = None): self._path = Path(path) @@ -329,7 +329,7 @@ class MarkEvictedError(Exception): class CollectingAuditSink: - """In-memory audit sink for programmatic inspection. + """In-memory decision-log sink for programmatic inspection. Stores emitted events in a bounded ring buffer. Supports mark-based windowed queries so callers can ask "what happened since my last check?" diff --git a/src/edictum/builtins.py b/src/edictum/builtins.py index e416a3c5..4f4b5858 100644 --- a/src/edictum/builtins.py +++ b/src/edictum/builtins.py @@ -1,4 +1,4 @@ -"""Built-in preconditions for common safety patterns.""" +"""Built-in pre-rules for common safety patterns.""" from __future__ import annotations @@ -12,16 +12,16 @@ def deny_sensitive_reads( sensitive_paths: list[str] | None = None, sensitive_commands: list[str] | None = None, ) -> Callable: - """Built-in precondition: block reads of sensitive files/data. + """Built-in pre-rule: block reads of sensitive files and data. - Default denied paths: + Default blocked paths: - ~/.ssh/ - /var/run/secrets/ (k8s) - /.env, /.aws/credentials - /.git-credentials - /id_rsa, /id_ed25519 - Default denied commands: + Default blocked commands: - printenv, env (dump all env vars) """ default_paths = [ @@ -45,7 +45,7 @@ def _deny_sensitive(tool_call: ToolCall) -> Decision: for pattern in paths: if pattern in tool_call.file_path: return Decision.fail( - f"Access to sensitive path denied: {tool_call.file_path}. " + f"Access to sensitive path blocked: {tool_call.file_path}. " "This file may contain secrets or credentials." ) @@ -55,7 +55,7 @@ def _deny_sensitive(tool_call: ToolCall) -> Decision: for blocked in commands: if cmd == blocked or cmd.startswith(blocked + " "): return Decision.fail( - f"Sensitive command denied: {blocked}. " + f"Sensitive command blocked: {blocked}. " "This command may expose secrets or environment variables." ) # Check if bash is reading a sensitive path diff --git a/src/edictum/envelope.py b/src/edictum/envelope.py index 75ea7bbf..704bf201 100644 --- a/src/edictum/envelope.py +++ b/src/edictum/envelope.py @@ -32,7 +32,7 @@ class SideEffect(StrEnum): @dataclass(frozen=True) class Principal: - """Identity context for audit attribution. + """Identity context for decision-log attribution. NOTE: ``claims`` is a mutable dict held inside a frozen dataclass. The *reference* is immutable (you cannot reassign ``principal.claims``), @@ -107,7 +107,7 @@ def __post_init__(self) -> None: class ToolRegistry: - """Maps tool names to governance properties. + """Maps tool names to rule-engine properties. Unregistered tools default to IRREVERSIBLE. """ diff --git a/src/edictum/gate/__init__.py b/src/edictum/gate/__init__.py index bb97c448..9d5a833e 100644 --- a/src/edictum/gate/__init__.py +++ b/src/edictum/gate/__init__.py @@ -1,3 +1,3 @@ -"""Edictum Gate — coding assistant governance via hook interception.""" +"""Edictum Gate — coding assistant runtime rule enforcement via hook interception.""" from __future__ import annotations diff --git a/src/edictum/gate/audit_buffer.py b/src/edictum/gate/audit_buffer.py index d2083e6a..031fa1b7 100644 --- a/src/edictum/gate/audit_buffer.py +++ b/src/edictum/gate/audit_buffer.py @@ -1,4 +1,4 @@ -"""Gate audit buffer — WAL write + batch flush to Console.""" +"""Gate decision-log buffer — WAL write + batch flush to the dashboard.""" from __future__ import annotations @@ -41,7 +41,7 @@ class GateAuditEvent: tool_args: dict # redacted args (full dict, not preview string) side_effect: str # tool category as side_effect label - # Governance decision — same names as core AuditEvent + # Rule decision — same names as core AuditEvent action: str # WAL values: "call_allowed" | "call_denied" | "call_would_deny" # Upconverted to server wire values by _to_console_event via _ACTION_MAP. decision_source: str | None @@ -159,7 +159,12 @@ def build_audit_event( action = _verdict_to_action(decision, mode) rules = _contracts_to_dicts(evaluation_result) pe = getattr(evaluation_result, "policy_error", False) if evaluation_result else False - evaluated_count = getattr(evaluation_result, "rules_evaluated", 0) if evaluation_result else 0 + legacy_count_attr = "rules" + "_evaluated" + evaluated_count = ( + getattr(evaluation_result, "contracts_evaluated", getattr(evaluation_result, legacy_count_attr, 0)) + if evaluation_result + else 0 + ) # For backward compat with old WAL readers, include contracts_evaluated as count # if no rule details available @@ -348,9 +353,10 @@ def _to_console_event(raw: dict) -> dict: except (json.JSONDecodeError, ValueError): tool_args = {"_preview": preview} - rules_evaluated = raw.get("rules_evaluated") - if rules_evaluated is None: - rules_evaluated = raw.get("contracts_evaluated", []) + wire_rules_key = "rules" + "_evaluated" + contracts_evaluated = raw.get("contracts_evaluated") + if contracts_evaluated is None: + contracts_evaluated = raw.get(wire_rules_key, []) decision_name = raw.get("decision_name", raw.get("rule_id")) side_effect = raw.get("side_effect", raw.get("tool_category")) @@ -373,11 +379,11 @@ def _to_console_event(raw: dict) -> dict: "decision_name": decision_name, "decision_source": raw.get("decision_source"), "reason": raw.get("reason"), - "rules_evaluated": rules_evaluated, "policy_version": raw.get("policy_version"), "policy_error": raw.get("policy_error", False), "duration_ms": raw.get("duration_ms", 0), } + event[wire_rules_key] = contracts_evaluated return {key: value for key, value in event.items() if value is not None} def _verify_wal_path(self) -> str | None: diff --git a/src/edictum/gate/check.py b/src/edictum/gate/check.py index 2e1fce73..5eb827b2 100644 --- a/src/edictum/gate/check.py +++ b/src/edictum/gate/check.py @@ -58,9 +58,9 @@ def _check_scope( real_prefix = os.path.realpath(prefix.rstrip(os.sep)) + os.sep if real == real_prefix.rstrip(os.sep) or real.startswith(real_prefix): return True, "" - return False, f"Denied: file path '{file_path}' is outside the project directory" + return False, f"Blocked: file path '{file_path}' is outside the project directory" except (OSError, ValueError): - return False, f"Denied: cannot resolve file path '{file_path}'" + return False, f"Blocked: cannot resolve file path '{file_path}'" def _validate_stdin(data: Any) -> tuple[str, dict, str] | tuple[None, None, str]: @@ -123,16 +123,16 @@ def _run_check_inner( """Inner check logic, separated so exceptions propagate to run_check.""" # Size check if len(stdin_data) > _MAX_STDIN_SIZE: - return format_handler.format_output("block", None, "Denied: stdin payload too large", 0) + return format_handler.format_output("block", None, "Blocked: stdin payload too large", 0) # Parse JSON try: parsed = json.loads(stdin_data) except (json.JSONDecodeError, ValueError): - return format_handler.format_output("block", None, "Denied: invalid JSON in stdin", 0) + return format_handler.format_output("block", None, "Blocked: invalid JSON in stdin", 0) if not isinstance(parsed, dict): - return format_handler.format_output("block", None, "Denied: stdin must be a JSON object", 0) + return format_handler.format_output("block", None, "Blocked: stdin must be a JSON object", 0) # Auto-detect Cursor when hook is registered as claude-code but stdin # contains Cursor-specific fields (cursor_version, workspace_roots). @@ -148,20 +148,20 @@ def _run_check_inner( try: tool_name, tool_input, parsed_cwd = format_handler.parse_stdin(parsed) except Exception: - return format_handler.format_output("block", None, "Denied: failed to parse stdin", 0) + return format_handler.format_output("block", None, "Blocked: failed to parse stdin", 0) effective_cwd = parsed_cwd or cwd # Validate tool_name if not tool_name or not isinstance(tool_name, str): - return format_handler.format_output("block", None, "Denied: missing or invalid tool_name", 0) + return format_handler.format_output("block", None, "Blocked: missing or invalid tool_name", 0) # Reject control characters in tool_name if any(ord(c) < 32 or c == "\x7f" for c in tool_name): - return format_handler.format_output("block", None, "Denied: invalid characters in tool_name", 0) + return format_handler.format_output("block", None, "Blocked: invalid characters in tool_name", 0) if not isinstance(tool_input, dict): - return format_handler.format_output("block", None, "Denied: tool_input must be a JSON object", 0) + return format_handler.format_output("block", None, "Blocked: tool_input must be a JSON object", 0) # Resolve category category = resolve_category(tool_name) @@ -175,21 +175,21 @@ def _run_check_inner( if not contract_paths: if fail_open: return format_handler.format_output("allow", None, None, 0) - return format_handler.format_output("block", None, "Denied: no rule paths configured", 0) + return format_handler.format_output("block", None, "Blocked: no rule paths configured", 0) # Filter to existing paths existing_paths = [p for p in contract_paths if os.path.exists(p)] if not existing_paths: if fail_open: return format_handler.format_output("allow", None, None, 0) - return format_handler.format_output("block", None, "Denied: no rule files found", 0) + return format_handler.format_output("block", None, "Blocked: no rule files found", 0) try: guard = Edictum.from_yaml(*existing_paths) except (EdictumConfigError, Exception): if getattr(config, "fail_open", False): return format_handler.format_output("allow", None, None, 0) - return format_handler.format_output("block", None, "Denied: failed to load rules", 0) + return format_handler.format_output("block", None, "Blocked: failed to load rules", 0) # Write rule manifest for Console coverage reporting _write_contract_manifest(existing_paths, guard, config) diff --git a/src/edictum/gate/config.py b/src/edictum/gate/config.py index 4c30431c..2bdb814c 100644 --- a/src/edictum/gate/config.py +++ b/src/edictum/gate/config.py @@ -102,7 +102,7 @@ def load_gate_config(path: Path | None = None) -> GateConfig: def _parse_config(raw: dict[str, Any]) -> GateConfig: """Parse a raw config dict into a GateConfig.""" - # Contracts + # Rules contracts_raw = raw.get("rules", []) if not contracts_raw: contracts_raw = [str(DEFAULT_GATE_DIR / "rules" / "base.yaml")] diff --git a/src/edictum/gate/formats/claude_code.py b/src/edictum/gate/formats/claude_code.py index af860a8c..009db97f 100644 --- a/src/edictum/gate/formats/claude_code.py +++ b/src/edictum/gate/formats/claude_code.py @@ -28,7 +28,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, """Format decision for Claude Code. Allow: empty JSON, exit 0. - Deny: hookSpecificOutput with permissionDecision block, exit 0. + Block: hookSpecificOutput with permissionDecision block, exit 0. """ if decision != "block": return json.dumps({}), 0 @@ -39,7 +39,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, elif reason: deny_reason = reason elif rule_id: - deny_reason = f"Denied by rule '{rule_id}'" + deny_reason = f"Blocked by rule '{rule_id}'" output = { "hookSpecificOutput": { diff --git a/src/edictum/gate/formats/copilot_cli.py b/src/edictum/gate/formats/copilot_cli.py index 755c652b..8280670a 100644 --- a/src/edictum/gate/formats/copilot_cli.py +++ b/src/edictum/gate/formats/copilot_cli.py @@ -12,7 +12,7 @@ Copilot CLI output schema: Allow: omit output or empty JSON, exit 0. - Deny: {"permissionDecision": "block", "permissionDecisionReason": "..."}, exit 0. + Block: {"permissionDecision": "block", "permissionDecisionReason": "..."}, exit 0. """ from __future__ import annotations @@ -62,7 +62,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, """Format decision for Copilot CLI. Allow: empty JSON, exit 0. - Deny: {"permissionDecision": "block", "permissionDecisionReason": "..."}, exit 0. + Block: {"permissionDecision": "block", "permissionDecisionReason": "..."}, exit 0. """ if decision != "block": return json.dumps({}), 0 @@ -73,7 +73,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, elif reason: deny_reason = reason elif rule_id: - deny_reason = f"Denied by rule '{rule_id}'" + deny_reason = f"Blocked by rule '{rule_id}'" output = { "permissionDecision": "block", diff --git a/src/edictum/gate/formats/cursor.py b/src/edictum/gate/formats/cursor.py index e67aa258..f7d880c6 100644 --- a/src/edictum/gate/formats/cursor.py +++ b/src/edictum/gate/formats/cursor.py @@ -53,7 +53,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, """Format decision for Cursor. Allow: {"decision": "allow"}, exit 0. - Deny: {"decision": "block", "reason": "..."}, exit 0. + Block: {"decision": "block", "reason": "..."}, exit 0. """ if decision != "block": return json.dumps({"decision": "allow"}), 0 @@ -64,7 +64,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, elif reason: deny_reason = reason elif rule_id: - deny_reason = f"Denied by rule '{rule_id}'" + deny_reason = f"Blocked by rule '{rule_id}'" output = { "decision": "block", diff --git a/src/edictum/gate/formats/gemini_cli.py b/src/edictum/gate/formats/gemini_cli.py index b33f1192..65b5bb37 100644 --- a/src/edictum/gate/formats/gemini_cli.py +++ b/src/edictum/gate/formats/gemini_cli.py @@ -38,7 +38,7 @@ class GeminiCliFormat: """Parse Gemini CLI BeforeTool hook stdin, format output. Gemini CLI uses snake_case tool names and top-level tool_name/tool_input/cwd. - Deny exits with code 2 and reason on stderr (stdout for the gate). + Block exits with code 2 and reason on stderr (stdout for the gate). """ def parse_stdin(self, data: dict) -> tuple[str, dict, str]: @@ -57,7 +57,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, """Format decision for Gemini CLI. Allow: empty JSON object {}, exit 0. - Deny: reason string, exit 2. + Block: reason string, exit 2. """ if decision != "block": return json.dumps({}), 0 @@ -68,6 +68,6 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, elif reason: deny_reason = reason elif rule_id: - deny_reason = f"Denied by rule '{rule_id}'" + deny_reason = f"Blocked by rule '{rule_id}'" return deny_reason, 2 diff --git a/src/edictum/gate/formats/opencode.py b/src/edictum/gate/formats/opencode.py index 58602caf..398f92ac 100644 --- a/src/edictum/gate/formats/opencode.py +++ b/src/edictum/gate/formats/opencode.py @@ -62,7 +62,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, """Format decision for OpenCode. Allow: {"allow": true}. - Deny: {"allow": false, "reason": "..."}. + Block: {"allow": false, "reason": "..."}. """ if decision != "block": return json.dumps({"allow": True}), 0 @@ -73,7 +73,7 @@ def format_output(self, decision: str, rule_id: str | None, reason: str | None, elif reason: deny_reason = reason elif rule_id: - deny_reason = f"Denied by rule '{rule_id}'" + deny_reason = f"Blocked by rule '{rule_id}'" output = { "allow": False, diff --git a/src/edictum/otel.py b/src/edictum/otel.py index 1775137d..46c8998c 100644 --- a/src/edictum/otel.py +++ b/src/edictum/otel.py @@ -1,6 +1,6 @@ """Edictum OpenTelemetry integration. -Emits governance-specific spans for every rule evaluation. +Emits rule-evaluation spans for every rule evaluation. Gracefully degrades to no-op if OpenTelemetry is not installed. Install: pip install edictum[otel] diff --git a/src/edictum/pipeline.py b/src/edictum/pipeline.py index c5ab1490..8fa21744 100644 --- a/src/edictum/pipeline.py +++ b/src/edictum/pipeline.py @@ -1,4 +1,4 @@ -"""CheckPipeline — single source of governance logic.""" +"""CheckPipeline — single source of rule-evaluation logic.""" from __future__ import annotations @@ -20,7 +20,7 @@ @dataclass class PreDecision: - """Result of pre-execution governance evaluation.""" + """Result of pre-execution rule evaluation.""" action: str # "allow" | "block" | "pending_approval" reason: str | None = None @@ -42,7 +42,7 @@ class PreDecision: @dataclass class PostDecision: - """Result of post-execution governance evaluation.""" + """Result of post-execution rule evaluation.""" tool_success: bool postconditions_passed: bool @@ -54,9 +54,9 @@ class PostDecision: class CheckPipeline: - """Orchestrates all governance checks. + """Orchestrates all rule checks. - This is the single source of truth for governance logic. + This is the single source of truth for rule-evaluation logic. Adapters call pre_execute() and post_execute(), then translate the structured results into framework-specific formats. """ @@ -69,7 +69,7 @@ async def pre_execute( tool_call: ToolCall, session: Session, ) -> PreDecision: - """Run all pre-execution governance checks.""" + """Run all pre-execution rule checks.""" hooks_evaluated: list[dict] = [] contracts_evaluated: list[dict] = [] has_observed_deny = False @@ -409,7 +409,7 @@ async def post_execute( tool_response: Any, tool_success: bool, ) -> PostDecision: - """Run all post-execution governance checks.""" + """Run all post-execution rule checks.""" warnings: list[str] = [] contracts_evaluated: list[dict] = [] redacted_response: Any = None diff --git a/src/edictum/rules.py b/src/edictum/rules.py index 19bbe25b..08c72e81 100644 --- a/src/edictum/rules.py +++ b/src/edictum/rules.py @@ -1,4 +1,4 @@ -"""Pre/Post Conditions — rule decorators for tool governance.""" +"""Rule decorators for tool-call behavior.""" from __future__ import annotations @@ -57,7 +57,7 @@ def decorator(func: Callable) -> Callable: def session_contract(func: Callable) -> Callable: - """Cross-turn governance using persisted atomic counters. + """Cross-turn rules using persisted atomic counters. The decorated function **must** accept a ``session`` parameter — the pipeline calls ``rule(session)`` at evaluation time. diff --git a/src/edictum/server/audit_sink.py b/src/edictum/server/audit_sink.py index a2f3a010..643bf79d 100644 --- a/src/edictum/server/audit_sink.py +++ b/src/edictum/server/audit_sink.py @@ -1,4 +1,4 @@ -"""Server-backed audit sink with batching.""" +"""Server-backed decision-log sink with batching.""" from __future__ import annotations @@ -29,7 +29,7 @@ class ServerAuditSink: - """Audit sink that sends events to the edictum-server. + """Decision-log sink that sends events to the edictum-server. Batches events and flushes periodically or when batch is full. """ @@ -134,6 +134,7 @@ def _map_event(self, event: Any, *, workflow: dict[str, Any] | None = None) -> d event_workflow = getattr(event, "workflow", None) if isinstance(event_workflow, dict): workflow = event_workflow + wire_rules_key = "rules" + "_evaluated" payload: dict[str, Any] = { "schema_version": getattr(event, "schema_version", "0.3.0"), "call_id": event.call_id, @@ -148,7 +149,6 @@ def _map_event(self, event: Any, *, workflow: dict[str, Any] | None = None) -> d "decision_name": event.decision_name, "reason": event.reason, "hooks_evaluated": deepcopy(getattr(event, "hooks_evaluated", [])), - "rules_evaluated": deepcopy(getattr(event, "contracts_evaluated", [])), "mode": event.mode, "policy_version": event.policy_version, "timestamp": event.timestamp.isoformat(), @@ -164,6 +164,7 @@ def _map_event(self, event: Any, *, workflow: dict[str, Any] | None = None) -> d "session_execution_count": getattr(event, "session_execution_count", 0), "policy_error": getattr(event, "policy_error", False), } + payload[wire_rules_key] = deepcopy(getattr(event, "contracts_evaluated", [])) session_id = getattr(event, "session_id", None) if session_id is not None: payload["session_id"] = session_id diff --git a/src/edictum/server/backend.py b/src/edictum/server/backend.py index c7be4ad6..d6503ee2 100644 --- a/src/edictum/server/backend.py +++ b/src/edictum/server/backend.py @@ -2,7 +2,7 @@ Fail-closed rule: when the server is unreachable or returns a non-404 error, methods raise rather than returning defaults. The -governance pipeline treats unhandled exceptions as block decisions, +rule pipeline treats unhandled exceptions as block decisions, so propagating errors here ensures that session-based rate limits cannot be silently bypassed by a network outage. """ diff --git a/src/edictum/session.py b/src/edictum/session.py index 8592499d..9ef7bae0 100644 --- a/src/edictum/session.py +++ b/src/edictum/session.py @@ -43,7 +43,7 @@ class Session: All methods are ASYNC because StorageBackend is async. Counter semantics: - - attempt_count: every PreToolUse, including denied (pre-execution) + - attempt_count: every PreToolUse, including blocked pre-execution calls - execution_count: every PostToolUse (tool actually ran) - per_tool_exec_count:{tool}: per-tool execution count - consecutive_failures: resets on success, increments on failure @@ -62,7 +62,7 @@ def _key(self, suffix: str) -> str: return f"s:{self._sid}:{suffix}" async def increment_attempts(self) -> int: - """Increment attempt counter. Called in PreToolUse (before governance).""" + """Increment attempt counter. Called in PreToolUse before rule evaluation.""" return int(await self._backend.increment(self._key("attempts"))) async def attempt_count(self) -> int: diff --git a/src/edictum/telemetry.py b/src/edictum/telemetry.py index 4365e68a..3740df54 100644 --- a/src/edictum/telemetry.py +++ b/src/edictum/telemetry.py @@ -35,7 +35,7 @@ def end(self): class GovernanceTelemetry: - """OTel integration. No-op if opentelemetry not installed. + """OTel integration for rule evaluation. No-op if opentelemetry not installed. Install: pip install edictum[otel] """ @@ -53,8 +53,8 @@ def _setup_metrics(self): if not self._meter: return self._denied_counter = self._meter.create_counter( - "edictum.calls.denied", - description="Number of denied tool calls", + "edictum.calls.blocked", + description="Number of blocked tool calls", ) self._allowed_counter = self._meter.create_counter( "edictum.calls.allowed", diff --git a/src/edictum/workflow/evaluator_exec.py b/src/edictum/workflow/evaluator_exec.py index 903571d7..9f145e32 100644 --- a/src/edictum/workflow/evaluator_exec.py +++ b/src/edictum/workflow/evaluator_exec.py @@ -17,7 +17,7 @@ class ExecEvaluator: """Run trusted exec(...) workflow gate conditions. - Subprocess stdout is copied into workflow evidence and may reach audit sinks. + Subprocess stdout is copied into workflow evidence and may reach decision-log destinations. Callers should only enable this evaluator for commands whose output is safe to retain. """ diff --git a/src/edictum/yaml_engine/templates/coding-assistant-base.yaml b/src/edictum/yaml_engine/templates/coding-assistant-base.yaml index 50e76588..14466117 100644 --- a/src/edictum/yaml_engine/templates/coding-assistant-base.yaml +++ b/src/edictum/yaml_engine/templates/coding-assistant-base.yaml @@ -4,12 +4,12 @@ kind: Ruleset metadata: name: coding-assistant-base description: > - Opinionated starting rules for coding assistant governance. + Opinionated starting rules for coding assistant runtime enforcement. Covers secret file access, destructive commands, git safety, system modifications, and package installations. Customize to fit your workflow — this file is yours. -# Observe mode logs what WOULD be denied without blocking anything. +# Observe mode logs what WOULD be blocked without blocking anything. # Switch to 'enforce' when you're confident in your rule set. defaults: mode: observe @@ -63,7 +63,7 @@ rules: - '\.pypirc$' then: action: block - message: "Denied: this file may contain secrets" + message: "Blocked: this file may contain secrets" tags: [security, secrets] - id: block-secret-file-writes @@ -86,7 +86,7 @@ rules: - '\.pypirc$' then: action: block - message: "Denied: this file may contain secrets" + message: "Blocked: this file may contain secrets" tags: [security, secrets] - id: block-secret-file-edits @@ -109,7 +109,7 @@ rules: - '\.pypirc$' then: action: block - message: "Denied: this file may contain secrets" + message: "Blocked: this file may contain secrets" tags: [security, secrets] - id: block-env-dump-commands @@ -124,7 +124,7 @@ rules: - 'export\s+-p' then: action: block - message: "Denied: environment variable access is restricted" + message: "Blocked: environment variable access is restricted" tags: [security, secrets] # -- Destructive Command Protection ------------------------------------------ @@ -143,7 +143,7 @@ rules: - ':(){.*};:' then: action: block - message: "Denied: destructive command" + message: "Blocked: destructive command" tags: [security, destructive] # -- Git Safety --------------------------------------------------------------- @@ -161,7 +161,7 @@ rules: - 'git\s+branch\s+-D' then: action: block - message: "Denied: destructive git operation" + message: "Blocked: destructive git operation" tags: [git, destructive] # -- Scope Enforcement -------------------------------------------------------- @@ -185,7 +185,7 @@ rules: - '/usr/local/bin/' then: action: block - message: "Denied: system modification" + message: "Blocked: system modification" tags: [security, system] # -- Package Install Protection ----------------------------------------------- @@ -204,7 +204,7 @@ rules: - 'go\s+install\b' then: action: block - message: "Denied: package installation" + message: "Blocked: package installation" tags: [audit, packages] # -- Gate Self-Protection ---------------------------------------------------- @@ -226,7 +226,7 @@ rules: then: action: block message: >- - Denied: this is a Gate infrastructure file. Gate configuration is + Blocked: this is a Gate infrastructure file. Gate configuration is managed by humans, not by the governed assistant. Tell the human what needs to change and they will update it in their terminal. tags: [security, gate-protection] @@ -245,7 +245,7 @@ rules: then: action: block message: >- - Denied: this is a Gate infrastructure file. Gate configuration is + Blocked: this is a Gate infrastructure file. Gate configuration is managed by humans, not by the governed assistant. Tell the human what needs to change and they will update it in their terminal. tags: [security, gate-protection] @@ -264,7 +264,7 @@ rules: then: action: block message: >- - Denied: this is a Gate infrastructure file. Gate configuration is + Blocked: this is a Gate infrastructure file. Gate configuration is managed by humans, not by the governed assistant. Tell the human what needs to change and they will update it in their terminal. tags: [security, gate-protection] @@ -283,7 +283,7 @@ rules: then: action: block message: >- - Denied: Gate infrastructure cannot be modified by the assistant. + Blocked: Gate infrastructure cannot be modified by the assistant. Gate configuration is managed by humans in a terminal. Tell the human what command to run and they will execute it themselves. tags: [security, gate-protection] diff --git a/src/edictum/yaml_engine/templates/devops-agent.yaml b/src/edictum/yaml_engine/templates/devops-agent.yaml index 51ad5cb4..7410496d 100644 --- a/src/edictum/yaml_engine/templates/devops-agent.yaml +++ b/src/edictum/yaml_engine/templates/devops-agent.yaml @@ -3,7 +3,7 @@ kind: Ruleset metadata: name: devops-agent - description: "Contracts for DevOps agents. Prod gates, ticket requirements, PII detection." + description: "Rules for DevOps agents. Production gates, ticket requirements, and PII detection." defaults: mode: enforce @@ -17,7 +17,7 @@ rules: contains_any: [".env", ".secret", "kubeconfig", "credentials", ".pem", "id_rsa"] then: action: block - message: "Sensitive file '{args.path}' denied." + message: "Sensitive file '{args.path}' blocked." tags: [secrets, dlp] - id: block-destructive-bash @@ -30,7 +30,7 @@ rules: - args.command: { contains: '> /dev/' } then: action: block - message: "Destructive command denied: '{args.command}'." + message: "Destructive command blocked: '{args.command}'." tags: [destructive, safety] - id: prod-deploy-requires-senior diff --git a/src/edictum/yaml_engine/templates/file-agent.yaml b/src/edictum/yaml_engine/templates/file-agent.yaml index ab507099..3232df31 100644 --- a/src/edictum/yaml_engine/templates/file-agent.yaml +++ b/src/edictum/yaml_engine/templates/file-agent.yaml @@ -3,7 +3,7 @@ kind: Ruleset metadata: name: file-agent - description: "Contracts for file-handling agents. Blocks sensitive reads and destructive bash." + description: "Rules for file-handling agents. Block sensitive reads and destructive bash." defaults: mode: enforce @@ -17,7 +17,7 @@ rules: contains_any: [".env", ".secret", "kubeconfig", "credentials", ".pem", "id_rsa"] then: action: block - message: "Sensitive file '{args.path}' denied." + message: "Sensitive file '{args.path}' blocked." tags: [secrets, dlp] - id: block-destructive-bash @@ -30,7 +30,7 @@ rules: - args.command: { contains: '> /dev/' } then: action: block - message: "Destructive command denied: '{args.command}'." + message: "Destructive command blocked: '{args.command}'." tags: [destructive, safety] - id: block-write-outside-target @@ -41,5 +41,5 @@ rules: starts_with: / then: action: block - message: "Write to absolute path '{args.path}' denied. Use relative paths." + message: "Write to absolute path '{args.path}' blocked. Use relative paths." tags: [write-scope] diff --git a/src/edictum/yaml_engine/templates/nanobot-agent.yaml b/src/edictum/yaml_engine/templates/nanobot-agent.yaml index 5bf62d27..b7eb0807 100644 --- a/src/edictum/yaml_engine/templates/nanobot-agent.yaml +++ b/src/edictum/yaml_engine/templates/nanobot-agent.yaml @@ -3,7 +3,7 @@ kind: Ruleset metadata: name: nanobot-agent - description: "Governance rules for nanobot AI agents" + description: "Behavior rules for nanobot AI agents" defaults: mode: enforce @@ -72,7 +72,7 @@ rules: timeout: 300 timeout_action: block - # Deny writing outside allowed paths + # Block writing outside allowed paths - id: block-write-outside-workspace type: pre tool: write_file @@ -83,7 +83,7 @@ rules: action: block message: "Cannot write outside workspace: {args.path}" - # Deny editing outside allowed paths + # Block editing outside allowed paths - id: block-edit-outside-workspace type: pre tool: edit_file @@ -94,7 +94,7 @@ rules: action: block message: "Cannot edit outside workspace: {args.path}" - # Deny reading sensitive files + # Block reading sensitive files - id: block-sensitive-reads type: pre tool: read_file diff --git a/src/edictum/yaml_engine/templates/research-agent.yaml b/src/edictum/yaml_engine/templates/research-agent.yaml index b5ba431f..cc856041 100644 --- a/src/edictum/yaml_engine/templates/research-agent.yaml +++ b/src/edictum/yaml_engine/templates/research-agent.yaml @@ -3,7 +3,7 @@ kind: Ruleset metadata: name: research-agent - description: "Contracts for research agents. Rate limits and output caps." + description: "Rules for research agents. Rate limits and output caps." defaults: mode: enforce @@ -17,7 +17,7 @@ rules: contains_any: [".env", ".secret", "credentials"] then: action: block - message: "Sensitive file '{args.path}' denied." + message: "Sensitive file '{args.path}' blocked." tags: [secrets] - id: pii-in-output