@sakk-dev/sakk 0.0.0 → 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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +27 -0
- package/CHANGELOG.md +1421 -0
- package/LICENSE +202 -0
- package/NOTICE +82 -0
- package/README.md +617 -10
- package/README.zh-CN.md +555 -0
- package/bin/sakk.mjs +1460 -0
- package/integrations/litellm/LICENSE +202 -0
- package/integrations/litellm/NOTICE +82 -0
- package/integrations/litellm/README.md +366 -0
- package/integrations/litellm/config.example.yaml +52 -0
- package/integrations/litellm/sakk_gateway.py +823 -0
- package/package.json +79 -5
- package/policies/guard-default.json +161 -0
- package/policies/permissive-dev.json +22 -0
- package/prices/snapshot.json +1404 -0
- package/src/append.mjs +81 -0
- package/src/archive.mjs +489 -0
- package/src/classify.mjs +914 -0
- package/src/console-login.mjs +374 -0
- package/src/console.mjs +1628 -0
- package/src/gateway-status.mjs +203 -0
- package/src/guard-install.mjs +387 -0
- package/src/hook.mjs +730 -0
- package/src/ledger.mjs +1229 -0
- package/src/match.mjs +52 -0
- package/src/mcp.mjs +299 -0
- package/src/policy.mjs +423 -0
- package/src/prices.mjs +152 -0
- package/src/secret-rules.generated.mjs +225 -0
- package/src/secret-scan.mjs +489 -0
- package/src/spend.mjs +443 -0
package/README.md
CHANGED
|
@@ -1,14 +1,621 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sakk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
workflow (trusted publishing) after the package exists, so this empty
|
|
5
|
-
version was published by hand first.
|
|
3
|
+
**English** | [简体中文](README.zh-CN.md)
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
[sakk-dev/sakk](https://github.com/sakk-dev/sakk) by its `release.yml`
|
|
9
|
-
workflow, with a provenance attestation. Install the latest one (the
|
|
10
|
-
command it adds is `sakk`):
|
|
5
|
+
> **The original check, rebuilt for AI agents.**
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+

|
|
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"
|
|
14
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._
|