|
| 1 | +# writ |
| 2 | + |
| 3 | +**Codification gate and Merkle audit chain for Go agents.** |
| 4 | + |
| 5 | +``` |
| 6 | +go get github.com/opskernel-io/writ |
| 7 | +``` |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## The problem |
| 12 | + |
| 13 | +Autonomous coding agents run with whatever permissions you gave them at setup time. There is no runtime gate that asks "is this call within policy?" before the LLM acts, and standard observability logs can be edited after the fact. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## What writ does |
| 18 | + |
| 19 | +- **Blocks unauthorized agent calls before execution** — the codification gate intercepts every outbound LLM API call at the process level and evaluates it against your OPA policy before it leaves |
| 20 | +- **Logs every agent decision in a tamper-evident chain** — SHA-256-linked Merkle chain; altering any entry breaks verification from that point forward |
| 21 | +- **Routes tasks to the cheapest model tier that can handle them** — Ollama (free/local) → Claude Sonnet → Claude Opus, enforced by policy |
| 22 | +- **Reloads policy without restarting** — OPA hot-reload via file watch; policy changes take effect on the next call |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## How it's different |
| 27 | + |
| 28 | +| | writ | Asqav | Snyk Agent Guard | Microsoft AGT | |
| 29 | +|---|---|---|---|---| |
| 30 | +| Pre-execution gate | **✓** | ✗ | ✗ | ✗ | |
| 31 | +| Tamper-evident audit | Merkle chain | Hash chain + RFC 3161 | Mutable session logs | ✗ | |
| 32 | +| OPA policy enforcement | **✓** | ✗ | private preview | ✓ | |
| 33 | +| Tiered dispatch | **✓** | ✗ | ✗ | ✗ | |
| 34 | +| Language | Go | Python | N/A | TypeScript | |
| 35 | +| License | Apache 2.0 + commercial | MIT | Commercial | MIT | |
| 36 | + |
| 37 | +**The key distinction from Asqav:** Asqav logs what happened after execution. writ controls what is allowed to happen before execution. These are different architectural positions: authorize-then-enforce vs. observe-and-record. In a regulated environment you need both; writ's gate is the piece no other tool ships. |
| 38 | + |
| 39 | +**The key distinction from Microsoft AGT:** The Agent Governance Toolkit has OPA enforcement and Ed25519 signing. It has no tamper-evident audit log. writ's Merkle chain fills that specific gap. |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## EU AI Act Article 12 |
| 44 | + |
| 45 | +EU AI Act Article 12 requires automatic, technical, tamper-evident logging over the lifetime of high-risk AI systems. Enforcement begins August 2, 2026. |
| 46 | + |
| 47 | +**Three requirements met in full:** |
| 48 | +- Automatic logging — the gate emits chain entries without human action per call |
| 49 | +- Technical logging — machine-generated, structured, SHA-256-linked |
| 50 | +- Traceability — every gate decision links to its chain entry by AuditID |
| 51 | + |
| 52 | +**Three requirements partially met (remediation paths defined, days not weeks):** |
| 53 | +- Tamper-evident — SHA-256 chain detects alteration; filesystem-level write protection (`chattr +a` + `writ.ChainProtected()`) closes the gap |
| 54 | +- Lifetime coverage — chain is append-only within a process run; cross-restart hash linking closes the gap |
| 55 | +- Granularity — input hashes logged by default; `StoreFullInputs: true` opt-in logs the full input data |
| 56 | + |
| 57 | +**Two named gaps:** |
| 58 | +- **RFC 3161 timestamping** — chain timestamps are machine-generated but not externally signed. Integration roadmap: `github.com/digitorus/timestamp`. Commercial tier: hosted eIDAS-compliant timestamping. |
| 59 | +- **6-month retention management** — the open-source core writes to local storage with no managed expiry. Commercial-tier feature. |
| 60 | + |
| 61 | +writ-core is suitable for teams building toward Article 12 compliance. The three partial requirements close with days of engineering. The commercial tier closes the two named gaps. |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## Quick start |
| 66 | + |
| 67 | +```go |
| 68 | +import ( |
| 69 | + "github.com/opskernel-io/writ" |
| 70 | + "github.com/anthropics/anthropic-sdk-go" |
| 71 | +) |
| 72 | + |
| 73 | +// Initialize — wraps your anthropic.Client with gate + audit. |
| 74 | +// One-line change at construction time; the rest of your agent code stays the same. |
| 75 | +client, err := writ.New(writ.Config{ |
| 76 | + PolicyPath: "/etc/writ/policy.rego", // OPA Rego bundle directory |
| 77 | + AuditPath: "/var/writ/audit.chain", // Merkle chain JSONL file |
| 78 | + CallerID: "myagent-v1", // optional stable agent identifier |
| 79 | +}) |
| 80 | +if err != nil { |
| 81 | + log.Fatal(err) |
| 82 | +} |
| 83 | + |
| 84 | +// LLM calls go through the gate automatically. |
| 85 | +// Allowed calls are audited and dispatched; denied calls return *writ.DenialError. |
| 86 | +msg, err := client.Messages.New(ctx, anthropic.MessageNewParams{ |
| 87 | + Model: anthropic.ModelClaude_Sonnet_4_6, |
| 88 | + MaxTokens: 1024, |
| 89 | + Messages: anthropic.F([]anthropic.MessageParam{ |
| 90 | + anthropic.NewUserMessage(anthropic.NewTextBlock("refactor the auth module")), |
| 91 | + }), |
| 92 | +}) |
| 93 | +if err != nil { |
| 94 | + var denial *writ.DenialError |
| 95 | + if errors.As(err, &denial) { |
| 96 | + log.Printf("blocked by policy: %s (audit_id=%s)", denial.Reason, denial.AuditID) |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +// Streaming calls work the same way. |
| 101 | +stream, err := client.Messages.NewStreaming(ctx, params) |
| 102 | +if err != nil { ... } |
| 103 | +defer stream.Close() // writes the streaming-complete audit entry |
| 104 | + |
| 105 | +// For tool use and other non-LLM events: |
| 106 | +client.Audit(writ.AuditEvent{ |
| 107 | + ActionType: "write_file", |
| 108 | + Actor: writ.ActorAgent, |
| 109 | + InputHash: writ.HashInput([]byte(filePath)), |
| 110 | + Result: "success", |
| 111 | +}) |
| 112 | + |
| 113 | +// Verify the chain (run in CI or on-demand audits): |
| 114 | +err = writ.Verify("/var/writ/audit.chain") |
| 115 | +``` |
| 116 | + |
| 117 | +**Helm (Kubernetes):** writ-core is a library dependency of your agent process, not a sidecar. Add to your agent container's Go dependencies. See `docs/helm/` for a reference values.yaml with policy ConfigMap mounting. |
| 118 | + |
| 119 | +--- |
| 120 | + |
| 121 | +## Supported agents |
| 122 | + |
| 123 | +v0 targets **Go agents that call `anthropic.Client` directly** — any agent built with `github.com/anthropics/anthropic-sdk-go`. Drop `writ.New()` in at the client construction site. |
| 124 | + |
| 125 | +Agents that proxy the Anthropic API through their own server (rather than calling the SDK directly from your Go process) require an alternative integration path. Integration guides for specific agents are on the roadmap. |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## Architecture |
| 130 | + |
| 131 | +``` |
| 132 | +┌─────────────────────────────────────────────────────────┐ |
| 133 | +│ Agent Process │ |
| 134 | +│ │ |
| 135 | +│ task ──► [writ codification gate] ──► LLM API call │ |
| 136 | +│ │ │ │ |
| 137 | +│ DENY (blocked) ALLOW (proceed) │ |
| 138 | +│ │ │ │ |
| 139 | +│ structured error [Merkle audit] │ |
| 140 | +│ │ │ |
| 141 | +│ [tiered dispatch] │ |
| 142 | +│ │ │ |
| 143 | +│ Ollama / Sonnet / Opus │ |
| 144 | +└─────────────────────────────────────────────────────────┘ |
| 145 | + │ │ |
| 146 | + [OPA policy] [audit chain] |
| 147 | + file watch append-only |
| 148 | + hot-reload tamper-evident |
| 149 | + │ │ |
| 150 | + /etc/writ/policy.rego /var/writ/audit.chain |
| 151 | +``` |
| 152 | + |
| 153 | +The gate sits between your agent's code and the LLM API call — not between your machine and the internet. It intercepts at the call site in-process. |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +## License |
| 158 | + |
| 159 | +The writ-core SDK is Apache 2.0. Use it, fork it, embed it, build commercial products on top of it. |
| 160 | + |
| 161 | +**What's in the commercial tier** ([writ.opskernel.io](https://writ.opskernel.io)): |
| 162 | +- Compliance dashboard — Article 12 audit report export (PDF + JSON) |
| 163 | +- Hosted RFC 3161 timestamping — eIDAS-compliant, third-party signed timestamps |
| 164 | +- 6-month retention management — cloud storage backend with compliance attestation |
| 165 | +- Multi-agent audit chains — cross-agent tracing across chained agent calls |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +## Roadmap |
| 170 | + |
| 171 | +- [ ] RFC 3161 timestamping integration (closes Article 12 gap 1) |
| 172 | +- [ ] 6-month retention management, commercial tier (closes Article 12 gap 2) |
| 173 | +- [ ] Multi-agent audit chains — single verifiable chain spanning Agent A → Agent B calls |
| 174 | +- [ ] Agent integration guides — Devin, Cursor, and other agents (requires OQ-5 investigation) |
| 175 | +- [x] `writ verify` CLI — standalone chain verification for CI pipelines |
| 176 | + |
| 177 | +--- |
| 178 | + |
| 179 | +## Related |
| 180 | + |
| 181 | +- **[hookd](https://opskernel.io)** — governs external AI traffic: MCP server verification, replay detection, per-source audit. writ governs what agents do internally; hookd governs what external servers agents call. |
| 182 | +- **[opskern-policy](https://github.com/OpsKern/opskern-policy)** — shared OPA Rego policy templates for hookd + writ. |
| 183 | +- **[Asqav](https://github.com/jagmarques/asqav-sdk)** — MIT Python SDK for agent audit trails with RFC 3161 timestamps. Complementary to writ (observe-and-record layer); writ adds the pre-execution gate that Asqav does not have. |
| 184 | +- **[EU AI Act Article 12 text](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32024R1689)** — the specific requirement writ's architecture addresses. |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +*writ is built by [OpsKern](https://opskern.io). Early access: [writ.opskernel.io](https://writ.opskernel.io)* |
0 commit comments