@sakk-dev/sakk 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,621 @@
1
- # Temporary Holding Version
1
+ # Sakk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **English** | [简体中文](README.zh-CN.md)
4
+
5
+ > **The original check, rebuilt for AI agents.**
6
+
7
+ Sakk stops a coding agent **before** it overspends, leaks a secret or
8
+ force-pushes: a fail-closed guard for Claude Code, Cursor and Codex, a daily
9
+ spend cap for Claude Code, a kill switch, and a spend ledger for your own agent
10
+ code. Local, zero dependencies, Apache-2.0.
11
+
12
+ ![Sakk's policy check allowing ls -la and denying rm -rf ~](docs/media/sakk-block.gif)
13
+
14
+ **I use Claude Code, Cursor or Codex:**
15
+
16
+ ```bash
17
+ npm install -g @sakk-dev/sakk && sakk guard install claude-code && sakk doctor # run in your project
18
+ ```
19
+
20
+ **My own AI app, any language:** Sakk's spend gate runs inside the LiteLLM
21
+ proxy, and your app changes one line. `sakk gateway init` needs LiteLLM
22
+ installed (the pinned 1.106.2); without it, it prints the hash-locked install
23
+ steps and writes nothing.
24
+
25
+ ```bash
26
+ npm install -g @sakk-dev/sakk && sakk init --cap 5 # the ledger the gateway charges, capped at $5
27
+ sakk gateway init # writes sakk_gateway.py, config.yaml and start.sh to ~/.sakk/gateway
28
+ LITELLM_MASTER_KEY=<a long random key> DEEPSEEK_API_KEY=<your key> ~/.sakk/gateway/start.sh
29
+ ```
30
+
31
+ Then point the app at the gateway, and send `LITELLM_MASTER_KEY` as its API
32
+ key. The OpenAI SDKs for Python and Node read this variable:
33
+
34
+ ```bash
35
+ OPENAI_BASE_URL=http://127.0.0.1:4000/v1
36
+ ```
37
+
38
+ Or in code:
39
+
40
+ ```python
41
+ import os
42
+ from openai import OpenAI
43
+
44
+ client = OpenAI(base_url="http://127.0.0.1:4000/v1", api_key=os.environ["LITELLM_MASTER_KEY"])
45
+ ```
46
+
47
+ ```js
48
+ import OpenAI from 'openai';
49
+
50
+ const client = new OpenAI({
51
+ baseURL: 'http://127.0.0.1:4000/v1',
52
+ apiKey: process.env.LITELLM_MASTER_KEY,
53
+ });
54
+ ```
55
+
56
+ ```bash
57
+ curl -s http://127.0.0.1:4000/v1/chat/completions \
58
+ -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
59
+ -d '{"model":"deepseek-chat","max_tokens":512,"messages":[{"role":"user","content":"hi"}]}'
60
+ ```
61
+
62
+ If the gateway cannot get an answer from the spend ledger, it lets the call
63
+ through by default and says so: a WARNING in the proxy's log, an
64
+ `x-sakk-status: unmetered` header on the response, a red banner in `sakk ui`
65
+ and a line in `sakk status`. A call let through this way is not recorded in
66
+ the ledger and not held to the cap. Start the gateway with
67
+ `SAKK_ON_UNAVAILABLE=deny` to refuse those calls instead. A deliberate refusal
68
+ is always a refusal: 402 when the budget is exceeded, 403 for a model with no
69
+ price, 400 for a malformed request. This is the gateway only; the coding-agent guard and the spend ledger
70
+ for your own code stay fail-closed. Prices, refusals and limits:
71
+ [integrations/litellm/README.md](integrations/litellm/README.md).
72
+
73
+ In medieval Arab commerce, a **ṣakk** (صك) was a written order of payment — a merchant's promise backed by real funds, honored across the trade routes. The word traveled through Persian and Latin into English as _cheque / check_. Its plural, _sukuk_, still names the Gulf's financial instruments today.
74
+
75
+ English kept both meanings: a **check** you write (money) and a **check** you run (verification). AI agents need exactly those two things, welded together. That is Sakk:
76
+
77
+ **a fail-closed spend ledger for AI agents — no reservation, no spawn; no proof, no pass.**
78
+
79
+ Questions this page answers:
80
+
81
+ - [How do I block dangerous commands like force-push from an AI coding agent?](#how-do-i-block-dangerous-commands-like-force-push-from-an-ai-coding-agent)
82
+ - [How do I stop Claude Code from spending too much?](#how-do-i-stop-claude-code-from-spending-too-much)
83
+ - [How do I stop an AI agent from leaking secrets?](#how-do-i-stop-an-ai-agent-from-leaking-secrets)
84
+ - [How do I give my own agent code a hard budget?](#quickstart-how-do-i-give-my-own-agent-code-a-hard-budget)
85
+ - [How do I stop a runaway AI agent?](#kill-switch-how-do-i-stop-a-runaway-ai-agent)
86
+ - [How do I put a budget gate in front of any agent?](#mcp-how-do-i-put-a-budget-gate-in-front-of-any-agent)
87
+ - [How is Sakk different from ccusage or Claude Code's /cost?](#how-is-sakk-different-from-ccusage-or-claude-codes-cost)
88
+
89
+ New here? Start with the [tutorial](docs/tutorial/en.md) or the [FAQ](docs/FAQ.md).
90
+
91
+ ## Why
92
+
93
+ Agents spend real money and report their own success. Both are problems:
94
+
95
+ - A retry loop at 3 a.m. is indistinguishable from a working agent — until the invoice.
96
+ - "All tests pass" from an agent that never ran the tests looks identical to the real thing.
97
+
98
+ The ledger closes the first gap. The eval harness in [`eval/`](eval/) grades the second from machine evidence (portable evidence packs are on the roadmap). Every potentially paid action must **reserve** against a capped ledger _before_ it runs, and **settle** with the real cost after. Everything pathological refuses:
99
+
100
+ | Situation | Sakk's answer |
101
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | Budget would reach the cap | **Refused.** The cap is a wall, not a target — reaching it counts as crossing it. |
103
+ | Same `callId` reserved twice | **Refused.** Deduplicated billing with re-executed side effects is how money leaks. |
104
+ | Ledger missing / corrupted / stale / wrong schema | **Refused.** A broken ledger is never "zero spend so far". Stale = not confirmed for 7 days; a person revives it with `sakk reconcile --by <name>`. |
105
+ | Actual cost unknowable after a failure | **No fake numbers.** The entry enters reconciliation, a global flag halts all further paid calls until a named human clears it. |
106
+ | Lock held by another process | **Refused after timeout.** The budget is never judged without mutual exclusion. |
107
+ | Ledger reached through a symlink or hard link | **Refused** (`LEDGER_PATH_NOT_CANONICAL`). A write would replace the link and fork the books; use the one real path. |
108
+ | Ordinary read failure | **Never auto-creates an empty ledger.** "Lost ledger equals free money" is the exact back door this tool exists to close. |
109
+
110
+ Two concurrent agents cannot jointly overspend: read-modify-write runs under an atomic lock, writes are temp+rename, and a real race proves it: eight processes started together reserve against one cap, and every grant must be booked with nothing past the cap (`test/ledger.test.mjs`, "8 concurrent processes cannot jointly overspend"; mutation drill MUT-Z removes the lock and the race goes red).
111
+
112
+ ## Install
113
+
114
+ ```bash
115
+ npm install @sakk-dev/sakk # or call the CLI without installing: npx @sakk-dev/sakk …
116
+ ```
117
+
118
+ ## How do I block dangerous commands like force-push from an AI coding agent?
119
+
120
+ ```bash
121
+ sakk guard install claude-code # or: cursor, codex
122
+ sakk doctor # checks the hook really fires
123
+ sakk try 'git push --force origin main' # what the guard would answer
124
+ ```
125
+
126
+ Every Edit, Write and shell call of the agent is judged before it runs:
127
+ force pushes, deletes outside the project, reading secrets and deploys are
128
+ refused or asked by a policy you can read and change. A write whose content is
129
+ a secret, or a command that prints one, is denied too. The guard judges a
130
+ command by what it spells out — see the limits at the end of this page and
131
+ [docs/GUARD.md](docs/GUARD.md).
132
+
133
+ Or load Sakk into Claude Code as a **plugin** — it bundles the same hook,
134
+ fail-closed (`onFailure: block`), and sees every tool. The repository is its
135
+ own plugin marketplace, so inside Claude Code:
136
+
137
+ ```text
138
+ /plugin marketplace add sakk-dev/sakk
139
+ /plugin install sakk@sakk
140
+ ```
141
+
142
+ To load it for one session from the npm package instead:
143
+
144
+ ```bash
145
+ npm install -g @sakk-dev/sakk
146
+ claude --plugin-dir "$(npm root -g)/@sakk-dev/sakk"
147
+ ```
148
+
149
+ No automated test can drive Claude Code, so both routes are checked by hand
150
+ (owner run P-INS-008 in [docs/V2-BACKLOG.md](docs/V2-BACKLOG.md)).
151
+
152
+ See what the guard did in a local, read-only console — today's spend and cap,
153
+ recent decisions, the policy with a try box, and secret blocks:
154
+
155
+ ```bash
156
+ sakk ui # prints http://127.0.0.1:<port>/?t=… — read-only; Ctrl+C to stop
157
+ sakk ui --set-password # optional: a username and password for http://127.0.0.1:<port>/
158
+ sakk ui --clear-password # back to the token address only
159
+ ```
160
+
161
+ A password sign-in reads the console; stopping an agent still needs
162
+ `--allow-halt` and the token address. Restart `sakk ui` after setting or
163
+ clearing it.
164
+
165
+ ## How do I stop Claude Code from spending too much?
166
+
167
+ Cap the day's spend (read from Claude Code's local logs — an estimate, not the invoice). A person sets it. The guard denies the agent a `sakk cap` command and a write to `~/.sakk`, and a one-command symlink chain that would reach it needs a person's approval. At the terminal, lifting the cap, raising it or moving its day asks you to retype a word; with no terminal it refuses with `HUMAN_CONFIRMATION_REQUIRED` and changes nothing. A path held in a shell variable is not caught yet — see the limits at the end of this page:
168
+
169
+ ```bash
170
+ sakk cap set 5 --zone Asia/Dubai # deny paid calls once today's spend reaches $5
171
+ sakk spend # today's spend by model
172
+ sakk cap off # lift it (asks you to confirm)
173
+ sakk prices # the price table in use, and where it came from
174
+ ```
175
+
176
+ The per-token rates ship in `prices/snapshot.json`, regenerated from the
177
+ public [LiteLLM price table](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
178
+ (MIT) — the file's `source` block records the exact commit and SHA-256 it was
179
+ built from, and `sakk prices` prints that provenance. They are community
180
+ figures, an estimate and not any provider's invoice. A model the table does
181
+ not list is **not guessed**: it prices to nothing, and the cap then fails
182
+ closed for the day (`PRICE_UNKNOWN`) rather than undercount your spend. To
183
+ override a rate or add a model, drop a `sakk-prices.v1` table at
184
+ `~/.sakk/prices.json` (a user table wins over the shipped one, and a malformed
185
+ one is refused, never silently ignored); maintainers refresh the shipped
186
+ snapshot by re-running `scripts/prices-trim.mjs` against the LiteLLM table.
187
+
188
+ ## How do I stop an AI agent from leaking secrets?
189
+
190
+ Two layers, with the same rules:
191
+
192
+ - **In the agent's hook.** With the guard installed, a write whose content is
193
+ a secret, or a command that prints one, is denied before it runs, and the
194
+ default policy refuses reading secret files such as `.env`. The console's
195
+ secret-blocks panel shows what was stopped.
196
+ - **In git.** `sakk scan` checks files and staged commits, and
197
+ `--install-hook` refuses a commit that contains a secret.
198
+
199
+ ### Secret scanning
200
+
201
+ ```bash
202
+ npm install --save-dev @sakk-dev/sakk # the hook runs THIS installed copy — not npx
203
+ npx sakk scan src config # scan paths; exit 1 if anything is found
204
+ npx sakk scan --install-hook # refuse commits and merge commits that contain secrets
205
+ ```
206
+
207
+ It recognizes eight formats: database URLs with a password (`postgres`,
208
+ `postgresql`, `mysql`, `mariadb`, `mongodb`, `mongodb+srv`, `redis`, `rediss`,
209
+ `amqp`, `amqps`, `mssql`, `sqlserver`), URL-encoded passwords in any URL,
210
+ AWS access keys, API keys shaped `sk_live_…`/`sk_test_…`/`rk_live_…`/`rk_test_…`,
211
+ `sk-`/`sk_`/`rk-`/`rk_` followed by at least 20 characters that include a
212
+ digit, or `sk-` followed by at least 40 (so `sk_set_flags` does not match,
213
+ but a long identifier that starts `sk_` and contains a digit does), GitHub
214
+ tokens (classic and fine-grained), private-key blocks (PEM and PGP), bearer tokens, and
215
+ quoted `password = "…"` assignments. It does NOT catch secrets in any other
216
+ shape — an unquoted `.env` line such as `PASSWORD=hunter2hunter2`, other URL
217
+ schemes, a bare random string, personal data. A reference to a secret in
218
+ the secret's place (`${REDIS_PASSWORD}`, `$PW`, `%(pw)s`, `{{ pw }}`) is not
219
+ reported. A clean scan means "none of these formats", not "no secrets".
220
+
221
+ Findings print the rule, path, line and a 12-character keyed fingerprint —
222
+ never the secret. The key is random per run; set `SAKK_SCAN_KEY` (and keep
223
+ it private) when you need to compare fingerprints across runs. A file name
224
+ that itself contains a secret is printed redacted and counted as a finding;
225
+ control characters in names are printed escaped. The only exemption is a
226
+ secret that declares itself fake by containing
227
+ `TEST_ONLY_FAKE_SECRET_DO_NOT_USE` with nothing secret-shaped beside it;
228
+ there is no localhost exception.
229
+
230
+ Exit codes: 0 clean, 1 findings, 2 the scan could not do its job — a path
231
+ that does not exist (`SCAN_PATH_MISSING`), a file it could not read, a fifo
232
+ or device, or anything over 256 MB (`SCAN_UNREADABLE`), not a git repository,
233
+ git missing, an existing or unwritable hook, `--staged` combined with paths
234
+ (`SCAN_USAGE`). Paths you name are always scanned (even `dist/`, even
235
+ through a symlink); inside them, `node_modules`, `.git`, `.next`, `dist`,
236
+ symlinks, binary files and files named like media or archives (`.png`,
237
+ `.jpg`, `.gif`, `.zip`, `.gz`, `.tgz`, `.woff`, `.ico`, `.mp4`, `.bundle` —
238
+ by name, not content) are skipped and the skips are counted in the
239
+ summary. UTF-16 and UTF-32 files (with a byte-order mark) are scanned as
240
+ text.
241
+
242
+ `--staged` scans exactly what the commit would record — every added,
243
+ modified or type-changed blob in the index, wherever you run it from;
244
+ a staged symlink's target counts as content (replace refs are ignored: the
245
+ bytes scanned are the bytes the commit records). `--install-hook` writes
246
+ pre-commit and pre-merge-commit hooks (git 2.24+) that run the installed
247
+ sakk by absolute path. No git hook sees fast-forward merges, rebases,
248
+ cherry-picks or `--no-verify` — run `sakk scan .` in CI as well. It
249
+ refuses if either hook already exists (even a dangling symlink) and then
250
+ installs neither, and it refuses to install from a temporary `npx`,
251
+ `bunx` or `pnpm dlx` cache (`SCAN_HOOK_EPHEMERAL`) — that copy disappears
252
+ when the cache is cleaned. The hook pins node's real path: after a node
253
+ upgrade (Homebrew, nvm) or moving sakk, commits refuse with
254
+ `SCAN_HOOK_STALE` until you re-run `--install-hook`.
255
+
256
+ ## Quickstart: how do I give my own agent code a hard budget?
257
+
258
+ ```bash
259
+ npx sakk init --cap 25 # create ~/.sakk/sakk-ledger.json (refuses to overwrite) and wire it to the guard
260
+ npx sakk status # totals, entries, reconciliation state (--json for machines)
261
+ npx sakk reconcile --by alice # clear a reconciliation halt — takes a name
262
+ npx sakk ledger verify # check the sealed history's hash chain (exit 1 if broken)
263
+ ```
264
+
265
+ The ledger commands (`init`, `status`, `reconcile`, `settle`, `halt`,
266
+ `resume`, `unlock`, `mcp`, `ledger verify|export`) accept `--ledger <path>`. Without
267
+ it they use the absolute path `sakk init` recorded in `~/.sakk/config.json`
268
+ (the guard hook reads the same entry), then `SAKK_LEDGER_PATH`
269
+ (set-but-empty refuses), then `~/.sakk/sakk-ledger.json` — out of the
270
+ project tree the agent writes. A leftover `./sakk-ledger.json` in the
271
+ current directory refuses with `LEDGER_PATH_LEGACY` until you pass
272
+ `--ledger` or move it, so a halt never lands in a file nothing reads.
273
+ `mcp` still needs the flag or the variable.
274
+
275
+ Settled entries are sealed into an append-only, hash-chained cold trail
276
+ (`<ledger>.cold/`) so the live file stays small and a call costs the same
277
+ whatever the history; nothing is capped and no entry is dropped.
278
+ Each call checks the summary against the newest segment, so an edited total,
279
+ or a lost or rolled-back newest segment, refuses `LEDGER_ARCHIVE_BROKEN`;
280
+ `sakk ledger verify` walks the whole chain (an older segment's loss shows
281
+ there), `sakk ledger export` rebuilds one flat ledger. At most
282
+ 10,000 reservations may be outstanding at once.
283
+
284
+ `sakk status` exits 3 while a person must act before spending resumes —
285
+ reconciliation required (flag or entry), or a stale ledger — and 4 while
286
+ globally halted; gate your CI on it. `max_reservable_usd` is 0 whenever
287
+ every reserve would refuse.
288
+
289
+ In code:
290
+
291
+ ```js
292
+ import { initLedger, reserve, settle, LedgerError } from 'sakk';
293
+
294
+ const ledgerPath = './ledger.json';
295
+ initLedger({ ledgerPath, capUsd: 25 }); // explicit — ledgers are never created implicitly
296
+
297
+ // before every paid action. reserve() THROWS a LedgerError on refusal
298
+ // (e.code names the reason) — if it returns, the money is reserved:
299
+ try {
300
+ reserve({ ledgerPath, callId: 'task-42:call-1', estimatedMaxUsd: 1.5 });
301
+ } catch (e) {
302
+ if (e instanceof LedgerError) return skipThisAction(e.code); // do NOT call the model
303
+ throw e;
304
+ }
305
+ const result = await callYourModel();
306
+ settle({ ledgerPath, callId: 'task-42:call-1', actualUsd: result.costUsd });
307
+
308
+ // if the cost could not be determined:
309
+ settle({ ledgerPath, callId: 'task-42:call-1', failedUnknownCost: true });
310
+ // → ledger enters RECONCILIATION_REQUIRED; every further paid call refuses.
311
+ // To recover, a human first settles the open entry with the figure they
312
+ // decided on (the invoice amount, or a write-off) — clearing refuses while
313
+ // any entry is still open (RECONCILIATION_ENTRIES_OPEN):
314
+ settle({ ledgerPath, callId: 'task-42:call-1', actualUsd: 0.4 });
315
+ clearReconciliation({ ledgerPath, by: 'your-name' });
316
+ ```
317
+
318
+ Zero dependencies. Plain Node (≥20) on **macOS or Linux** (the lock and the
319
+ tooling are POSIX; Windows is not supported yet). The ledger is a
320
+ human-readable JSON file; alerts append to a greppable `sakk-alerts.jsonl`
321
+ next to it (never written through a planted symlink). Writes are fsync'd
322
+ before an atomic rename. On Linux that survives power loss; on macOS fsync
323
+ does not flush the drive cache (Node has no F_FULLFSYNC), so a power cut can
324
+ lose the last writes there.
325
+
326
+ The API is deliberately **synchronous** (lock waits block up to ~3s): correct
327
+ for CLIs, hooks and per-task processes. Inside a long-running async server,
328
+ call it from a worker thread. Ledgers are single JSON files scanned per call
329
+ — right for hundreds-to-thousands of entries per ledger; archive or rotate
330
+ beyond that (a hash-chained ledger mode — enterprise, on the roadmap — is
331
+ the designed scale path).
332
+
333
+ Practical notes, earned the hard way:
334
+
335
+ - **`estimatedMaxUsd`** = the worst case the provider can bill for this one
336
+ call (e.g. max output tokens × price, or the provider's own budget cap
337
+ parameter). Too high parks budget; too low is dishonest — the cap check
338
+ runs against estimates until settlement corrects them.
339
+ - **Crash between reserve and settle?** The reservation stays outstanding
340
+ and keeps occupying the cap — correctly, because the money may genuinely
341
+ have been spent. Recover by settling the **same `callId`** from any
342
+ process: `settle({ ledgerPath, callId, actualUsd })` once you know the
343
+ figure, or `failedUnknownCost: true` if it is unknowable (which halts the
344
+ ledger for a human). When the figure is unknown and the worst case is an
345
+ acceptable answer, a person can run `sakk settle <callId> --by <name>` at a
346
+ terminal instead: it books the reserved amount and writes a
347
+ `SETTLED_AT_RESERVED` alert naming them (F-242). The LiteLLM gateway does
348
+ this itself for its own reservations once they are an hour old.
349
+ - **`markReconciliationRequired`** is for out-of-band discoveries — an
350
+ invoice that disagrees with the books, a provider incident. It halts all
351
+ further reservations until a named human clears the flag.
352
+
353
+ If a process crashes while holding the lock, the next caller usually detects
354
+ the dead owner (same host) and breaks the lock automatically. A crash in the
355
+ few microseconds while the lock is being created or released leaves a lock
356
+ it cannot judge (measured: about 3 in 100 SIGKILLs); that stays locked —
357
+ fail-closed — until `npx sakk unlock --by <your-name>`. One ledger directory
358
+ must not be shared across hosts: the dead-owner check compares hostnames.
359
+
360
+ **Auditors**: [docs/AUDITOR_REPLAY.md](docs/AUDITOR_REPLAY.md) — what you can
361
+ verify on your own clean machine in under 30 minutes, without trusting us.
362
+
363
+ ## API
364
+
365
+ `initLedger · reserve · settle · halt · resume · markReconciliationRequired · clearReconciliation · forceUnlock · readLedger · totalsOf · LedgerError (every refusal carries .code and .details)`
366
+
367
+ Accounting rule: `settled actuals + outstanding reservations (at estimated max) + this estimate >= cap ⇒ refuse`.
368
+
369
+ ### `sakk/policy` — the decision kernel
370
+
371
+ ```js
372
+ import { loadPolicy, decide, logDecision, PolicyError } from 'sakk/policy';
373
+ ```
374
+
375
+ A declarative, hash-pinned answer to _"may this agent do this action at this
376
+ cost?"_, decided **before** any reservation exists. `decide()` is pure: same
377
+ policy bytes + same context → same verdict on any machine, and every logged
378
+ decision carries the SHA-256 of the policy file that produced it — "why was
379
+ this allowed on March 3rd?" is answered by replay, not by trust. Policy files
380
+ (`sakk-policy.v1`, plain JSON) read top-to-bottom, **first match wins**, the
381
+ `default_verdict` is mandatory, and everything pathological refuses:
382
+ `POLICY_VACUOUS_ALLOW` (a gate that can only say yes: no rule that refuses
383
+ under a default allow, or a first rule that allows everything),
384
+ `POLICY_RULE_SHADOWED` (provably dead rules), `POLICY_SCHEMA_INVALID` (also
385
+ for a `*` inside an agent or action name — only the exact `*` is a
386
+ wildcard), `CONTEXT_INVALID` (no coercion — and no path aliases: a
387
+ non-canonical scope like `prod//x` refuses outright rather than sliding
388
+ past a deny), `POLICY_TOO_COMPLEX` (bounded by refusal, never by hanging),
389
+ `DECISION_UNLOGGABLE` (an unauditable allow is a deny).
390
+
391
+ Two limits to know before relying on a deny:
392
+
393
+ - **Under `default_verdict: allow`, a deny constrains only what the caller
394
+ declares.** A rule on `scope` or `action` does not match a request that
395
+ omits that field, so the request falls through to the allow default. Use
396
+ a deny default (like `policies/guard-default.json`, the guard's own
397
+ policy) when the caller is the party being constrained.
398
+ - **Paths are compared byte for byte.** The kernel and the MCP enforcer do
399
+ not fold case, resolve symlinks or normalize unicode: on a
400
+ case-insensitive volume (the macOS default) `Prod/api` is `prod/api` on
401
+ disk but not to the policy. Normalize before calling `decide()`, or use a
402
+ deny default. Two reference policies ship in
403
+ [`policies/`](policies/) — a permissive dev profile and the guard's
404
+ deny-default profile; regulated profiles and case packs are kept for the
405
+ enterprise edition (coming soon). The kernel never imports the ledger and never performs
406
+ actions; an enforcer composes the two: `decide()` → on `allow` only →
407
+ `reserve()`.
408
+
409
+ ## Kill switch: how do I stop a runaway AI agent?
410
+
411
+ ```bash
412
+ npx sakk halt --by alice --reason "runaway agent" # every new reservation refuses
413
+ npx sakk halt --by alice --agent bot-7 # stop one agent only
414
+ npx sakk resume --by alice # lift it (named)
415
+ ```
416
+
417
+ `sakk init` records the ledger's absolute path in `~/.sakk/config.json`,
418
+ so a halt also stops an agent behind the installed guard: every tool call
419
+ the guard sees, except reading the repository, is denied until `sakk resume`.
420
+ For Claude Code, `sakk guard install claude-code` routes shell commands, file
421
+ reads and edits, Monitor and MCP tools through the guard; Glob, Grep,
422
+ WebFetch, WebSearch and subagents (Task) do not pass through it. Loaded as a
423
+ plugin, the guard sees every tool. Running `init`
424
+ again wires an existing ledger without touching it, and never replaces a
425
+ different recorded path — it says which ledger the hook still reads.
426
+ Because it decides where the hook reads halts from, the guard denies the
427
+ agent `sakk init`, as it denies `sakk cap`; a person runs it.
428
+
429
+ While halted, `reserve()` refuses `LEDGER_HALTED` (or `AGENT_HALTED`),
430
+ every reservation the halt refuses is logged to `sakk-alerts.jsonl`
431
+ (attempts refused earlier for another reason — an invalid agent id, a stale
432
+ ledger — carry their own code and are not halt alerts), and `sakk status`
433
+ shows who stopped it and, for a global halt, exits 4 — so CI stops too
434
+ (per-agent halts are listed, exit 0: other agents may still spend). In-flight calls can
435
+ still settle their real cost. The halt cannot be lifted by any money
436
+ operation, only by `resume`; agents cannot resume themselves over MCP. A
437
+ stale ledger or one in reconciliation can still be halted. Per-agent halts
438
+ match the agent id exactly (case-sensitive, code point for code point — no
439
+ Unicode normalisation, so `café` typed two ways is two ids) and rely on the
440
+ id the caller
441
+ declares when it reserves — an agent that switches ids escapes a per-agent
442
+ halt; the global halt is the hard stop. The first halt upgrades the ledger
443
+ file to schema `sakk-ledger.v2`, which earlier sakk versions refuse to read:
444
+ an old binary can never spend through a stop it does not understand.
445
+
446
+ ## MCP: how do I put a budget gate in front of any agent?
447
+
448
+ `sakk mcp` speaks the Model Context Protocol over stdio — the same three
449
+ operations as the library, as tools an agent must call. Claude Code
450
+ config (Cursor/Copilot analogous):
451
+
452
+ ```json
453
+ {
454
+ "mcpServers": {
455
+ "sakk": {
456
+ "command": "npx",
457
+ "args": [
458
+ "sakk",
459
+ "mcp",
460
+ "--ledger",
461
+ "/abs/path/ledger.json",
462
+ "--policy",
463
+ "/abs/path/policy.json"
464
+ ]
465
+ }
466
+ }
467
+ }
468
+ ```
469
+
470
+ The ledger path is REQUIRED (no cwd default — a server guessing where the
471
+ books live is not a gate). `--policy` (or `SAKK_POLICY_PATH`; set-but-empty
472
+ refuses) is optional; when set, every
473
+ `sakk_reserve` is decided by the policy kernel first and every decision —
474
+ allow and deny alike — lands hash-pinned in `<ledger>.decisions.jsonl` next to
475
+ the ledger. `require_approval` refuses over MCP by design (approval
476
+ collection is an enterprise concern, not an agent tool). The server adds
477
+ no semantics: replaying the same calls through the library yields the same
478
+ entries (callId, status, amounts — timestamps differ by construction), and
479
+ the test suite proves it. A policy `allow` is a decision, not a
480
+ reservation: the ledger can still refuse it (a zero estimate, for one), so
481
+ count reservations from the books, not allows from the decision log.
482
+
483
+ ## How is Sakk different from ccusage or Claude Code's /cost?
484
+
485
+ ccusage tells you, Sakk stops you. ccusage and Claude Code's `/cost` report
486
+ what an agent has spent; neither refuses a call. Sakk sits in the agent's hook
487
+ and refuses the next tool call once the day's estimated spend reaches your cap,
488
+ and refuses force pushes and secret leaks whatever the spend. Use them
489
+ together: a report for where the money went, a guard for the stop. Microsoft's
490
+ Agent Governance Toolkit covers a wider governance surface for agents built on
491
+ the frameworks it adapts. The fair, sourced comparison, including what Sakk
492
+ does not do: [docs/compare.md](docs/compare.md).
493
+
494
+ ## Docs
495
+
496
+ - [Tutorial](docs/tutorial/en.md) — first run, step by step
497
+ - [FAQ](docs/FAQ.md)
498
+ - [Comparison with ccusage, `/cost` and the Agent Governance Toolkit](docs/compare.md)
499
+ - [The agent guard](docs/GUARD.md) · [Spend and the daily cap](docs/SPEND.md) · [Architecture](docs/ARCHITECTURE.md)
500
+ - [Auditor replay](docs/AUDITOR_REPLAY.md) · [Roadmap](docs/ROADMAP.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
501
+
502
+ ## Roadmap — honest edition
503
+
504
+ The longer view, with what each next step depends on: [docs/ROADMAP.md](docs/ROADMAP.md).
505
+
506
+ - [x] CLI (`sakk init / status / reconcile / settle / unlock / halt / resume / scan / mcp`)
507
+ - [x] Eval harness: gold cases with mutation-tested scoring (the judge gets judged) — in the repository's [`eval/`](eval/), not in the npm package
508
+ - [x] Policy kernel (`sakk/policy`): declarative `sakk-policy.v1`, pure `decide()`, hash-pinned decision log
509
+ - [x] MCP server enforcer (`sakk mcp`): reserve/settle/status as tools, policy-gated
510
+ - [x] Kill switch (`sakk halt / resume`): named (self-declared `--by`), global or per agent; stop state provable from the books
511
+ - [x] Secret scanning (`sakk scan`, pre-commit hook): eight hand-written formats plus 192 of gitleaks' 195 rules (194 ported, 192 enabled), fingerprints only
512
+ - [x] Secret guard in the hook: a write whose content is a secret, or a command that prints one, is denied (open-source v1 M3; the same rules as `sakk scan`)
513
+ - [x] Daily spend cap enforced by the hook from local transcripts, an estimate not the invoice (open-source v1 M2); set with `sakk cap set`
514
+ - [x] Local, read-only console `sakk ui`: spend, decisions, policy, secret blocks (open-source v1 M4)
515
+ - [x] Ships as a Claude Code plugin (`.claude-plugin/`) (open-source v1 M5)
516
+ - [x] `sakk cap set/off`, `sakk spend`, `sakk prices` — set the cap, see the day's spend, show the price table (F-161)
517
+ - [ ] Case packs by regulation (CBUAE, DORA, DIFC — thematic, clause numbers unverified) with per-requirement verdicts — enterprise edition (moved out of the open core)
518
+ - [x] `sakk settle` CLI command (late settlement at the reserved amount without writing code; an amount argument is not there yet)
519
+ - [x] Evidence bundles: one .tgz per run with a manifest and its own zero-dependency verifier — auditors check it without the repository ([docs/AUDITOR_REPLAY.md](docs/AUDITOR_REPLAY.md) §6); built from the repository, not in the npm package
520
+ - [ ] Hash-chained (tamper-evident) ledger mode — enterprise
521
+ - [ ] Named-approver identity, SSO — enterprise
522
+ - [ ] Team console: approvals, roles, audit export, outbound metering — enterprise (the local read-only `sakk ui` above is the open-source edition)
523
+ - [x] README demo: a one-line install at the top and a GIF of a deny
524
+ - [ ] Sourced figures: each accuracy number in this README backed by a linked measurement — not in this release (v2)
525
+ - [ ] Packaging sweep at pack time: a stray-file check plus a secret scan over the packed files — not in this release (v2)
526
+ - [ ] A clear startup refusal on Node older than 20, instead of a raw syntax error — not in this release (v2)
527
+
528
+ Not planned: orchestration frameworks, model-level safety (prompt-injection detection). Sakk is the change-control and evidence layer, not another agent platform.
529
+
530
+ Know these limits of the open core before relying on it:
531
+
532
+ The guard and the cap:
533
+
534
+ - **It reads a command as written.** A command built at run time — `eval`,
535
+ backticks, `bash -c "$X"`, `python -c`, `source` — can only be asked or
536
+ denied, never judged by what it does. A variable or brace expansion anywhere
537
+ in a command counts: a read, write, delete or flag held in one —
538
+ `cat .e{nv,x}`, `f=.env; cat $f`, `f=~/.sakk/config.json; rm $f`,
539
+ `o=--force; git push $o` — is asked, never allowed. A file the shell or a
540
+ program picks at run time from literal text is still judged by the name
541
+ written in the command, so a read of `.env` spelled as a glob (`cat .e*`),
542
+ `grep -r`, `find … -exec cat` or `xargs` is allowed. A script the agent runs
543
+ is one `shell.run` call.
544
+ Only isolation (a container) answers what a command does.
545
+ - **`@` references skip the hook.** Files you @-mention in Claude Code are read
546
+ without a tool call, so the guard never sees them.
547
+ - **The cap stops the next call, not the one that crosses it.** A response
548
+ already in progress completes, so a running agent can go over by one
549
+ response.
550
+ - **The logs are local files the agent can reach.** The cap reads Claude Code's
551
+ transcripts; a direct write to them needs a person's approval and a write to
552
+ `~/.sakk` is denied, but a script the agent runs is not inspected and can
553
+ rewrite them. Spend already counted never goes down; spend not yet counted
554
+ can be erased. A tamper-evident ledger is enterprise. The cap also finds the
555
+ logs through the agent's own environment (`CLAUDE_CONFIG_DIR`, `HOME`), so
556
+ an agent run with either pointed at an empty folder reads $0 spend.
557
+ - **Older Claude Code fails open.** Before 2.1.295 a hook that times out or is
558
+ killed lets the call run. `sakk guard install` says so and `sakk doctor`
559
+ warns.
560
+ - **The confirmation checks for a terminal, not a person.** An agent whose
561
+ shell runs in a pseudo-terminal, or a script it writes that opens one, can
562
+ type the word. The guard asks before `script`, `expect` and `unbuffer`, and
563
+ knows Sakk only by the name `sakk`: `npx sakk cap off` meets the
564
+ confirmation but not the guard's deny. Named approvals are enterprise.
565
+
566
+ The ledger and the library:
567
+
568
+ - **Names are self-declared.** `--by` on halt, resume, reconcile, settle and unlock
569
+ records the name the operator typed; nothing authenticates it.
570
+ - **The ledger is a local file its owner can rewrite.** The guard denies a
571
+ guarded agent `resume`, `reconcile` and `unlock` — each asks a person at a
572
+ terminal — and the default ledger (`~/.sakk/sakk-ledger.json`) sits outside
573
+ the project tree, but a process that is not the guarded agent can still
574
+ edit the file directly. Reserve checks the summary against the newest
575
+ sealed segment, whose hash covers the running total, so lowering the
576
+ settled total alone, or losing or rolling back that segment, refuses
577
+ `LEDGER_ARCHIVE_BROKEN`. The hash is unkeyed: a writer who rewrites that
578
+ segment and the summary together passes reserve, and
579
+ `sakk ledger verify` reports it.
580
+ - **Costs are what the caller reports.** `reserve()` and `settle()` book the
581
+ amounts they are given; nothing reads the provider's usage yet.
582
+ - **Approval-required means refused.** A policy that asks for approval
583
+ answers `POLICY_APPROVAL_REQUIRED`; the open core has no approval service.
584
+ - **Gating is voluntary.** An agent that never calls the MCP tools or the
585
+ library is not gated.
586
+
587
+ Also open in v1, found by the 2026-10-28 acceptance run and deferred to v2:
588
+
589
+ The guard compares names in one spelling. On a case-insensitive disk (the
590
+ macOS default) a command or a protected folder written in another case is
591
+ allowed, and so are the spellings of git's force flag other than its two
592
+ common ones. A symlink is judged by its own name, so a link with an ordinary
593
+ name that points at a secret file can be read. `sakk guard install` does not
594
+ route Claude Code's search tools (Grep, Glob) to the hook and adds no
595
+ deny-read rules, so a search can read a secret file; the plugin's hook sees
596
+ every tool. A Write whose content is not text is not scanned. A write to the
597
+ agent's own settings asks a person instead of being denied. Installed into
598
+ the project's `node_modules`, the hook's code and default policy are project
599
+ files the agent may change, so install globally. `sakk init` prints "hook
600
+ wired" but installs no guard: run `sakk guard install` after it.
601
+
602
+ The console (`sakk ui`) masks a secret only in command text, not in a
603
+ decision's path or scope, on screen or in an export. Its try box refreshes
604
+ the spend cache on disk. Its Stop button does not work in v1, because the
605
+ page cannot read its own session token: use `sakk halt` at the terminal.
606
+ (Since this release every response is sent `Cache-Control: no-store` and a
607
+ cross-site Origin is refused on every route, not only Stop — F-195.)
608
+
609
+ [docs/GUARD.md](docs/GUARD.md), "Known open in v1", has the detail, and
610
+ [docs/V2-BACKLOG.md](docs/V2-BACKLOG.md) lists every known gap with the fix
611
+ planned for it.
612
+
613
+ ## License
614
+
615
+ Apache-2.0 — see [LICENSE](LICENSE). Shipped third-party notices are in [NOTICE](NOTICE). Contributions are taken under the same licence with a DCO sign-off (`git commit -s`), no CLA — see [CONTRIBUTING.md](CONTRIBUTING.md).
616
+
617
+ Enterprise edition — team console, SSO, named approvers, hash-chained ledger: coming soon (the unchecked roadmap items above).
618
+
619
+ ---
620
+
621
+ _Sakk (صك): a written promise that the money is really there. Now your agents make the same promise._