Styx 的 SQLite 只追加雜湊鏈審計日誌與驗證腳本
An append-only, hash-chained audit log in SQLite, and a script to check it
Styx 以本機 SQLite 建立只追加、雜湊鏈式審計日誌,記錄編碼智能體申請及使用生產環境存取權等事件,並附有可用 Node 執行的驗證腳本。每列記錄行為者、目標、範圍、時限、工作階段及觸發原因等資訊,寫入前會清除看似秘密的內容。修改任一列或刪除中間列會令雜湊鏈斷裂,但有權寫入檔案的人仍可重寫整份日誌並重算雜湊;Styx 也不是沙盒,無法阻止刻意繞過其工具的智能體。
When a coding agent can ask for production access, the question afterwards is simple: who touched production, with what, and why? Styx answers it with an audit log on your machine. This post is about how that log is built, what it does and doesn't protect against, and a short script you can run to verify it.
What goes in
Every time an agent asks for access, and every time access is granted, used, denied, revoked or expires, one row is written. Connecting or removing a target, changing a policy and opening a pull request are rows too. Each row carries:
- the actor: you, the system (a policy or a timer), or the agent by name
- the target, the scope and the duration
- the agent session and the worktree it came from
- what triggered it: for a use, the command itself, like
$ vercel deploy --prod - the policy that decided it, if one did
- detail such as the agent's reason, and whether you verified with Touch ID, Windows Hello or your password
Before a row is written, anything shaped like a secret is scrubbed out of commands, reasons and labels, so a token an agent pasted into a command line doesn't end up in the log.
Append-only means the database says no
The log is a table in the local SQLite database. Triggers on the table abort any update or delete. Nothing in the app edits a row, and rows are only written through one function, AuditService.append. When a grant is revoked, Styx doesn't change the 'granted' row; it adds a 'revoked' row.
The chain
Each row has a sequence number, the hash of the row before it (prev_hash) and its own hash: a SHA-256 of the previous hash and the row's contents. Change any field of any row, or remove one from the middle, and the chain breaks from that point on.
Verifying it yourself
The app has no verify button yet, but the data is just a table. Export it read-only with sqlite3 (3.33 or later, for -json) and pipe it into Node:
sqlite3 -readonly -json ~/Library/Application\ Support/Styx/styx.db \
"SELECT * FROM audit_entries ORDER BY seq" | node verify-audit.mjs
With this saved as verify-audit.mjs:
import { createHash } from 'node:crypto';
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const rows = JSON.parse(Buffer.concat(chunks).toString('utf8') || '[]');
const camel = (k) => k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
let prev = '';
for (const r of rows) {
const row = {};
for (const [k, v] of Object.entries(r)) if (k !== 'hash') row[camel(k)] = v;
const canonical = JSON.stringify(row, Object.keys(row).sort());
const hash = createHash('sha256').update(prev).update('\n').update(canonical).digest('hex');
if (row.prevHash !== prev || hash !== r.hash) {
console.log(`Chain broken at entry ${row.seq}`);
process.exit(1);
}
prev = r.hash;
}
console.log(`Chain intact: ${rows.length} entries`);
It prints Chain intact with the number of entries, or the first row where the chain breaks. The database is at ~/Library/Application Support/Styx/styx.db on a Mac, %APPDATA%\Styx\styx.db on Windows and ~/.config/Styx/styx.db on Linux.
What the chain can't do
The chain shows that rows weren't edited one at a time after the fact. It doesn't stop someone who can write to your files from rewriting the whole log and recomputing every hash. If that matters to you, copy the latest hash somewhere else now and then; a later rewrite of the history before it won't match.
It's also a record, not a barrier. Styx is a guardrail, not a sandbox: agents run as you, and one that deliberately goes around Styx's tools isn't stopped by the log or by anything else in it. What the log gives you is a trustworthy account of everything that did go through Styx.
The code is Apache-2.0 at github.com/NicholasFlemmer/styx-app.
來源:DEV Community · dev.to