@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Changed — the npm package is `@sakk-dev/sakk`
|
|
13
|
+
|
|
14
|
+
- npm refused the unscoped name `sakk` as too close to existing packages, so
|
|
15
|
+
Sakk is published as `@sakk-dev/sakk`: install it with
|
|
16
|
+
`npm install -g @sakk-dev/sakk`. The command it installs is still `sakk`,
|
|
17
|
+
so every `sakk …` command in these notes and the docs is unchanged. A test
|
|
18
|
+
now fails when an install line in the docs or the CLI names any other
|
|
19
|
+
package.
|
|
20
|
+
|
|
21
|
+
### Fixed — the spend cap and the ledger summary can be trusted
|
|
22
|
+
|
|
23
|
+
- One Claude Code log file that cannot be opened (permissions) no longer
|
|
24
|
+
crashes the spend walk. `sakk spend` names the file and its error,
|
|
25
|
+
`SPEND_LOGS_UNLISTABLE`, and exits 1; the other files are still indexed
|
|
26
|
+
and the cap stays closed until a person fixes the file. When any step of
|
|
27
|
+
the guard fails, the deny names that step (spend cap, kill switch, secret
|
|
28
|
+
scan or rules) instead of always blaming the rules (F-192).
|
|
29
|
+
- Reserve no longer trusts the ledger's summary alone. Each sealed segment's
|
|
30
|
+
hash now covers its running total (`total: {settledCount, settledUsd}`,
|
|
31
|
+
a new field in `<ledger>.cold/<seq>.json`), and reserve and the seal check
|
|
32
|
+
the summary against the newest segment. An edited settled total, or a
|
|
33
|
+
lost or rolled-back newest segment, now refuses `LEDGER_ARCHIVE_BROKEN`
|
|
34
|
+
where it used to give cap room back (F-193). A reserve call reads the hot
|
|
35
|
+
file plus that one segment, so its cost still does not grow with history.
|
|
36
|
+
Ledgers sealed by 0.4.0 keep working: their newest segment has no total,
|
|
37
|
+
so reserve walks the chain on each call until the next seal writes one.
|
|
38
|
+
Limit: the hash is unkeyed, so a writer who rewrites the newest segment
|
|
39
|
+
and the summary together passes reserve; `sakk ledger verify` catches it.
|
|
40
|
+
- A seal that crashed before its hot commit no longer locks reserve: the
|
|
41
|
+
segment it left is adopted as written by the next reserve or settle (a
|
|
42
|
+
segment number on disk is never sealed again, and a leftover whose entries
|
|
43
|
+
differ from the hot file is refused); `sakk ledger verify` says so and
|
|
44
|
+
exits 0.
|
|
45
|
+
A summary rolled back over real segments, a stray or out-of-place
|
|
46
|
+
segment file, or a 0.4.0 segment holding an entry with no amount is
|
|
47
|
+
refused (`LEDGER_ARCHIVE_BROKEN`), and verify exits 1 where it used to
|
|
48
|
+
report sound. A seal never rewrites a segment that already exists.
|
|
49
|
+
`sakk init` refuses `LEDGER_ALREADY_EXISTS` beside a leftover
|
|
50
|
+
`<ledger>.cold` or `<ledger>.idx` (F-193).
|
|
51
|
+
- Downgrading is unsupported: segments sealed by this version carry a
|
|
52
|
+
running total that 0.4.0 does not hash, so 0.4.0 cannot verify or
|
|
53
|
+
export them.
|
|
54
|
+
- The test that holds "a call costs the same whatever the history" now sees
|
|
55
|
+
the reads it counts: it patched `fs` without syncing the named imports
|
|
56
|
+
the code uses, so it passed on zero reads whatever the code did. A new
|
|
57
|
+
check refuses that shape in any test or script (F-241).
|
|
58
|
+
|
|
59
|
+
### Fixed — the guard judges an act however it is spelled
|
|
60
|
+
|
|
61
|
+
- A command or protected name in another case (`RM -rf ~`,
|
|
62
|
+
`.CLAUDE/settings.json`) is judged as the lower-case name. The default
|
|
63
|
+
macOS volume opens both spellings as the same file; on a case-sensitive
|
|
64
|
+
volume this only tightens (F-183).
|
|
65
|
+
- Every spelling git accepts for a destructive flag is denied: long-option
|
|
66
|
+
prefixes (`git push --forc`) and short clusters (`git push -fu`).
|
|
67
|
+
`git push --prune` is destructive. A git alias defined for one command
|
|
68
|
+
(`git -c alias.x=…`) is asked (F-184).
|
|
69
|
+
- `sakk` run through any launcher (`npx sakk cap off`, `node …/bin/sakk.mjs`,
|
|
70
|
+
`unbuffer sakk …`) is judged as `sakk` itself (F-185).
|
|
71
|
+
- A read through a symbolic link to a secret file is denied, and a write or
|
|
72
|
+
delete through a dangling link is judged by where it points; a link loop
|
|
73
|
+
is refused (F-186).
|
|
74
|
+
|
|
75
|
+
### Changed — the version is 0.4.0
|
|
76
|
+
|
|
77
|
+
- `package.json`, the plugin manifest and the `sakk-litellm` package say
|
|
78
|
+
0.4.0. Both tutorials were re-run on the packed 0.4.0 tarball in a
|
|
79
|
+
throwaway home; their output, dates and timings come from that run.
|
|
80
|
+
The maintainer pushes the `v0.4.0` tag; `release.yml` then publishes it
|
|
81
|
+
to npm with provenance, and refuses if the tag is not `v` plus this
|
|
82
|
+
version.
|
|
83
|
+
|
|
84
|
+
### Added — the LiteLLM gateway runs as a service, in CI and as a Python package
|
|
85
|
+
|
|
86
|
+
- CI has a `gateway` job: it installs LiteLLM 1.106.2 from the new hash lock
|
|
87
|
+
`integrations/litellm/requirements.lock` (hashes required, no source builds,
|
|
88
|
+
uv pinned by checksum) and runs the plugin's end-to-end tests against the
|
|
89
|
+
mock provider, with no key and no cost. It adds 15 runner-minutes to each
|
|
90
|
+
pull request commit and each merge to main (about $0.09); the ceilings in
|
|
91
|
+
`test/ci-cost.test.mjs` say so. It is not on main's required checks, which
|
|
92
|
+
the owner sets.
|
|
93
|
+
- Service templates in `integrations/litellm/service/`: a launchd agent
|
|
94
|
+
(`KeepAlive`, at most one restart per 10 s) and a systemd user unit
|
|
95
|
+
(`Restart=always`), both on 127.0.0.1 only, with install, check and
|
|
96
|
+
uninstall commands. `test/gateway-service.test.mjs` checks both against the
|
|
97
|
+
variables `sakk_gateway.py` reads.
|
|
98
|
+
- `integrations/litellm/pyproject.toml` packages the plugin as `sakk-litellm`
|
|
99
|
+
(version 0.4.0, the module alone, Apache-2.0 with NOTICE). It requires
|
|
100
|
+
`litellm[proxy]>=1.106.2,!=1.107.0`, which keeps the compromised releases
|
|
101
|
+
out. Not yet published; `docs/launch/pypi-publish.md` has the steps.
|
|
102
|
+
`test/gateway-package.test.mjs` ties the lock, the package and the CI job
|
|
103
|
+
together.
|
|
104
|
+
|
|
105
|
+
### Fixed — the gateway releases stuck reservations and stops queueing on a hung ledger (F-242, F-243)
|
|
106
|
+
|
|
107
|
+
- A reservation the gateway made whose settle never arrived (the proxy
|
|
108
|
+
restarted, the ledger refused the settle, or the settle failed twice)
|
|
109
|
+
stayed outstanding for good: it held one of the ledger's outstanding slots
|
|
110
|
+
(`LEDGER_FULL` at 10,000) and kept the books from sealing (F-242). The
|
|
111
|
+
gateway now settles its own reservations (agent `litellm-gateway`) older
|
|
112
|
+
than `SAKK_RESERVATION_TTL_S` (default 3600 s; zero, negative or
|
|
113
|
+
non-numeric values refuse to start) at the worst case they reserved. A
|
|
114
|
+
background sweep does this, started by the next call at most once a
|
|
115
|
+
minute, 50 settles per run, each logged as a WARNING JSON line
|
|
116
|
+
(`"event": "sakk_reservation_expired"`, with `callId`, `settled_usd`,
|
|
117
|
+
`created_at` and `ttl_s`). The dollars stay counted: a worst-case settle
|
|
118
|
+
over-records, never under. With several gateways on one ledger, one may
|
|
119
|
+
settle another's long call; that call's own settle then gets
|
|
120
|
+
`SETTLE_MISMATCH` and an alert.
|
|
121
|
+
- New command `sakk settle <callId> --by <name> [--ledger <path>]`: a person
|
|
122
|
+
at a terminal closes one outstanding reservation at its reserved amount,
|
|
123
|
+
typing `settle` to confirm, and the alerts file gets a
|
|
124
|
+
`SETTLED_AT_RESERVED` line naming who did it. Without a terminal it exits
|
|
125
|
+
2 with `HUMAN_CONFIRMATION_REQUIRED`; an entry that is already settled is
|
|
126
|
+
a no-op that asks no one; an id the ledger never saw exits 1 with
|
|
127
|
+
`SETTLE_UNKNOWN_CALL`; an entry that awaits reconciliation exits 1 with
|
|
128
|
+
`SETTLE_AWAITS_RECONCILIATION` and points at `sakk reconcile`. When the
|
|
129
|
+
settle lands but the alert cannot be written, it says so and exits 1. The
|
|
130
|
+
guard hook denies `sakk settle` to agents, through every launcher.
|
|
131
|
+
- `SAKK_TIMEOUT_S` defaults to 5 s (it was 2 s, equal to the ledger's own
|
|
132
|
+
2 s wait for its lock, so a busy but healthy ledger could count as an
|
|
133
|
+
outage and the call went through unmetered) (F-243). A reserve that fails
|
|
134
|
+
now opens a breaker for `SAKK_BREAKER_S` (default 10 s; 0 turns it off;
|
|
135
|
+
negative or non-numeric values refuse to start): until it closes, reserves
|
|
136
|
+
take the outage path at once without asking the ledger, including calls
|
|
137
|
+
already queued behind the failing one, which used to wait out their own
|
|
138
|
+
timeout each in turn. Each one is still counted in the status file and
|
|
139
|
+
logged as `sakk_unavailable_allowed` (or refused with 503 in deny mode).
|
|
140
|
+
Settles are never held by the breaker. The first answer after it closes
|
|
141
|
+
ends the outage as before.
|
|
142
|
+
- Not done yet: a side file of the calls let through during an outage, to
|
|
143
|
+
record them in the ledger afterwards; the status file's outage counter
|
|
144
|
+
(`allowed_calls`, `est_max_unrecorded_usd`) stays the record of them.
|
|
145
|
+
|
|
146
|
+
### Changed — the LiteLLM gateway lets calls through when Sakk cannot answer
|
|
147
|
+
|
|
148
|
+
- When Sakk's process will not start, crashes, hangs past the timeout or
|
|
149
|
+
replies with garbage, the gateway now sends the call to the provider
|
|
150
|
+
instead of answering 503. `SAKK_ON_UNAVAILABLE=deny` keeps the old 503
|
|
151
|
+
`SAKK_UNAVAILABLE`; any value other than `allow` or `deny` refuses to
|
|
152
|
+
start. **Calls let through this way are not recorded in the ledger and
|
|
153
|
+
not capped.** A refusal from Sakk that arrives within `SAKK_TIMEOUT_S`
|
|
154
|
+
(402 over the cap, halted, reconciliation, any other Sakk code) and the
|
|
155
|
+
gateway's own 403 and 400 refusals stay refusals in both modes. A ledger
|
|
156
|
+
file lock another process holds longer than the timeout is no answer and
|
|
157
|
+
counts as an outage; Sakk waits about 2 s for the lock itself, inside the
|
|
158
|
+
5 s default timeout (F-243).
|
|
159
|
+
- Each such call logs one JSON WARNING line (`"event":
|
|
160
|
+
"sakk_unavailable_allowed"`, with the model, `est_max_usd`, the reason,
|
|
161
|
+
`outage_since` and `allowed_calls`) and its response, streamed or not,
|
|
162
|
+
carries the header `x-sakk-status: unmetered`. The first answer from Sakk
|
|
163
|
+
again, a reservation or a refusal, ends the outage and logs
|
|
164
|
+
`sakk_outage_ended`.
|
|
165
|
+
- A Sakk process that died is started again within the next call, and that
|
|
166
|
+
call is metered if the new process answers. The request is sent again
|
|
167
|
+
only if writing it to the old process failed; a request that was written
|
|
168
|
+
and got no reply is an outage and is not sent again, because the first
|
|
169
|
+
reservation may have landed (it stays reserved until reconciled).
|
|
170
|
+
- `SAKK_TIMEOUT_S` now defaults to 5 seconds (was 10; 2 seconds for a while
|
|
171
|
+
before release, see F-243 above), for reserve and settle alike; zero,
|
|
172
|
+
negative or non-numeric values refuse to start.
|
|
173
|
+
- New status file, `SAKK_GATEWAY_STATUS` or `<SAKK_LEDGER>.gateway.json`
|
|
174
|
+
with the ledger's folder resolved to its real path on both the gateway and
|
|
175
|
+
the `sakk` side, so a relative or linked ledger path names the same file;
|
|
176
|
+
written whole (temporary file, fsync, rename) with mode 0600 on the
|
|
177
|
+
gateway's event loop:
|
|
178
|
+
`{version: 1, updatedAt, mode, outage, last_outage}`, where `outage` is
|
|
179
|
+
null or `{since, allowed_calls, est_max_unrecorded_usd, last_reason,
|
|
180
|
+
last_at}` and `last_outage` adds `ended_at`. The gateway assumes one
|
|
181
|
+
process per ledger.
|
|
182
|
+
- `sakk status` prints a `gateway` line when the status file exists, and
|
|
183
|
+
`sakk status --json` has a `gateway` key (null without the file). The exit
|
|
184
|
+
code does not change.
|
|
185
|
+
- The console shows a red banner on every page during an outage and a
|
|
186
|
+
warning after one, and, when the status file exists, checks
|
|
187
|
+
`SAKK_GATEWAY_URL` (default `http://127.0.0.1:4000`) at
|
|
188
|
+
`/health/liveliness` (500 ms, cached 5 s), with a red banner when the
|
|
189
|
+
gateway does not answer.
|
|
190
|
+
- `integrations/litellm/config.example.yaml` no longer has a Claude route.
|
|
191
|
+
|
|
192
|
+
### Fixed — the console's spend figures read right (F-236, F-237, F-238)
|
|
193
|
+
|
|
194
|
+
- The console's 7-day spend chart cut the `$0` off its axis labels and,
|
|
195
|
+
for days under a cent, printed the same label on several lines (F-236).
|
|
196
|
+
The axis now steps in round amounts, prints as many decimals as the step
|
|
197
|
+
needs and widens its margin to the longest label.
|
|
198
|
+
- The console printed spend under $0.00005 as `$0.0000`, so the gateway
|
|
199
|
+
ledger card read as free next to a chart showing the same calls (F-237).
|
|
200
|
+
Amounts under $0.0001 now print with two significant digits, such as
|
|
201
|
+
`$0.000049`; every other amount prints as before.
|
|
202
|
+
- The Today and Gateway ledger cards printed spend and cap as one large
|
|
203
|
+
string, so `$0.000050 / $1.00` broke across two lines on a desktop and
|
|
204
|
+
`— / no cap` across three on a phone (F-238). The cap now sits after the
|
|
205
|
+
amount in smaller type and never breaks inside, and the figure shrinks
|
|
206
|
+
with its card so an amount stays whole on a phone.
|
|
207
|
+
|
|
208
|
+
### Fixed — `sakk ui --set-password` no longer shows a password (F-240)
|
|
209
|
+
|
|
210
|
+
- The password prompts were shown a moment before echo went off, so a
|
|
211
|
+
password typed the instant a prompt appeared could be echoed. A script
|
|
212
|
+
that answers the prompts (such as `expect`) showed the password in 2 runs
|
|
213
|
+
out of 20. Echo now goes off before each prompt is shown, the way
|
|
214
|
+
Python's `getpass` does it; the same script showed it in none of 60 runs.
|
|
215
|
+
- Keys typed before a prompt appears are still echoed by the terminal
|
|
216
|
+
itself. Wait for the prompt before you type the password.
|
|
217
|
+
|
|
218
|
+
### Added — `sakk gateway init`: connect your own app, in any language
|
|
219
|
+
|
|
220
|
+
- The README, its Chinese version, the FAQ and `llms.txt` now open with two
|
|
221
|
+
ways in: the coding-agent guard, and your own app through the LiteLLM
|
|
222
|
+
proxy, where the app changes one line,
|
|
223
|
+
`OPENAI_BASE_URL=http://127.0.0.1:4000/v1`. They state what the gateway
|
|
224
|
+
does when the spend ledger cannot answer: calls go through unmetered by
|
|
225
|
+
default with a warning, `SAKK_ON_UNAVAILABLE=deny` refuses them, and 402
|
|
226
|
+
and 403 refusals stay refusals. The guard and the library ledger stay
|
|
227
|
+
fail-closed.
|
|
228
|
+
- New command `sakk gateway init`, with `--dir <path>`, `--ledger <path>`,
|
|
229
|
+
`--port N`, `--litellm <path>` and `--force`. It copies the shipped gateway callback,
|
|
230
|
+
writes `config.yaml` from `config.example.yaml` (keys stay `os.environ/`
|
|
231
|
+
references; no key is written) and a `start.sh` that sets `SAKK_BIN`,
|
|
232
|
+
`SAKK_LEDGER` and `SAKK_GATEWAY_CONFIG` to absolute paths and runs
|
|
233
|
+
`litellm --config … --host 127.0.0.1 --port N`. Defaults: the folder
|
|
234
|
+
`gateway` under `SAKK_HOME` (or `~/.sakk`), the ledger the other commands
|
|
235
|
+
use, port 4000. It prints the one line the app changes. It only checks that
|
|
236
|
+
an executable `litellm` exists on `PATH` or at `--litellm`; it never runs
|
|
237
|
+
it and does not check its version.
|
|
238
|
+
- Exit codes: 0 written; 1 refused or failed; 2 usage. Refusal codes, each
|
|
239
|
+
of which writes nothing: `GATEWAY_USAGE` (2), `GATEWAY_PORT_INVALID` (2,
|
|
240
|
+
port outside 1024-65535), `GATEWAY_EPHEMERAL` (2, run from an npx, bunx or
|
|
241
|
+
pnpm dlx cache whose path the start script would pin; install with
|
|
242
|
+
`npm install -g sakk` first), `GATEWAY_SOURCE_MISSING` (1),
|
|
243
|
+
`GATEWAY_LEDGER_UNREADABLE` (1, a missing or unreadable ledger; create it
|
|
244
|
+
with `sakk init --cap <usd>`), `GATEWAY_LITELLM_NOT_FOUND` (1, prints the
|
|
245
|
+
hash-locked install steps for LiteLLM 1.106.2 and warns off the compromised
|
|
246
|
+
1.82.7, 1.82.8 and 1.107.0), `GATEWAY_FILES_EXIST` (1, pass `--force` to
|
|
247
|
+
replace them). `GATEWAY_WRITE_FAILED` (1) removes the files that run
|
|
248
|
+
created. The written `start.sh` refuses to start without
|
|
249
|
+
`LITELLM_MASTER_KEY` and exits 1 with `GATEWAY_STALE` when the node, sakk
|
|
250
|
+
or ledger it names is gone.
|
|
251
|
+
- The guard judges `sakk gateway init` as the write of the `start.sh` it
|
|
252
|
+
makes: inside `SAKK_HOME` that is guard state, denied to the agent.
|
|
253
|
+
- The npm package now ships `integrations/litellm/sakk_gateway.py`,
|
|
254
|
+
`config.example.yaml` and its `README.md`, not the Python tests or the mock
|
|
255
|
+
provider. New keywords: `litellm`, `openai-compatible`, `llm-gateway`.
|
|
256
|
+
|
|
257
|
+
## [0.3.0] - 2026-11-01
|
|
258
|
+
|
|
259
|
+
### Fixed — the tutorial and FAQ match the 0.3.0 build (F-234)
|
|
260
|
+
|
|
261
|
+
- Both tutorials were re-run on the packed 0.3.0 tarball in a throwaway
|
|
262
|
+
home and say so. Step 2 now creates the `--pack-destination` folder first:
|
|
263
|
+
npm 11 does not, and `npm pack` stopped with `ENOENT`. The `sakk prices`
|
|
264
|
+
output shows the provenance line, and step 9 shows the optional sign-in.
|
|
265
|
+
- The FAQ no longer says the console has no sign-in; it describes
|
|
266
|
+
`--set-password`, what a password session may do and how it ends.
|
|
267
|
+
- New tests in `test/release.test.mjs` tie the version each tutorial names
|
|
268
|
+
to `package.json`, require a `mkdir -p` before every `--pack-destination`
|
|
269
|
+
in the docs, and compare the tutorials' prices block and the FAQ's sign-in
|
|
270
|
+
answer with what the CLI prints.
|
|
271
|
+
|
|
272
|
+
### Fixed — the console names the public repository (F-235)
|
|
273
|
+
|
|
274
|
+
- The console's "Enterprise · coming soon" panel named the private repository's
|
|
275
|
+
discussions. It now names `github.com/sakk-dev/sakk/discussions`. The repository-name test
|
|
276
|
+
read four hand-picked files; it now reads every file `npm pack` ships plus
|
|
277
|
+
the README and release docs, and refuses any owner but `sakk-dev`.
|
|
278
|
+
|
|
279
|
+
### Added — optional username and password sign-in for `sakk ui`
|
|
280
|
+
|
|
281
|
+
- `sakk ui --set-password` asks at a terminal, without echo, for a username
|
|
282
|
+
and a password of 15 or more characters, and stores a scrypt hash with a
|
|
283
|
+
random salt in `console-login.json` under `SAKK_HOME` (mode 0600).
|
|
284
|
+
`sakk ui --clear-password` removes it. Neither takes the password on the
|
|
285
|
+
command line or from a pipe (`UI_PASSWORD_NEEDS_TTY`).
|
|
286
|
+
- With a password set, `http://127.0.0.1:<port>/` shows a sign-in page; the
|
|
287
|
+
address `sakk ui` prints still opens the console. A sign-in sets an
|
|
288
|
+
`HttpOnly`, `SameSite=Strict` session cookie that ends after 30 minutes
|
|
289
|
+
idle or 8 hours, or at Sign out. A password session reads; stopping an
|
|
290
|
+
agent still needs `--allow-halt` and the start token.
|
|
291
|
+
- New routes `POST /login` and `POST /logout`, and new codes
|
|
292
|
+
`CONSOLE_LOGIN_FAILED` (wrong username or password, one answer for both),
|
|
293
|
+
`CONSOLE_LOGIN_LOCKED` (five failures in 15 minutes, with `Retry-After`),
|
|
294
|
+
`CONSOLE_LOGIN_BUSY`, `CONSOLE_LOGIN_BAD_BODY`, `CONSOLE_LOGIN_BAD_TYPE`
|
|
295
|
+
and `CONSOLE_LOGIN_TOO_BIG`. A credential file that is a link, readable by
|
|
296
|
+
others or malformed stops `sakk ui` with `CONSOLE_LOGIN_UNSAFE` or
|
|
297
|
+
`CONSOLE_LOGIN_INVALID` and exit 2.
|
|
298
|
+
- With no password set, `sakk ui` is unchanged. SSO, team accounts and named
|
|
299
|
+
approvers stay out of the open-source edition.
|
|
300
|
+
- An unknown argument or flag of `sakk ui` is no longer echoed back in the
|
|
301
|
+
error, so a password typed on the command line does not land on the screen.
|
|
302
|
+
|
|
303
|
+
### Changed — open-source release: Apache-2.0, DCO and fork-safe CI
|
|
304
|
+
|
|
305
|
+
- **Licence: Apache-2.0** (was AGPL-3.0-only). `LICENSE` is the verbatim
|
|
306
|
+
Apache License 2.0; `NOTICE` names the copyright holder, "The Sakk
|
|
307
|
+
authors", and keeps the MIT notices of the derived material; every SPDX
|
|
308
|
+
header, `package.json` and the plugin manifest say `Apache-2.0`.
|
|
309
|
+
- Contributions are taken under the Developer Certificate of Origin 1.1 —
|
|
310
|
+
a `git commit -s` sign-off on every commit — instead of a CLA. CI checks
|
|
311
|
+
each pull request commit with the base branch's copy of
|
|
312
|
+
`scripts/dco-check.mjs` and names any commit without its author's
|
|
313
|
+
sign-off.
|
|
314
|
+
- `npm install` runs no lifecycle script: the `prepare` hook is gone.
|
|
315
|
+
`npm run hooks` opts a clone in to the git hooks and the repository's
|
|
316
|
+
Claude Code hooks (`.claude/settings.json` now runs them only in a clone
|
|
317
|
+
that opted in).
|
|
318
|
+
- A pull request from a fork skips only the self-evolution and
|
|
319
|
+
base-checker jobs; tests, format, the DCO check and the drill still run.
|
|
320
|
+
The adversarial review never runs for a fork and shows as skipped.
|
|
321
|
+
- The repository moves to `github.com/sakk-dev/sakk` (`package.json`,
|
|
322
|
+
plugin manifest, `SECURITY.md`).
|
|
323
|
+
|
|
324
|
+
### Added — release and supply-chain workflows
|
|
325
|
+
|
|
326
|
+
- `release.yml`: a pushed `v*` tag publishes to npm by trusted publishing
|
|
327
|
+
(OIDC, no stored token) with provenance; it refuses on a private
|
|
328
|
+
repository, a tag that is not `v` + the package version, or an npm older
|
|
329
|
+
than 11.5.1.
|
|
330
|
+
- `scorecard.yml`: a weekly OpenSSF Scorecard run, skipped while the
|
|
331
|
+
repository is private.
|
|
332
|
+
- `.claude-plugin/marketplace.json`: the repository is its own Claude Code
|
|
333
|
+
plugin marketplace (`/plugin marketplace add sakk-dev/sakk`).
|
|
334
|
+
- `SECURITY.md` points to GitHub private vulnerability reporting and lists
|
|
335
|
+
the supported versions.
|
|
336
|
+
|
|
337
|
+
### Added — documentation for the open-source release
|
|
338
|
+
|
|
339
|
+
- `README.zh-CN.md`, a Chinese README kept in step with `README.md`.
|
|
340
|
+
- `docs/FAQ.md` and `docs/FAQ.zh-CN.md`; `docs/tutorial/en.md` and
|
|
341
|
+
`docs/tutorial/zh-CN.md`, a first-run tutorial in each language.
|
|
342
|
+
- `docs/ROADMAP.md`, the public form of `docs/V2-BACKLOG.md`, and
|
|
343
|
+
`docs/compare.md`, how Sakk sits beside neighbouring tools.
|
|
344
|
+
- Architecture decision records 0001–0014 under `docs/adr/`, and
|
|
345
|
+
retrospectives under `docs/retros/`.
|
|
346
|
+
- `llms.txt`, a plain index of the docs for AI search tools.
|
|
347
|
+
- `docs/media/sakk-block.gif`: the console's guard page allowing a listing
|
|
348
|
+
and denying a home-folder delete.
|
|
349
|
+
- The README names both plugin routes: the repository's own marketplace,
|
|
350
|
+
and `--plugin-dir` from the npm package.
|
|
351
|
+
- GUARD.md, SPEND.md and the README no longer say a halt or a reached cap
|
|
352
|
+
denies every tool call: the Claude Code install routes shell, file,
|
|
353
|
+
Monitor and MCP tools, not Glob, Grep, WebFetch, WebSearch or Task
|
|
354
|
+
(F-189); loaded as a plugin, the guard sees every tool (F-233).
|
|
355
|
+
|
|
356
|
+
### Changed — regulator-specific packs and policy leave the public repository
|
|
357
|
+
|
|
358
|
+
- Two example case packs written for a fictional authority,
|
|
359
|
+
`example-agent-oversight` and `example-change-control`, replace the
|
|
360
|
+
CBUAE, DORA and DIFC packs. They keep the same format and back the same
|
|
361
|
+
9 of the 12 gold cases; packs for specific regulations are not in this
|
|
362
|
+
repository.
|
|
363
|
+
- The regulated-strict reference policy is removed: `policies/` ships
|
|
364
|
+
`permissive-dev.json` and `guard-default.json` only, and a test pins
|
|
365
|
+
that list because `policies/` is in the npm package.
|
|
366
|
+
- The reviewer label gate is renamed `eval/05_LABEL_REVIEW.md`.
|
|
367
|
+
|
|
368
|
+
### Changed — `sakk ui` follows the frozen v10 open-source design
|
|
369
|
+
|
|
370
|
+
- The console is rebuilt page by page to the frozen v10 open-source
|
|
371
|
+
prototype, on the same `/api` routes (F-223). Long paths and call ids no
|
|
372
|
+
longer widen the page in a narrow window (F-222).
|
|
373
|
+
- A click no longer moves the nav: in a narrow window the nav strip keeps
|
|
374
|
+
its scroll and brings the chosen page into view, and nav items keep their
|
|
375
|
+
width when chosen (F-224).
|
|
376
|
+
|
|
377
|
+
### Fixed — what the console and CLI report after the 2026-11-01 UAT
|
|
378
|
+
|
|
379
|
+
- `sakk doctor`'s self-test is logged as `sakk-doctor`, not as Claude Code,
|
|
380
|
+
when it fires its fixed probe through the installed hook. The console
|
|
381
|
+
leaves it out of blocked counts, agents and the feed, and keeps it in
|
|
382
|
+
records and export as "Doctor self-test". A decisions line that is not a
|
|
383
|
+
record is counted as unreadable (F-229).
|
|
384
|
+
- The Secret guard page lists secret reads the guard blocked, not only
|
|
385
|
+
secrets found in content (F-226).
|
|
386
|
+
- The Agents page says when the Claude Code hook was installed fail-open
|
|
387
|
+
(F-227).
|
|
388
|
+
- Open items counts the rows it links to; a halt no longer adds one (F-228).
|
|
389
|
+
- `sakk prices` prints the provenance its price table records (source URL,
|
|
390
|
+
commit, SHA-256, fetch date, licence), or says none is recorded, as the
|
|
391
|
+
README already promised; `--json` adds a `provenance` object (or `null`)
|
|
392
|
+
and keeps `source` as the table's path (F-232).
|
|
393
|
+
- A deny by a policy rule reads "A person changes the policy." as its own
|
|
394
|
+
sentence before the dispute line.
|
|
395
|
+
- `sakk ledger export --out` says "entry" or "entries" instead of printing
|
|
396
|
+
`entr(y|ies)` (F-225).
|
|
397
|
+
- The overview's Today card shows "—" and "run sakk spend to count it" for
|
|
398
|
+
a day the spend index has not counted, instead of $0.00. `/api/overview`
|
|
399
|
+
gains `indexed_at` (when the index was last counted, or null) and
|
|
400
|
+
`counted_today`; the console still writes nothing on a GET (F-231).
|
|
401
|
+
|
|
402
|
+
### Added — the console shows the spend ledger
|
|
403
|
+
|
|
404
|
+
- `sakk ui` has a new **Spend ledger** page, and the overview a ledger card:
|
|
405
|
+
each metered call (agent, status, reserved maximum, actual), the cap,
|
|
406
|
+
settled, reserved now, what is left and the most the next call may
|
|
407
|
+
reserve, plus the refusals and alerts from `sakk-alerts.jsonl` next to the
|
|
408
|
+
ledger, newest first with the cap each was judged against.
|
|
409
|
+
- New read-only route `GET /api/ledger`. It shows the ledger the console was
|
|
410
|
+
started with (`--ledger`, `config.json` under `SAKK_HOME`, or
|
|
411
|
+
`SAKK_LEDGER_PATH`) and takes no query. With no ledger set it answers
|
|
412
|
+
`configured: false`; ledger refusals pass through by their own code; a
|
|
413
|
+
ledger over 32 MiB answers the new code `CONSOLE_LEDGER_TOO_BIG`.
|
|
414
|
+
- `ledgerStatus` and `alertsPathFor` are now exported from `src/ledger.mjs`.
|
|
415
|
+
`sakk status --json` is built by `ledgerStatus`, so the console and the
|
|
416
|
+
CLI print the same object; its shape is unchanged.
|
|
417
|
+
|
|
418
|
+
### Changed — the decisions tail skips files that are not regular files
|
|
419
|
+
|
|
420
|
+
- The console's log reader opens `decisions.jsonl` without blocking and
|
|
421
|
+
skips a fifo or directory in its place instead of hanging the request (the
|
|
422
|
+
alerts log is read the same way and also refuses a symlink). `truncated` is now also true when the
|
|
423
|
+
500-line page limit cuts the list, not only when the 1 MiB tail does.
|
|
424
|
+
|
|
425
|
+
## [0.1.0] - 2026-10-29
|
|
426
|
+
|
|
427
|
+
### Fixed — a backdated log line cannot escape the daily cap (F-210, F-211)
|
|
428
|
+
|
|
429
|
+
- The spend index booked every log line to the day its own timestamp
|
|
430
|
+
claimed, so a line first seen today claiming an old day landed outside
|
|
431
|
+
every cap window (the F-205 fix covered the future direction only). The
|
|
432
|
+
first whole scan now seals a baseline (`$SAKK_HOME/spend/baseline.json`):
|
|
433
|
+
history already on disk keeps its claimed day, and after sealing a line
|
|
434
|
+
first seen today claiming an earlier day is counted today and flagged
|
|
435
|
+
`backdated` — unless the claimed day's bucket already covers that call's
|
|
436
|
+
usage (a legitimately rewritten log). `sakk spend` reports the count (a
|
|
437
|
+
new `backdated` field in `--json`), and a `SPEND_CAP_REACHED` deny says
|
|
438
|
+
when backdated lines are included (both shapes are additive).
|
|
439
|
+
- The index also trusted file mtime as its scan boundary, so a planted
|
|
440
|
+
file faking an old mtime was never read. The incremental skip now
|
|
441
|
+
requires the index to already know the file at an unchanged size; mtime
|
|
442
|
+
is an optimization input, never a trust boundary (F-211).
|
|
443
|
+
|
|
444
|
+
### Changed — the release describes one artifact (F-111)
|
|
445
|
+
|
|
446
|
+
- Version 0.1.0 is cut: package.json, the plugin manifest and this
|
|
447
|
+
changelog's newest released section now name the same version, and a
|
|
448
|
+
release test pins them together with the `v0.1.0` tag-name format the
|
|
449
|
+
links below use. Publishing (the tag and npm) remains the owner's step.
|
|
450
|
+
|
|
451
|
+
### Fixed — a spend check that cannot see the log root fails closed (F-208)
|
|
452
|
+
|
|
453
|
+
- The daily-cap reader treated a missing log ROOT as "no spend yet", so an
|
|
454
|
+
unreadable parent directory, a dangling symlink or a symlink loop at the
|
|
455
|
+
root reported an honest-looking $0 and the cap allowed. The root is now
|
|
456
|
+
read like any subdirectory: only true absence is $0; every other failure
|
|
457
|
+
denies with `SPEND_LOGS_UNLISTABLE`.
|
|
458
|
+
|
|
459
|
+
### Fixed — a resume no-op never reaches the write path (F-209)
|
|
460
|
+
|
|
461
|
+
- `sakk resume` with nothing halted now answers and exits before the
|
|
462
|
+
in-lock write path is reachable (same message, same exit code), so a halt
|
|
463
|
+
landing between the check and the lock can no longer be lifted without a
|
|
464
|
+
person's confirmation — matching `sakk reconcile` and `sakk unlock`.
|
|
465
|
+
|
|
466
|
+
### Fixed — the console refuses a cross-site Origin on every route (F-195)
|
|
467
|
+
|
|
468
|
+
- `sakk ui` now refuses a request whose Origin header is not the console's
|
|
469
|
+
own loopback origin on every route — the page, every `/api/*` read and
|
|
470
|
+
the halt write — in one gate ahead of dispatch, so a route added later is
|
|
471
|
+
covered too. A request without an Origin header (a typed URL, curl) still
|
|
472
|
+
works, and the halt write still requires the Origin to be present.
|
|
473
|
+
- Every console response now carries `Cache-Control: no-store`, and a
|
|
474
|
+
non-HTML response's CSP no longer carries an empty-nonce script or style
|
|
475
|
+
allowance.
|
|
476
|
+
|
|
477
|
+
### Removed — the advertised prices updater (F-175)
|
|
478
|
+
|
|
479
|
+
- The update subcommand of `sakk prices` named in earlier notes was never
|
|
480
|
+
built and is retracted (owner decision D11, 2026-10-29): the hints in
|
|
481
|
+
`sakk ui` and in the PRICE_UNKNOWN refusal no longer point to it, and the
|
|
482
|
+
spend spec and docs name the one real route — copy `prices/snapshot.json`
|
|
483
|
+
to `~/.sakk/prices.json` and edit it by hand.
|
|
484
|
+
- The release check now validates every multi-word `sakk` phrase in the
|
|
485
|
+
README, this changelog, docs, specs and the runtime's user-facing strings
|
|
486
|
+
against the CLI's real subcommands, so an advertised-but-unbuilt command
|
|
487
|
+
cannot recur.
|
|
488
|
+
|
|
489
|
+
### Changed — agent edits of guard config are denied, not asked (F-188)
|
|
490
|
+
|
|
491
|
+
- The `ask-agent-config` policy is renamed and tightened to
|
|
492
|
+
`deny-agent-config`: from the agent, `sakk guard install|uninstall` is now
|
|
493
|
+
denied instead of asked. Mentions of the old name further down this file
|
|
494
|
+
describe the behavior as it was when those notes were written.
|
|
495
|
+
|
|
496
|
+
### Added — a NOTICE file for the MIT-derived material (F-199)
|
|
497
|
+
|
|
498
|
+
- A NOTICE file now states the MIT-licensed material this AGPL-3.0-only
|
|
499
|
+
package derives from — the secret-scanning rules ported from gitleaks and
|
|
500
|
+
the price snapshot derived from LiteLLM's model list — with their
|
|
501
|
+
attributions, and ships in the npm package.
|
|
502
|
+
|
|
503
|
+
### Added — spend failures and oddities get public names
|
|
504
|
+
|
|
505
|
+
- New public error code `SPEND_LOGS_UNLISTABLE`: a spend-log directory that
|
|
506
|
+
cannot be listed now fails closed under its own name.
|
|
507
|
+
- New public error code `LEDGER_PATH_LEGACY`: the legacy `./sakk-ledger.json`
|
|
508
|
+
default path is refused by name instead of being silently read.
|
|
509
|
+
- `sakk spend --json` adds a `future` field: a log line stamped in the
|
|
510
|
+
future counts toward the day it is observed, and is flagged there.
|
|
511
|
+
|
|
512
|
+
### Changed — unattended runs stop at the human gate
|
|
513
|
+
|
|
514
|
+
- `sakk resume`, `sakk reconcile` and `sakk unlock` without a real terminal
|
|
515
|
+
now exit 2 with `HUMAN_CONFIRMATION_REQUIRED` instead of succeeding, since
|
|
516
|
+
each needs a person's confirmation.
|
|
517
|
+
- The default ledger path moves to `~/.sakk/sakk-ledger.json`.
|
|
518
|
+
|
|
519
|
+
### Changed — v1 close-out: known limits stated, the rest moved to v2
|
|
520
|
+
|
|
521
|
+
- The 2026-10-28 acceptance run checked all 151 P0 cases by their expected
|
|
522
|
+
result. The owner closed v1 as it stands (docs/DECISIONS.md D10): no code
|
|
523
|
+
changes, and every case that did not pass is a finding (F-182…F-201) with
|
|
524
|
+
its fix in [docs/V2-BACKLOG.md](docs/V2-BACKLOG.md).
|
|
525
|
+
- The gaps an operator should know before relying on v1 are now stated in
|
|
526
|
+
the README "Limits", docs/GUARD.md "Known open in v1" and docs/SPEND.md:
|
|
527
|
+
names compared in one spelling, symlinks judged by their own name, search
|
|
528
|
+
tools outside the hook's matcher, a project-local install in the agent's
|
|
529
|
+
reach, the console's Stop button, and the spend figure's reliance on the
|
|
530
|
+
agent's environment.
|
|
531
|
+
- Corrected in the docs: the README plugin line (`claude --plugin-dir`, one
|
|
532
|
+
session; a persistent install needs a marketplace file, planned for v2),
|
|
533
|
+
the secret-rule counts (195 upstream, 192 enabled), which roadmap items
|
|
534
|
+
ship in the npm package, and the fetching prices updater, which was
|
|
535
|
+
advertised but never built (retracted in this release — F-175).
|
|
536
|
+
|
|
537
|
+
### Changed — the agent cannot re-point the ledger a halt is read from (F-181)
|
|
538
|
+
|
|
539
|
+
- From the agent, `sakk init` is now denied (`protect-guard-state`), as
|
|
540
|
+
`sakk cap` is. Run by an agent, it could record a ledger the agent writes in
|
|
541
|
+
`~/.sakk/config.json`; the hook reads that ledger first, so a halt held on
|
|
542
|
+
the ledger `SAKK_LEDGER_PATH` names stopped applying. A person runs `init`
|
|
543
|
+
at the terminal as before. Agents or scripts that ran `sakk init` through a
|
|
544
|
+
guarded agent must now run it themselves.
|
|
545
|
+
|
|
546
|
+
### Fixed — a cache-write breakdown can no longer lower the bill (F-179)
|
|
547
|
+
|
|
548
|
+
- A `cache_creation` breakdown that names only part of the cache writes now
|
|
549
|
+
bills the rest at the 5-minute price, as a log line with no breakdown does;
|
|
550
|
+
before, the breakdown replaced the total and the daily cap counted less than
|
|
551
|
+
was spent. Breakdown tokens beyond the total are billed and count toward
|
|
552
|
+
long context.
|
|
553
|
+
- A breakdown field that is not a whole-token count (a string, a fraction, a
|
|
554
|
+
boolean, a negative number), or a `cache_creation` that is not an object,
|
|
555
|
+
now makes the line unreadable — the cap denies with `SPEND_UNREADABLE` —
|
|
556
|
+
instead of being read as 0.
|
|
557
|
+
- docs/SPEND.md now states that the cap counts tokens only: server-side tools
|
|
558
|
+
billed per request, such as web search, are not included (F-180).
|
|
559
|
+
|
|
560
|
+
### Changed — the agent cannot lift its own cap or remove its own guard (F-171)
|
|
561
|
+
|
|
562
|
+
- `sakk cap off`, raising the cap, moving its day with `--zone`, and
|
|
563
|
+
`sakk guard uninstall` (when it would remove something) now ask a person at
|
|
564
|
+
the terminal to retype a word. With no terminal on stdin and stderr, or a
|
|
565
|
+
wrong answer, they exit 2 with the new code `HUMAN_CONFIRMATION_REQUIRED`
|
|
566
|
+
and change nothing. Setting a first cap, lowering it, `guard install` and
|
|
567
|
+
`init` never ask. Scripts that ran `cap off` or `guard uninstall` unattended
|
|
568
|
+
must now run them at a terminal.
|
|
569
|
+
- The guard judges Sakk's own commands by the write they make: from the agent,
|
|
570
|
+
`sakk cap …` is denied (`protect-guard-state`), `sakk guard install|uninstall`
|
|
571
|
+
asks (`ask-agent-config`; renamed and tightened to `deny-agent-config` in
|
|
572
|
+
this release — F-188), and `script`, `expect` and `unbuffer` ask
|
|
573
|
+
(`ask-constructed-commands`), because they can type into a prompt.
|
|
574
|
+
- Limits, stated in the README and docs/GUARD.md: the confirmation checks for a
|
|
575
|
+
terminal, not a person; Sakk is known by the name `sakk` only.
|
|
576
|
+
- Known gap, not yet fixed (F-177): `resume`, `reconcile` and `unlock` from a
|
|
577
|
+
guarded agent are allowed, and the default ledger is a project file the
|
|
578
|
+
agent may write, so one agent can lift a halt on another.
|
|
579
|
+
|
|
580
|
+
### Changed — the README states the guard's limits (REQ-P04, F-173)
|
|
581
|
+
|
|
582
|
+
- The README's limits section now leads with the five the requirements name: a
|
|
583
|
+
command built at run time is only asked or denied; `@` references skip the
|
|
584
|
+
hook; the cap stops the next call; the agent can reach the local logs; older
|
|
585
|
+
Claude Code fails open. The first states where the secret guard stops: a
|
|
586
|
+
name the shell or a program picks at run time (glob, brace, variable,
|
|
587
|
+
`grep -r`, `find -exec`, `xargs`) is judged as written, so such a read of
|
|
588
|
+
`.env` is allowed. The behaviour is unchanged; a test pins both sides.
|
|
589
|
+
- Known gap, not yet fixed (F-176): a delete, write or flag held in a shell
|
|
590
|
+
variable is allowed wherever it points, including `~/.sakk` and
|
|
591
|
+
`git push --force`. The README, docs/GUARD.md and docs/SPEND.md say so.
|
|
592
|
+
|
|
593
|
+
### Fixed — `sakk halt` now stops the guarded agent (F-172)
|
|
594
|
+
|
|
595
|
+
- No command wrote `ledgerPath` into `~/.sakk/config.json`, so the hook never
|
|
596
|
+
saw a ledger: `sakk halt` recorded a halt the installed guard could not read
|
|
597
|
+
and the agent kept running. `sakk init` now records the ledger's absolute path
|
|
598
|
+
there, and the CLI, the hook and the console's stop button resolve the ledger
|
|
599
|
+
by one shared rule (`--ledger` > the recorded path > `SAKK_LEDGER_PATH` > the
|
|
600
|
+
CLI's `./sakk-ledger.json`), so a halt lands where the hook reads. Re-running
|
|
601
|
+
`init` wires an existing ledger without touching it and never replaces a
|
|
602
|
+
different recorded path.
|
|
603
|
+
|
|
604
|
+
### Fixed — the shipped price snapshot (F-169)
|
|
605
|
+
|
|
606
|
+
- The shipped `prices/snapshot.json` priced none of the current Claude models,
|
|
607
|
+
so out of the box every metered run was refused as `PRICE_UNKNOWN`. It is now
|
|
608
|
+
regenerated from the LiteLLM table (170 models) by `scripts/prices-trim.mjs`,
|
|
609
|
+
which also writes the third-party credit (`LiteLLM (MIT, (c) Berri AI)`) into
|
|
610
|
+
the snapshot's provenance block. An unknown model still fails closed.
|
|
611
|
+
|
|
612
|
+
### Fixed — the guard says when it would fail open (F-174)
|
|
613
|
+
|
|
614
|
+
- On Claude Code older than 2.1.295 the hook cannot be installed with
|
|
615
|
+
`onFailure: "block"`, so a hook that times out lets the call run. `sakk guard
|
|
616
|
+
install` now says so, and `sakk doctor` warns on any installed Claude Code
|
|
617
|
+
hook that lacks the field.
|
|
618
|
+
|
|
619
|
+
### Fixed — spend accounting correctness (F-168, F-170)
|
|
620
|
+
|
|
621
|
+
- An empty `cache_creation:{}` object (which a trimmed or replayed log can emit)
|
|
622
|
+
was read as an authoritative all-zero breakdown, so cache-write tokens were
|
|
623
|
+
priced at `$0`. `costOf` now honours the split only when it enumerates tokens
|
|
624
|
+
and otherwise falls back to the `cache_creation_input_tokens` aggregate, so
|
|
625
|
+
those tokens are billed the same as with no split key (F-168).
|
|
626
|
+
- A fractional token count (e.g. `input_tokens: 1.5`) passed the spend parser's
|
|
627
|
+
validation and then threw deep in pricing, crashing `sakk spend`. Token counts
|
|
628
|
+
are now screened as whole numbers at the parse boundary — a non-integer line is
|
|
629
|
+
marked unreadable — and `daySpend` isolates any pricing throw as one unreadable
|
|
630
|
+
line, so the cap fails closed via `SPEND_UNREADABLE` instead of aborting
|
|
631
|
+
(F-170).
|
|
632
|
+
|
|
633
|
+
### Fixed — the release certificate's mutant-freshness parser (F-167)
|
|
634
|
+
|
|
635
|
+
- The certificate's drill-freshness parser read mutant ids with a `MUT-[A-Z]`
|
|
636
|
+
regex that was blind to the twelve two-letter mutants (`MUT-AA`…`MUT-AL`):
|
|
637
|
+
their tampered-file hash blocks were never in the verified set, so the
|
|
638
|
+
`freshness: N hashes verified` line a reader relies on could never be issued.
|
|
639
|
+
The parser now reads `MUT-[A-Z]+`, so every mutant the drill emits is in
|
|
640
|
+
reach, and `EXPECTED_MUTANTS` is pinned to the count the drill defines; a test
|
|
641
|
+
fails `npm test` if either drifts from `scripts/mutation-drill.sh`.
|
|
642
|
+
|
|
643
|
+
### Added — the gitleaks rule port in the secret guard (open-source v1, M3)
|
|
644
|
+
|
|
645
|
+
- The secret guard now runs **192 of gitleaks' 195 rules** (v8.21.2, MIT)
|
|
646
|
+
alongside the eight hand-written ones, in the hook and the commit scan.
|
|
647
|
+
`scripts/gitleaks-port.mjs` reads the pinned `config/gitleaks.toml` and
|
|
648
|
+
generates `src/secret-rules.generated.mjs`, translating RE2 to JS (a leading
|
|
649
|
+
`(?i)` to the `i` flag, a mid `(?i)`, `(?i:…)` and `(?-i:…)` by case-folding,
|
|
650
|
+
`(?P<>)` to `(?<>)`, `\z` to `$`) into syntax Node 20 runs; it refuses any
|
|
651
|
+
construct it does not know (1 rule, in `REFUSED`), never guessing.
|
|
652
|
+
- Rules are keyword-gated and filtered by gitleaks' global allowlist and
|
|
653
|
+
per-rule entropy; one secret matched by two rules is reported once. The port
|
|
654
|
+
is deterministic (a re-run reproduces the file byte-for-byte) and every
|
|
655
|
+
enabled rule is **linear on hostile input** — the `(?:.|\s){0,N}` span idiom,
|
|
656
|
+
which backtracks catastrophically in JS, is rewritten to `[\s\S]`.
|
|
657
|
+
- Two rules ship disabled as reviewed exclusions: `generic-api-key` (too broad
|
|
658
|
+
— it flags ordinary code, and in a fail-closed guard a false positive denies
|
|
659
|
+
a legitimate write) and `curl-auth-user` (a RE2 pattern that is not linear in
|
|
660
|
+
JS). The repository scans clean with the rest enabled.
|
|
661
|
+
- The full per-rule recall corpus is the remaining C-13 stopline
|
|
662
|
+
(`specs/secret-guard`, due 2026-10-28): gitleaks' own samples are generated
|
|
663
|
+
by Go helpers, not literal strings.
|
|
664
|
+
|
|
665
|
+
### Added — the spend and cap CLI (F-161 follow-up)
|
|
666
|
+
|
|
667
|
+
- `sakk cap set <usd> [--zone <IANA>]` / `sakk cap off`: set or lift the daily
|
|
668
|
+
cap the hook enforces at the next call. A person runs it; the agent is denied
|
|
669
|
+
writing `~/.sakk` directly by the guard (rule `protect-guard-state`), and a
|
|
670
|
+
one-command symlink/hard-link chain that would reach it is not silently
|
|
671
|
+
allowed (it is treated as constructed and needs a person's approval, F-163),
|
|
672
|
+
so the agent cannot raise its own cap by a command that names the file (a
|
|
673
|
+
path held in a shell variable is not caught yet, F-176). The amount must be a positive
|
|
674
|
+
plain decimal (`CAP_USAGE`) and the zone a real IANA name (`CAP_ZONE_INVALID`).
|
|
675
|
+
- `sakk spend [--day <YYYY-MM-DD>] [--json]`: a day's spend by model and what
|
|
676
|
+
could not be priced, read from Claude Code's local logs and labelled an
|
|
677
|
+
estimate, not the invoice; the day boundary follows the configured zone. A
|
|
678
|
+
malformed `--day` is `SPEND_USAGE`.
|
|
679
|
+
- `sakk prices [--json]`: the price table in use and its source file.
|
|
680
|
+
- These complete M2's documented surface (the library and the hook's
|
|
681
|
+
enforcement already shipped; see F-161). A fetching prices updater — the
|
|
682
|
+
one network call — was then still planned; it was never built and is
|
|
683
|
+
retracted (F-175).
|
|
684
|
+
|
|
685
|
+
### Added — release packaging (open-source v1, M5)
|
|
686
|
+
|
|
687
|
+
- Ships as a **Claude Code plugin**: `.claude-plugin/plugin.json` wires a
|
|
688
|
+
`PreToolUse` hook that runs `node ${CLAUDE_PLUGIN_ROOT}/bin/sakk.mjs hook
|
|
689
|
+
--agent claude-code` on every tool call, `onFailure: block` (fail-closed).
|
|
690
|
+
Installable with `claude plugin install --plugin-dir <path>`; the manifest is
|
|
691
|
+
the only file a local/npm install needs (no marketplace file).
|
|
692
|
+
- The npm package `files` list now ships `.claude-plugin` alongside `src`,
|
|
693
|
+
`bin`, `policies` and `prices`; `test/release.test.mjs` runs the real
|
|
694
|
+
`npm pack` and asserts the tarball excludes `test`, `specs`, `findings`,
|
|
695
|
+
`eval` and `scripts`, so dev-only files never leak into the package.
|
|
696
|
+
- A detector closes a gate blind spot (F-161): every `sakk <command>` named in
|
|
697
|
+
the README or the CHANGELOG must be a real CLI subcommand, so the docs can no
|
|
698
|
+
longer promise a command the binary does not implement. The package version
|
|
699
|
+
is checked to be one source of truth across the binary and the plugin.
|
|
700
|
+
- Publishing itself (npm publish, making the repository public, a marketplace
|
|
701
|
+
listing) stays a person's action.
|
|
702
|
+
|
|
703
|
+
### Added — the local console `sakk ui` (open-source v1, M4)
|
|
704
|
+
|
|
705
|
+
- `sakk ui [--port N] [--allow-halt] [--ledger <path>]` starts a read-only web
|
|
706
|
+
console on `127.0.0.1` (never `0.0.0.0`) and prints one address carrying a
|
|
707
|
+
fresh 128-bit token. It shows only what M1–M3 recorded, through the same
|
|
708
|
+
functions the CLI uses: today's spend and cap, recent guard decisions, the
|
|
709
|
+
policy in use with a try box, secret blocks, `sakk doctor`'s checks, and the
|
|
710
|
+
model prices — plus an "Enterprise · coming soon" panel.
|
|
711
|
+
- Security: the server binds loopback only; every request carries the token
|
|
712
|
+
(first in the query, then a `SameSite=Strict; HttpOnly` cookie) and is
|
|
713
|
+
checked against the `Host`; a write (stopping an agent, behind
|
|
714
|
+
`--allow-halt`) also needs the `Origin` and a token header; the token
|
|
715
|
+
compare is constant-time and refusals are counted and shown. Responses carry
|
|
716
|
+
a strict CSP (`default-src 'none'`), `nosniff` and `no-referrer`, and never a
|
|
717
|
+
CORS header. The page is one HTML file with inline, nonce'd script and style,
|
|
718
|
+
no external origin, and renders every value with `textContent` so a log
|
|
719
|
+
string is never HTML. The decision log is read in a bounded tail, so a 5 GB
|
|
720
|
+
log is never loaded whole.
|
|
721
|
+
- Read-only by default. The one write stops an agent via the ledger's `halt()`,
|
|
722
|
+
recorded with the OS user name and labelled self-declared; resume stays on
|
|
723
|
+
the command line. New refusal codes: `UI_PORT_INVALID`, `UI_PORT_PRIVILEGED`,
|
|
724
|
+
`UI_PORT_IN_USE`, `CONSOLE_NO_LEDGER`, `CONSOLE_BAD_BODY`, `CONSOLE_BAD_AGENT`,
|
|
725
|
+
`CONSOLE_TRY_EMPTY`. Export is CSV and JSONL; FOCUS and evidence bundles are
|
|
726
|
+
the enterprise edition.
|
|
727
|
+
|
|
728
|
+
### Fixed — M4 console, from the independent review (F-162)
|
|
729
|
+
|
|
730
|
+
- `GET /api/agents` is now read-only and subprocess-free: it reports each hook's
|
|
731
|
+
installed state via `doctor({probe:false})` instead of firing the hook
|
|
732
|
+
(`spawnSync`) and creating Sakk's home, so a read-only request can no longer
|
|
733
|
+
run subprocesses or block the event loop. It also passes the OS home to
|
|
734
|
+
`doctor`, fixing a path bug. `GET /api/overview` no longer updates the spend
|
|
735
|
+
index — a GET writes nothing. The halt record is labelled "self-declared (the
|
|
736
|
+
OS account name)", the CSV export neutralises spreadsheet formula injection
|
|
737
|
+
(leading `= + - @`), and the spec no longer claims a port-scoped cookie.
|
|
738
|
+
|
|
739
|
+
### Added — the secret guard in the hook (open-source v1, M3)
|
|
740
|
+
|
|
741
|
+
- A write whose content is a secret, and a shell command (or `apply_patch`
|
|
742
|
+
body) whose text contains a literal secret, are now denied inside the hook
|
|
743
|
+
(`evaluate` step 3), scanned once per call through the existing eight-rule
|
|
744
|
+
`scanText`. This runs for every agent (claude-code, cursor, codex) and over
|
|
745
|
+
every write surface (Write `content`, Edit/MultiEdit new text, NotebookEdit,
|
|
746
|
+
`apply_patch`). Reading a secret file was already denied by the `secret.read`
|
|
747
|
+
rule and is unchanged.
|
|
748
|
+
- The deny names the matched rule and a keyed fingerprint, never the secret
|
|
749
|
+
value; the decision-log line drops the reason text and the content, so the
|
|
750
|
+
value reaches neither the agent nor `~/.sakk/decisions.jsonl`. A scan that
|
|
751
|
+
cannot run fails closed as `HOOK_SECRET_SCAN_FAILED` (a deny, via the
|
|
752
|
+
guard's rules-unknown path). New refusal code: `HOOK_SECRET_SCAN_FAILED`.
|
|
753
|
+
- Deferred: the gitleaks 222-rule port (`specs/secret-guard/`, "Status") needs
|
|
754
|
+
the upstream rule file fetched at a pinned commit (a network download) and
|
|
755
|
+
waits on approval; the hook ships the eight rules until then.
|
|
756
|
+
|
|
757
|
+
### Added — spend and the daily cap (open-source v1, M2)
|
|
758
|
+
|
|
759
|
+
- A daily spend cap the guard enforces at the next tool call. Spend is read
|
|
760
|
+
from Claude Code's local transcripts (`~/.claude/projects/**/*.jsonl`) and
|
|
761
|
+
priced: each call counted once at its largest recorded usage (dedup by
|
|
762
|
+
`message.id`+`requestId`, max-merge); cache writes priced per duration
|
|
763
|
+
(5m/1h); the whole request billed at the long-context rate past a model's
|
|
764
|
+
threshold. The cap, zone and log folder live in `~/.sakk/config.json`, read
|
|
765
|
+
by a person's configuration — not from the agent's environment — and writing
|
|
766
|
+
`~/.sakk/` is denied by the guard (directly, or via a chained link that needs
|
|
767
|
+
approval, F-163), so an agent cannot silently raise its own cap.
|
|
768
|
+
Fail-closed: a model with no price (`PRICE_UNKNOWN`) or an unreadable log
|
|
769
|
+
line (`SPEND_UNREADABLE`) is a deny while a cap is set.
|
|
770
|
+
- A small, approximate starter price snapshot ships in `prices/` (schema
|
|
771
|
+
`sakk-prices.v1`); unlisted models fail closed until a newer table is placed
|
|
772
|
+
at `~/.sakk/prices.json`. Today's spend and the cap are viewable in the
|
|
773
|
+
console (`sakk ui`, M4). The spend index (`~/.sakk/spend/`) is incremental
|
|
774
|
+
and only grows — a deleted or rewritten log keeps what was counted. The
|
|
775
|
+
figures are an estimate from local logs, not the provider's invoice.
|
|
776
|
+
- New refusal codes: `PRICES_INVALID`, `PRICES_UNREADABLE`, `PRICE_UNKNOWN`,
|
|
777
|
+
`USAGE_INVALID`, `SPEND_UNREADABLE`, `SPEND_CAP_REACHED`,
|
|
778
|
+
`SPEND_TOO_MANY_LOGS`.
|
|
779
|
+
- M2 itself shipped the library and the hook's enforcement, not the CLI; the
|
|
780
|
+
`sakk cap set/off`, `sakk spend` and `sakk prices` commands followed (see the
|
|
781
|
+
F-161 section above). A fetching prices updater — the one network call —
|
|
782
|
+
was then still planned; it was never built and is retracted (F-175).
|
|
783
|
+
|
|
784
|
+
### Fixed — F-105 archive, from the independent review (F-159)
|
|
785
|
+
|
|
786
|
+
- `sakk ledger verify` now checks the call-id index as well as the segment
|
|
787
|
+
chain, so a lost or wrong index (which silently re-admitted a sealed
|
|
788
|
+
call-id — a double-spend) is caught; `reserve` refuses when the whole index
|
|
789
|
+
is gone but segments are sealed, and `sakk ledger reindex` rebuilds it from
|
|
790
|
+
cold.
|
|
791
|
+
- verify sums the sealed total per segment exactly as it is stored, so an
|
|
792
|
+
intact chain never false-fails on float order.
|
|
793
|
+
- `sakk ledger export --out` reports a typed `LEDGER_EXPORT_FAILED` instead of
|
|
794
|
+
a raw stack. The spec now states verify detects accidental corruption and
|
|
795
|
+
loss, not tampering by a writer who can recompute the unkeyed hashes.
|
|
796
|
+
|
|
797
|
+
### Added — the ledger's hot/cold split (F-105, schema v3)
|
|
798
|
+
|
|
799
|
+
- Settled entries are sealed into an append-only, hash-chained cold trail
|
|
800
|
+
(`<ledger>.cold/`), with a call-id index (`<ledger>.idx/`), so the live
|
|
801
|
+
`ledger.json` stays small and `reserve`/`settle`/`status` cost the same
|
|
802
|
+
whatever the history — 2万 entries used to cost 96 ms a call, 10万 539 ms;
|
|
803
|
+
now a call is flat. No entry is dropped and nothing is capped.
|
|
804
|
+
- The gate reads only the hot file; its total is `archive.settledUsd` plus the
|
|
805
|
+
hot entries. Cold integrity is on demand: `sakk ledger verify` walks the
|
|
806
|
+
hash chain (exit 1 if a segment is lost or altered), `sakk ledger export`
|
|
807
|
+
rebuilds one flat ledger from cold + hot.
|
|
808
|
+
- Schema `sakk-ledger.v3` (adds the `archive` summary; a ledger becomes v3 the
|
|
809
|
+
first time it seals and an older binary refuses it by schema name). v1/v2
|
|
810
|
+
ledgers read and migrate on first seal.
|
|
811
|
+
- In-flight cap (public API): at most 10,000 reservations may be outstanding
|
|
812
|
+
at once (`LEDGER_FULL`); settling what is open always works. New refusal
|
|
813
|
+
code `LEDGER_ARCHIVE_BROKEN`. `SEAL_AT` (settled kept hot before sealing) is
|
|
814
|
+
a tuning knob via `SAKK_SEAL_AT`, default 1000.
|
|
815
|
+
- The ledger's durable write now uses one shared hardened writer
|
|
816
|
+
(`writeFileAtomic` in `append.mjs`), used for the hot file and the cold
|
|
817
|
+
segments and index shards.
|
|
818
|
+
|
|
819
|
+
### Added — the agent guard (open-source v1, M1)
|
|
820
|
+
|
|
821
|
+
- `sakk hook --agent claude-code|cursor|codex`: one hook for all three
|
|
822
|
+
agents. Each tool call is classified into actions (`file.read`,
|
|
823
|
+
`file.write`, `file.delete`, `secret.read`, `git.destructive`,
|
|
824
|
+
`prod.delete`, `deploy`, `package.install`, `net.download`,
|
|
825
|
+
`shell.constructed`, `shell.run`, `mcp.<server>.<tool>`) and judged by the
|
|
826
|
+
policy kernel. Any error is a deny with a code.
|
|
827
|
+
- `sakk guard install|uninstall <agent> [--user]`, `sakk try "<command>"`,
|
|
828
|
+
`sakk doctor` (checks the hook really fires).
|
|
829
|
+
- `policies/guard-default.json`: the shipped policy (docs/GUARD.md).
|
|
830
|
+
- Writing outside the project (your home folder and elsewhere, temporary
|
|
831
|
+
folders excepted) now asks first (`ask-write-outside`): the home folder is
|
|
832
|
+
where a manipulated agent would otherwise leave a shell profile or an SSH
|
|
833
|
+
key to persist, so an outbound write is a confirmation, not a silent allow.
|
|
834
|
+
- Scopes (public API): repo-relative, `~sakk/…`, `~home/…`,
|
|
835
|
+
`~outside/<absolute>`, and `~protected/repo|home|sakk` for a delete that
|
|
836
|
+
would take the repository, the home folder or Sakk's state with it
|
|
837
|
+
(F-146).
|
|
838
|
+
- A broken rule file is isolated, never a refusal of every call (RC-09):
|
|
839
|
+
- A project policy that fails to load gives way to the last one that
|
|
840
|
+
loaded, kept in `$SAKK_HOME/policy-good/`. Refusals say so, and
|
|
841
|
+
`alerts.jsonl` records `POLICY_FALLBACK`.
|
|
842
|
+
- With nothing to fall back to, or an unreadable config or ledger, only
|
|
843
|
+
reading the project goes on (`ruleId: rules-unknown`).
|
|
844
|
+
- New refusal codes in hook answers (public API): `HOOK_PAYLOAD_INVALID`,
|
|
845
|
+
`HOOK_INPUT_TOO_LARGE`, `HOOK_POLICY_UNREADABLE`, `HOOK_LEDGER_UNREADABLE`,
|
|
846
|
+
`HOOK_CONFIG_UNREADABLE`, `HOOK_CONTEXT_INVALID`, `HOOK_DEADLINE`,
|
|
847
|
+
`HOOK_PATH_UNREADABLE`, `HOOK_USAGE`.
|
|
848
|
+
|
|
849
|
+
### Added — bounds on what the books store (F-105, first part; C-04)
|
|
850
|
+
|
|
851
|
+
- New refusal codes (public API):
|
|
852
|
+
- `INPUT_TOO_LONG`: a call id, run id, task id or agent id over 256
|
|
853
|
+
characters, a name over 256, or a halt reason over 1024.
|
|
854
|
+
- `POLICY_TOO_LARGE`: the policy file is over 4 MiB.
|
|
855
|
+
- `ESTIMATE_INVALID` now also refuses estimates below $0.000001.
|
|
856
|
+
- `test/cost-bounds.test.mjs` lists every exported function with the
|
|
857
|
+
refusal that bounds it; a new export without an entry fails the suite.
|
|
858
|
+
|
|
859
|
+
### Fixed — found while building M1 (F-141 to F-149)
|
|
860
|
+
|
|
861
|
+
- F-141: a checker that could not load locked the repository it guards. It
|
|
862
|
+
now loads its library lazily, and while it cannot load only the gate's own
|
|
863
|
+
files can be repaired.
|
|
864
|
+
- F-142: the adversarial review showed as passed when no key was set. It
|
|
865
|
+
is now skipped at the job (shown as skipped), and a review that cannot be
|
|
866
|
+
made or posted fails.
|
|
867
|
+
- F-143: a hung synchronous child could hold a test run with no bound.
|
|
868
|
+
`test/bounded-spawn.mjs` now bounds every one.
|
|
869
|
+
- F-144: the gate's files are listed once, in `scripts/gate-files.json`.
|
|
870
|
+
- F-145: a wrapper's flag value (`sudo -u root`) is no longer read as the
|
|
871
|
+
command.
|
|
872
|
+
- F-146: a temp-folder allow no longer covers the home folder, and a delete
|
|
873
|
+
of the repository itself is named and denied.
|
|
874
|
+
- F-147: drill ids of any length are recognised.
|
|
875
|
+
- F-148: the guard spec's rule table is compared with the shipped policy.
|
|
876
|
+
- F-149: every mutation-drill anchor is checked by `npm test`, not only by
|
|
877
|
+
the hour-long drill.
|
|
878
|
+
- F-150: the F-105 input bounds no longer lock settled money — a reservation
|
|
879
|
+
made before ids were bounded still settles — and a non-string `runId`,
|
|
880
|
+
`taskId` or `tokens` is now refused (`INPUT_INVALID`) instead of stored
|
|
881
|
+
whole; reconciliation reasons are truncated so a long id cannot jam the
|
|
882
|
+
books.
|
|
883
|
+
- F-151: the cost-bounds detector now accepts only a real test as a guard,
|
|
884
|
+
never a circular self-reference, and covers the non-string inputs.
|
|
885
|
+
- F-152: `max_reservable_usd` is computed once in the ledger and reports 0
|
|
886
|
+
when the headroom is below the estimate floor, so the figure `status`
|
|
887
|
+
shows is one `reserve` actually accepts.
|
|
888
|
+
- F-153: commands that move or destroy files but were not in the classifier's
|
|
889
|
+
set — `dd`, `tar --remove-files`, `rsync --delete`, in-place `perl`/`ruby` —
|
|
890
|
+
are now modelled as the writes and deletes they perform instead of being
|
|
891
|
+
waved through as `shell.run`.
|
|
892
|
+
- F-154: `find -exec`/`-ok` and `xargs` are judged by the command they run
|
|
893
|
+
(a remover deletes, a reader reads), not by the runner's name.
|
|
894
|
+
- F-155: reading a secret file through a command outside the reader list
|
|
895
|
+
(`base64`, `openssl`, `dd if=`) is still a `secret.read`.
|
|
896
|
+
- F-156: a git value-option (e.g. `--namespace x`) can no longer shift the
|
|
897
|
+
subcommand so a `push --force` reads as ordinary work.
|
|
898
|
+
- F-157: while the checker cannot load it now resolves a path's symlink leaf
|
|
899
|
+
(not just its parents), judges a Bash call by its command rather than a
|
|
900
|
+
stray `file_path` key, and denies instead of crashing when the library
|
|
901
|
+
throws a non-error; the loop's shell classifier treats `node --test` as
|
|
902
|
+
read-only only when it injects no module.
|
|
903
|
+
The same review's own detectors are strengthened too: the bounded-spawn
|
|
904
|
+
preload now bounds an explicit null/undefined options slot and the scripts
|
|
905
|
+
that spawn `node --test`, and the ci-honesty detector catches more
|
|
906
|
+
skip/failure-to-green shapes.
|
|
907
|
+
- F-158: the CI base checker runs in its own job with a fresh checkout, so the
|
|
908
|
+
pull request's own checker can no longer move HEAD or write the environment
|
|
909
|
+
to leave the base checker an empty range.
|
|
910
|
+
|
|
911
|
+
### Changed (release preparation)
|
|
912
|
+
|
|
913
|
+
- Halt, resume, reconcile and unlock records are now described as named:
|
|
914
|
+
they carry the name the operator typed and nothing verifies it (F-126).
|
|
915
|
+
A test fails if a user-facing document gives a record a security
|
|
916
|
+
property that no code provides.
|
|
917
|
+
- README states the open core's limits: self-declared names,
|
|
918
|
+
caller-reported costs, approval-required means refused, voluntary gating.
|
|
919
|
+
Dashboards are no longer listed as not planned; a web console is on the
|
|
920
|
+
enterprise roadmap.
|
|
921
|
+
|
|
922
|
+
### Fixed (regression of the original modules, findings F-80 … F-115)
|
|
923
|
+
|
|
924
|
+
- Ledger: the concurrency test was sequential and a lockless ledger passed
|
|
925
|
+
it — eight workers now race for real (drill MUT-Z); a ledger reached
|
|
926
|
+
through a symlink or hard link refuses (`LEDGER_PATH_NOT_CANONICAL`)
|
|
927
|
+
instead of forking the books; settle no longer makes a stale ledger
|
|
928
|
+
fresh; alerts and decision logs never follow a planted symlink; a fifo
|
|
929
|
+
ledger or policy refuses instead of blocking.
|
|
930
|
+
- `sakk status` judges reconciliation from both sources, shows a stale
|
|
931
|
+
ledger, exits 3 for either and reports nothing reservable when every
|
|
932
|
+
reserve would refuse; a named `sakk reconcile` confirms stale books.
|
|
933
|
+
- MCP: a nested frame can no longer crash the gate (bounded echo, frame
|
|
934
|
+
isolation); an agent cannot settle its own reconciliation entry; a
|
|
935
|
+
relative `--ledger` is resolved at startup.
|
|
936
|
+
- Policy: `*` inside agent/action names refuses at load; policies that can
|
|
937
|
+
only say yes refuse; decisions validate the path once.
|
|
938
|
+
- Eval: the child environment is an allowlist (operator credentials and
|
|
939
|
+
`NODE_OPTIONS` test flags no longer reach children); a scoring failure
|
|
940
|
+
is that case's FAIL; the answer key is checked in every mode; every
|
|
941
|
+
scorer rule has a negative test.
|
|
942
|
+
- CI: the review workflow actually posts and has a timeout; the timeout
|
|
943
|
+
detector reads every workflow.
|
|
944
|
+
|
|
945
|
+
### Added
|
|
946
|
+
|
|
947
|
+
- Attack rows for the ledger core, policy kernel, MCP enforcer and gold
|
|
948
|
+
harness environment (E5–E8); campaigns run in the audit and CI; every red
|
|
949
|
+
line names its drill anchors (26 mutants); `specs/ledger`.
|
|
950
|
+
|
|
951
|
+
### Added (self-evolution loop — docs/SELF_EVOLUTION.md)
|
|
952
|
+
|
|
953
|
+
- `npm run brief -- <paths>`: before a change, the past findings for those
|
|
954
|
+
files, the questions to answer first (`findings/prevention.json`), the
|
|
955
|
+
known blind spots of the checks that will run, and those checks. The
|
|
956
|
+
commit gate prints it.
|
|
957
|
+
- The ledger gate now requires `areas` on every finding, `since` on every
|
|
958
|
+
class-mechanism reference, and for every escape past an existing
|
|
959
|
+
detector a `mechanism_gap` and `mechanism_upgrade`; every class has a
|
|
960
|
+
prevention question; two escapes in a row that upgraded nothing need a
|
|
961
|
+
stop-line review. `findings-report` adds the past-detector escape rate
|
|
962
|
+
and the detector upgrade rate.
|
|
963
|
+
- Rule R-22; AGENTS.md "Before you change anything"; PR template section.
|
|
964
|
+
|
|
965
|
+
### Fixed (independent review on a second model, findings F-62 … F-79)
|
|
966
|
+
|
|
967
|
+
- Evidence bundles: a REAL run must name its model (verifier and
|
|
968
|
+
builder); every run records `harness_dirty` in `metrics.json`; a REAL
|
|
969
|
+
run on a modified or uncheckable harness tree is INCOMPLETE and its pack
|
|
970
|
+
requirements read `HARNESS_MODIFIED`, never `COVERED`; pack reports and
|
|
971
|
+
`bundle.json` are closed (every key required). The auditor procedure now
|
|
972
|
+
runs the CURRENT verifier from a clone — a bundle can ship an older
|
|
973
|
+
genuine verifier that misses later checks.
|
|
974
|
+
- Gold runner: a copy of sakk inside another git repository refuses
|
|
975
|
+
(`HARNESS_GIT_REQUIRED`) instead of recording that repository's commit.
|
|
976
|
+
- Secret scan: an exempt fiction-token match no longer hides a real secret
|
|
977
|
+
it swallowed; `--staged` ignores git replace refs; PGP private-key
|
|
978
|
+
blocks are detected; `--install-hook` writes pre-commit AND
|
|
979
|
+
pre-merge-commit hooks (both or neither) and the hook refuses with
|
|
980
|
+
`SCAN_HOOK_STALE` after a node upgrade instead of a bare shell error.
|
|
981
|
+
- Kill switch: halt and resume keep `updatedAt`, so a stop never makes a
|
|
982
|
+
stale ledger fresh; `halt.lifts` keeps who lifted which halt in the
|
|
983
|
+
ledger (v2 halt shape, unreleased); blank names refuse; `sakk status`
|
|
984
|
+
escapes operator text.
|
|
985
|
+
- Case packs: `gaps` and `not_covered` must be non-empty; pack evidence
|
|
986
|
+
sentences state what the cases check (G03 markers, G06's reference gate,
|
|
987
|
+
git facts the agent can rewrite).
|
|
988
|
+
|
|
989
|
+
### Added (class detectors)
|
|
990
|
+
|
|
991
|
+
- `test/bundle-absence.test.mjs`: deletes every key of every bundle
|
|
992
|
+
document; any deletion that still verifies must be declared.
|
|
993
|
+
- Secret-scan context matrix (every secret shape × every fiction-token
|
|
994
|
+
position); ledger-write classification (confirms the books or not);
|
|
995
|
+
the stopwatch detector covers deadlines; the repository self-scan test
|
|
996
|
+
scans the root, not a list of directories.
|
|
997
|
+
- Mutation drill: 23 anchors (was 18).
|
|
998
|
+
|
|
999
|
+
### Known, not fixed
|
|
1000
|
+
|
|
1001
|
+
- F-75: false positives on translation files and Ansible variables
|
|
1002
|
+
(precision needs a benign corpus and a re-measured attack list).
|
|
1003
|
+
- F-76: the scorer reads changed files from the agent's own `.git`;
|
|
1004
|
+
disclosed in every affected pack requirement; the fix changes the
|
|
1005
|
+
frozen scorer and needs a decision.
|
|
1006
|
+
|
|
1007
|
+
### Added (testing-standard audit)
|
|
1008
|
+
|
|
1009
|
+
- `npm run audit`: one command for every layer of the AI-agent testing
|
|
1010
|
+
standard that applies to Sakk — T0 suites, format, secret self-scan,
|
|
1011
|
+
mutation drill, ledger and policy fuzz; T2 `npm run golden-k` (k = 3,
|
|
1012
|
+
pass@1 with Wilson interval, pass^k; FAKE executor = harness only);
|
|
1013
|
+
T3 `npm run attack-asr` (attack list over the kill switch, secret
|
|
1014
|
+
scanning, case packs and evidence bundles, ≥ 5 variants per payload,
|
|
1015
|
+
CLAIMED ASR must be 0, documented non-coverage reported); §14 ledger gate
|
|
1016
|
+
and `npm run findings-report`. Exit codes taken directly; logs in
|
|
1017
|
+
`audit-logs/`. Layer map: `docs/testing-profile.md`.
|
|
1018
|
+
- Mutation drill: 23 anchors (was 7) — one per core guard, including the
|
|
1019
|
+
per-agent halt and resume scoping, the HARNESS_ONLY and HARNESS_MODIFIED
|
|
1020
|
+
rules, the secret-scan exemption (both directions) and staged type
|
|
1021
|
+
changes, the bundle verifier's spend, metrics, parsing, model and
|
|
1022
|
+
closed-report checks, the MCP policy gate, the path matcher, the
|
|
1023
|
+
paid-step cap and the bundle self-check. The certificate expects 23.
|
|
1024
|
+
- `specs/case-packs`, `specs/secret-scan`, `specs/evidence-bundle`: the
|
|
1025
|
+
three-tables files these L1 changes lacked; `test/core-specs.test.mjs`
|
|
1026
|
+
keeps every core file specified and drilled.
|
|
1027
|
+
|
|
1028
|
+
### Changed (kill switch — behaviour of existing APIs)
|
|
1029
|
+
|
|
1030
|
+
- `reserve()` now refuses an `agentId` that is not a non-empty string
|
|
1031
|
+
(`AGENT_ID_INVALID`); before, `agentId: 7` or `""` was accepted and
|
|
1032
|
+
recorded. `null`/absent stays anonymous. Reason: a numeric id could never
|
|
1033
|
+
match a per-agent halt.
|
|
1034
|
+
- The `sakk` CLI refuses unknown flags, the `--flag=value` form and repeated
|
|
1035
|
+
flags with exit 2 (`USAGE_UNKNOWN_ARGUMENT`, `USAGE_EQUALS_FORM`,
|
|
1036
|
+
`USAGE_REPEATED_FLAG`); before, they were ignored — `sakk resume --by op
|
|
1037
|
+
--agent=bot` lifted the GLOBAL halt.
|
|
1038
|
+
|
|
1039
|
+
### Added (kill switch)
|
|
1040
|
+
|
|
1041
|
+
- `halt()` / `resume()` and `sakk halt|resume --by <name> [--agent <id>]`:
|
|
1042
|
+
stop all new spending (or one agent's) with a named record stored in a
|
|
1043
|
+
top-level ledger key `halt` — deliberately outside `flags`, which four
|
|
1044
|
+
money paths rewrite wholesale. **Schema `sakk-ledger.v2`**: the first
|
|
1045
|
+
halt upgrades the file to v2, which REQUIRES `halt` (deleting it refuses
|
|
1046
|
+
rather than reading as "not halted"); v1 forbids it; resume keeps v2
|
|
1047
|
+
with an emptied record. Genesis ledgers stay v1. New public codes:
|
|
1048
|
+
`LEDGER_HALTED`, `AGENT_HALTED`, `HALT_BY_REQUIRED`,
|
|
1049
|
+
`RESUME_BY_REQUIRED`, `AGENT_ID_INVALID` (reserve/halt/resume now refuse
|
|
1050
|
+
a non-string or empty agent id — a numeric id could never match a halt;
|
|
1051
|
+
`null` stays anonymous); `sakk status` exits **4** while globally halted
|
|
1052
|
+
(4 wins over 3; both conditions are printed). Agent ids are matched
|
|
1053
|
+
exactly as own keys, so ids like `__proto__` or `constructor` halt and
|
|
1054
|
+
resume like any other.
|
|
1055
|
+
Halting works on stale ledgers and during reconciliation; the CLI
|
|
1056
|
+
retries lock contention; settle stays open for in-flight calls; first
|
|
1057
|
+
halt wins; alerts `HALT_SET` / `HALT_REFUSED` / `HALT_CLEARED`. MCP
|
|
1058
|
+
status shows halts, and no MCP tool can halt or resume. Older versions
|
|
1059
|
+
refuse a v2 ledger by schema name — they can never ignore a stop. Drill gains MUT-G (7 mutants); invariant fuzz gains halt ops and
|
|
1060
|
+
refuses vacuous seeds that never probe a halt.
|
|
1061
|
+
|
|
1062
|
+
### Added (secret scanning)
|
|
1063
|
+
|
|
1064
|
+
- `sakk scan <paths> | --staged | --install-hook` and `src/secret-scan.mjs`
|
|
1065
|
+
(ported from the predecessor system, English-ized and hardened): eight
|
|
1066
|
+
rules (DSNs with passwords, URL-encoded passwords, AWS keys, `sk-`/`rk-`
|
|
1067
|
+
keys, GitHub tokens, private-key blocks, bearer tokens, password
|
|
1068
|
+
assignments); fingerprint-only findings; a single declared-fiction
|
|
1069
|
+
exemption token, judged on the secret part of a match only (a token in a
|
|
1070
|
+
username exempts nothing). New over the predecessor: staged-blob scanning
|
|
1071
|
+
for pre-commit (read by blob id, so type changes and names like `0:foo`
|
|
1072
|
+
are scanned; submodule pointers skipped and counted), a hook that runs the
|
|
1073
|
+
installed copy by absolute path and is written with `wx` (never overwrites,
|
|
1074
|
+
never writes through a symlink), symlinks skipped (no loops, no escaping
|
|
1075
|
+
the tree), NUL-sniffed binary skipping with UTF-16 decoded as text,
|
|
1076
|
+
keyed fingerprints (random per run, `SAKK_SCAN_KEY` to compare), redacted
|
|
1077
|
+
secret-bearing paths, length-bounded patterns (a 200KB hostile line scans
|
|
1078
|
+
in linear time). Exit 0 clean / 1 findings / 2 the scan could not do its
|
|
1079
|
+
job (`SCAN_PATH_MISSING`, `SCAN_UNREADABLE`, `SCAN_NOT_A_REPO`,
|
|
1080
|
+
`SCAN_HOOK_EXISTS`, `SCAN_HOOK_DIR_MISSING`, `SCAN_FAILED`). Recognized
|
|
1081
|
+
formats only — unquoted `.env` passwords are not detected.
|
|
1082
|
+
Verify-pass round: `--staged` reads each blob id from one `git diff
|
|
1083
|
+
--cached --raw` record (two names that decode alike, or a run from a
|
|
1084
|
+
subdirectory, can no longer hide a blob) and batches blob reads; staged
|
|
1085
|
+
symlink targets are scanned; explicit roots are followed, a fifo/device
|
|
1086
|
+
or a >256 MB input is `SCAN_UNREADABLE`; `--staged` with paths is
|
|
1087
|
+
`SCAN_USAGE`; git missing is `SCAN_GIT_MISSING`; hook write failures are
|
|
1088
|
+
`SCAN_HOOK_UNWRITABLE`, installing from the npx cache is
|
|
1089
|
+
`SCAN_HOOK_EPHEMERAL`; the hook line is shell-quoted and chmod'ed past the
|
|
1090
|
+
umask. Patterns: `sk`/`rk` keys need a digit (identifiers no longer
|
|
1091
|
+
match), `mariadb`/`rediss`/`amqps`/`mssql`/`sqlserver` and empty-user URLs
|
|
1092
|
+
added, aligned `password = "…"` found, a fiction token glued to a real
|
|
1093
|
+
key exempts nothing. UTF-32 and odd-length UTF-16 decoded; names with
|
|
1094
|
+
control characters printed escaped; exit codes set without truncating
|
|
1095
|
+
piped output.
|
|
1096
|
+
Verify round 3: key shapes the digit rule had dropped are back
|
|
1097
|
+
(`sk_live_`/`rk_test_`-prefixed keys need no digit; `sk_<hex>` and `sk-`
|
|
1098
|
+
keys of 40+ chars match); `--no-relative` so `diff.relative=true` cannot
|
|
1099
|
+
hide files from a subdirectory scan; values on the next line
|
|
1100
|
+
(`password:` + newline, wrapped `Bearer`) found; placeholders such as
|
|
1101
|
+
`${REDIS_PASSWORD}` in the secret position are not secrets; a key glued
|
|
1102
|
+
right after the fiction token is found; C1 controls and U+2028/2029 in
|
|
1103
|
+
names escaped; a missing staged object is named as missing;
|
|
1104
|
+
`--install-hook` with paths is `SCAN_USAGE`, without git
|
|
1105
|
+
`SCAN_GIT_MISSING`; ephemeral-cache detection covers npx hash dirs, bunx
|
|
1106
|
+
and pnpm dlx without refusing ordinary paths that contain `_npx`.
|
|
1107
|
+
|
|
1108
|
+
### Added (case packs)
|
|
1109
|
+
|
|
1110
|
+
- `eval/packs/`: gold cases grouped by regulatory requirement —
|
|
1111
|
+
`cbuae-ai-oversight` (4), `dora-ict-change` (3), `difc-data-protection` (3)
|
|
1112
|
+
requirements over 9 of the 12 cases (G04 and G09 map to no requirement in
|
|
1113
|
+
these packs; control-plane G12 cannot run against a model, and packs admit
|
|
1114
|
+
only `execution_mode: "model"` cases). `gold-runner --pack <id>` runs a
|
|
1115
|
+
pack's cases and writes `pack-report.json` with its provenance (mode, run
|
|
1116
|
+
id, harness commit, stop info). FAKE runs read `HARNESS_ONLY`; only REAL
|
|
1117
|
+
runs can read `COVERED`. Every requirement carries `gaps`, every pack
|
|
1118
|
+
`not_covered` — mandatory fields, so the honest boundary travels with the
|
|
1119
|
+
claim. Refusals: `PACK_UNKNOWN`, `PACK_INVALID`, `SELECTOR_CONFLICT`,
|
|
1120
|
+
`USAGE_EQUALS_FORM`, `USAGE_UNKNOWN_ARGUMENT`, `USAGE_REPEATED_FLAG`,
|
|
1121
|
+
`USAGE_MISSING_VALUE`, `USAGE_MODE_CONFLICT` — the runner now refuses any
|
|
1122
|
+
argument it does not know (a misspelled `--packs x` used to run ALL
|
|
1123
|
+
cases). A REAL pack report names the model (`model`: provider, model id,
|
|
1124
|
+
executor binary, whether `CLAUDE_BIN` overrode it) and whether the harness
|
|
1125
|
+
tree was dirty; `not_covered` is marked non-exhaustive. **Exit code:** a
|
|
1126
|
+
REAL batch that stops early now exits 1 even if every case that ran passed.
|
|
1127
|
+
Verify round 3: `harness_dirty` is computed from the repository top (it
|
|
1128
|
+
could never be true before) and excludes the run's own out dir;
|
|
1129
|
+
`model.executor_binary_resolved` records the file PATH actually ran, and
|
|
1130
|
+
the REAL meaning says the binary is recorded, not verified; empty values
|
|
1131
|
+
and flags the chosen mode never reads refuse (`USAGE_FLAG_NOT_FOR_MODE`).
|
|
1132
|
+
`--validate` checks all packs and reports `packs_ok`.
|
|
1133
|
+
|
|
1134
|
+
### Added (evidence bundles)
|
|
1135
|
+
|
|
1136
|
+
- `scripts/evidence-bundle.mjs <out-dir> --to <file.tgz>` (`npm run
|
|
1137
|
+
evidence`): packs a run into one self-contained bundle — the out-dir, the
|
|
1138
|
+
case definitions read from the run's own harness commit (`git show`),
|
|
1139
|
+
`bundle.json` (schema `sakk-evidence-bundle.v1`), `MANIFEST.sha256`, a
|
|
1140
|
+
README and `verify.mjs`, a zero-dependency verifier that runs with Node
|
|
1141
|
+
alone. The auditor checks the bundle WITHOUT running its code first:
|
|
1142
|
+
fingerprint = `shasum -a 256 MANIFEST.sha256` against a value received
|
|
1143
|
+
out of band, then `shasum -a 256 -c MANIFEST.sha256` (AUDITOR_REPLAY §6).
|
|
1144
|
+
verify.mjs then checks integrity (regular files only — links are
|
|
1145
|
+
failures, never followed) and internal consistency: one run, one commit,
|
|
1146
|
+
one executor mode cross-checked against every trace's provider; case list
|
|
1147
|
+
vs case directories; verdict vs violations; per-case cost vs its own
|
|
1148
|
+
ledger; every metric except trace_completeness_rate recomputed (pass
|
|
1149
|
+
rates, first-pass yield, counts, costs, p50/p95); pack report recomputed
|
|
1150
|
+
(a FAKE run can only read HARNESS_ONLY). Unreadable, falsy or ill-typed
|
|
1151
|
+
files are named failures (`CASE_FILE_INVALID`, `METRICS_UNVERIFIABLE`,
|
|
1152
|
+
`BUNDLE_META_INVALID`, `NOT_A_REGULAR_FILE`, `MODE_MISMATCH`,
|
|
1153
|
+
`CASE_SET_MISMATCH`, …), never skipped checks. Building refuses
|
|
1154
|
+
(`BUNDLE_SOURCE_INVALID` incl. symlinks and commits the clone lacks,
|
|
1155
|
+
`BUNDLE_TARGET_EXISTS`, `BUNDLE_SELF_CHECK_FAILED`, `BUNDLE_WRITE_FAILED`),
|
|
1156
|
+
verifies the archive itself after unpacking it fresh, and places it with
|
|
1157
|
+
an exclusive link — no partial file is ever left at the target.
|
|
1158
|
+
Authenticity is out of scope until the anchored ledger ships: a full
|
|
1159
|
+
rebuild passes verify.mjs; only the out-of-band fingerprint catches it.
|
|
1160
|
+
Verify-pass round: a third outcome, **INCOMPLETE** (exit 3, like `sakk
|
|
1161
|
+
status`), for a genuine run with unsettled/unreconciled spend, unknown
|
|
1162
|
+
cost, a stopped batch, or selected cases that never ran — such a run used
|
|
1163
|
+
to read PASS while its spend was understated; it now ships labelled and
|
|
1164
|
+
never reads PASS. The runner records `selected_cases` and `stopped` in
|
|
1165
|
+
`metrics.json` (bundles refuse older run dirs). Per-case ledgers are
|
|
1166
|
+
checked against sakk's ledger rules, this run's id and the case's budget
|
|
1167
|
+
(`LEDGER_INVALID`); mode labels and trace model ids join the mode check;
|
|
1168
|
+
pack reports recompute their quoted verdicts, commit, total and stop
|
|
1169
|
+
record; lowercase rule names, duplicate JSON keys (`DUPLICATE_JSON_KEY`),
|
|
1170
|
+
negative durations and empty directories (`EMPTY_DIRECTORY`) fail;
|
|
1171
|
+
`verifyBundle` never throws. The shipped verifier must be committed
|
|
1172
|
+
(`BUNDLE_VERIFIER_DIRTY`) and its commit is recorded (`verifierCommit`);
|
|
1173
|
+
targets must be `.tgz`/`.tar.gz` (`BUNDLE_TARGET_INVALID`), a dangling
|
|
1174
|
+
symlink counts as existing, files over 1 GiB are refused. A SIGINT or
|
|
1175
|
+
SIGTERM during a build lets it finish or fail as a whole — it does not
|
|
1176
|
+
cancel it — and temp files are always removed (SIGKILL excepted).
|
|
1177
|
+
Verify round 3: a truthful overrun past a case cap (sakk flagged it) is
|
|
1178
|
+
INCOMPLETE (`SPEND_OVER_CAP`) and ships, labelled — it used to be
|
|
1179
|
+
unbundleable; an unflagged one is a forgery. Ledgers are checked as
|
|
1180
|
+
closed documents (unknown keys, missing updatedAt, duplicate callIds, an
|
|
1181
|
+
overrun without the flag sakk sets); every paid step in a trace must have
|
|
1182
|
+
a ledger entry and vice versa; estimated costs (`cost_is_estimate`,
|
|
1183
|
+
`real-exec.json` without a cost) are INCOMPLETE; pack reports may only
|
|
1184
|
+
quote selected cases and must name the run's model; stop records compare
|
|
1185
|
+
regardless of key order; hidden-dotfile targets are refused.
|
|
1186
|
+
|
|
1187
|
+
### Fixed (gold runner)
|
|
1188
|
+
|
|
1189
|
+
- A relative `--out` (e.g. `--out run1`) reached executors and verification
|
|
1190
|
+
commands that run in other working directories, so every case scored
|
|
1191
|
+
FAIL while the same run with an absolute path passed. The out path is now
|
|
1192
|
+
resolved to an absolute path once, at parse time; the printed `outDir` is
|
|
1193
|
+
absolute. Fails safe before and after (no false PASS), but a real-model
|
|
1194
|
+
run with a relative path would have spent budget on wrong verdicts.
|
|
1195
|
+
|
|
1196
|
+
### Fixed (hardening campaign, 2026-10-16)
|
|
1197
|
+
|
|
1198
|
+
- **C-4 (disk-full could install broken books)**: `writeSync`'s return was
|
|
1199
|
+
unchecked, so a short write under ENOSPC could be fsynced and renamed
|
|
1200
|
+
into place — an atomically installed TRUNCATED ledger (found by the
|
|
1201
|
+
container matrix's tmpfs probe; the post-mortem read came back
|
|
1202
|
+
`LEDGER_MALFORMED`). Writes now account for every byte, zero-progress
|
|
1203
|
+
refuses, any write failure cleans the temp file and surfaces as the new
|
|
1204
|
+
public code `LEDGER_WRITE_FAILED` — and the matrix probe asserts the
|
|
1205
|
+
books remain readable after disk-full.
|
|
1206
|
+
- **C-1**: `sakk-ledger.v1` is a CLOSED schema — unknown top-level keys
|
|
1207
|
+
(incl. a smuggled own `__proto__` key) refuse `LEDGER_SCHEMA_INVALID`.
|
|
1208
|
+
- Campaign harnesses added under `scripts/campaign/` (soak with SIGKILL
|
|
1209
|
+
injection, fault-injection corpus, seeded MCP torture, container matrix
|
|
1210
|
+
with ENOSPC probe); measured lock-contention refusal rate ~0.016% under
|
|
1211
|
+
8-way chaos (C-2).
|
|
1212
|
+
|
|
1213
|
+
### Added (policy engine kernel — 🔒L2, design signed 2026-10-15)
|
|
1214
|
+
|
|
1215
|
+
- `sakk/policy` export: `loadPolicy` (validate + SHA-256 over raw bytes) ·
|
|
1216
|
+
`decide` (pure, first-match-wins, mandatory `default_verdict`) ·
|
|
1217
|
+
`logDecision` (hash-pinned JSONL) · `PolicyError`. New public error codes:
|
|
1218
|
+
`POLICY_UNREADABLE`, `POLICY_SCHEMA_INVALID`, `POLICY_VACUOUS_ALLOW`,
|
|
1219
|
+
`POLICY_RULE_SHADOWED`, `CONTEXT_INVALID`, `DECISION_UNLOGGABLE`. The
|
|
1220
|
+
kernel never imports the ledger; enforcers compose the two.
|
|
1221
|
+
- `policies/` reference profiles (`permissive-dev`, `regulated-strict`) ship
|
|
1222
|
+
in the npm package; documented schema at `eval/schemas/policy.schema.json`
|
|
1223
|
+
with a drift-guard test against the kernel's own validation.
|
|
1224
|
+
- Path matcher extracted to `src/match.mjs` — ONE implementation shared by
|
|
1225
|
+
scorer and policy kernel (scope entries are prefix rules, not wildcards).
|
|
1226
|
+
- Mutation drill grows to 6 mutants (MUT-E rule-order, MUT-F vacuous-allow);
|
|
1227
|
+
the pristine control now runs both suites. `npm run fuzz:policy`: seeded
|
|
1228
|
+
policy/context fuzz (5 invariants, benign + adversarial size probes,
|
|
1229
|
+
replayable seed).
|
|
1230
|
+
- Decision log line format (public API): one JSON object per line with
|
|
1231
|
+
`at` (ISO timestamp) · `by` (string|null) · `verdict` · `ruleId`
|
|
1232
|
+
(string|null) · `approversMin` (only on require_approval) · `policyHash`
|
|
1233
|
+
(64-hex) · `context` ({agent, action, scope, estimatedUsd, callId}).
|
|
1234
|
+
|
|
1235
|
+
### Added (MCP enforcer)
|
|
1236
|
+
|
|
1237
|
+
- `sakk mcp`: zero-dependency MCP stdio server exposing `sakk_reserve` /
|
|
1238
|
+
`sakk_settle` / `sakk_status` — the policy kernel's first enforcer.
|
|
1239
|
+
Ledger path is required (`MCP_LEDGER_PATH_REQUIRED`, no cwd default); a
|
|
1240
|
+
configured policy is loaded once, hash-pinned, and refuses startup on
|
|
1241
|
+
any `POLICY_*` code. Policy refusals surface as `POLICY_DENIED` /
|
|
1242
|
+
`POLICY_APPROVAL_REQUIRED` (public API) with the full decision attached;
|
|
1243
|
+
every decision logs to `<ledger>.decisions.jsonl` beside the ledger. 1 MiB
|
|
1244
|
+
frame cap; contract tests drive the real binary over real frames,
|
|
1245
|
+
including a thinness proof (library replay yields identical books).
|
|
1246
|
+
|
|
1247
|
+
### Fixed (certificate + MCP adversarial-review round)
|
|
1248
|
+
|
|
1249
|
+
R-05 on PR #3 found 4 merge-blocking defects and 11 more; all closed with
|
|
1250
|
+
attack-shaped regressions:
|
|
1251
|
+
|
|
1252
|
+
- MCP `sakk_settle` swallowed the ledger-halt signal (replied
|
|
1253
|
+
`settled:true` while the whole ledger froze) and truthy-coerced
|
|
1254
|
+
`failedUnknownCost`, breaking thinness — the library's return now rides
|
|
1255
|
+
the response and arguments pass verbatim (a divergence test pins MCP ==
|
|
1256
|
+
library behavior).
|
|
1257
|
+
- Certificate freshness now requires EVERY drill kill to carry a verified
|
|
1258
|
+
hash block (a parse drift silently shrank the verified set); the tree is
|
|
1259
|
+
re-verified right before rendering (TOCTOU); refusals delete any stale
|
|
1260
|
+
certificate; the final write is atomic; `src/mcp.mjs` and the issuer
|
|
1261
|
+
itself join the certified surface; suite summaries with skipped/todo
|
|
1262
|
+
holes refuse; internal failures obey the 0/2 exit contract.
|
|
1263
|
+
- MCP hardening: string args cap at 4096 (`MCP_ARG_TOO_LONG`, public
|
|
1264
|
+
API); batch/primitive frames answer -32600 (never a silent drop); one
|
|
1265
|
+
refusal per oversized flood; set-but-empty `SAKK_POLICY_PATH` refuses to
|
|
1266
|
+
start; `sakk_status` reads with the CLI's Infinity staleness; agent
|
|
1267
|
+
identity reaches the ledger (`agentId`); decision log is per-ledger
|
|
1268
|
+
(`<ledger>.decisions.jsonl`).
|
|
1269
|
+
|
|
1270
|
+
### Added (gate non-regression certificate)
|
|
1271
|
+
|
|
1272
|
+
- `npm run certificate`: executes every gate (both suites, gold manifest,
|
|
1273
|
+
6-mutant drill with evidence-freshness verification against the current
|
|
1274
|
+
bytes, both seeded fuzz legs, double-run reproducibility fingerprint) and
|
|
1275
|
+
renders CERTIFICATE.md. Fail-closed: any red gate, stale drill evidence
|
|
1276
|
+
or dirty tree refuses with a `CERT_*` code and emits nothing — the
|
|
1277
|
+
document's existence is the claim.
|
|
1278
|
+
|
|
1279
|
+
### Fixed (policy kernel adversarial-review round, 2026-10-15)
|
|
1280
|
+
|
|
1281
|
+
The R-05 independent review of the first kernel implementation found 4
|
|
1282
|
+
merge-blocking defects and 8 more; all fixed with attack-shaped regressions:
|
|
1283
|
+
|
|
1284
|
+
- **F1 (critical, fail-open)**: a `default_verdict: "require_approval"`
|
|
1285
|
+
decision carried no `approversMin`, so the reference enforcer's
|
|
1286
|
+
`0 < undefined` comparison acted with ZERO approvals. Every
|
|
1287
|
+
require_approval decision — default path included — now carries it.
|
|
1288
|
+
- **F2**: non-canonical scopes (`prod//x`, `./prod/x`, `/prod/x`) matched no
|
|
1289
|
+
rule and slid past explicit denies into later allows; they now refuse
|
|
1290
|
+
(`CONTEXT_INVALID`). Unicode normalization is the enforcer's documented
|
|
1291
|
+
duty.
|
|
1292
|
+
- **F3**: the reference enforcer counted the same approver twice toward
|
|
1293
|
+
four-eyes; approvers are now a named set excluding the requester.
|
|
1294
|
+
- **F4**: a schema-legal policy could park the quadratic shadow scan for
|
|
1295
|
+
minutes; a work budget now refuses with the new `POLICY_TOO_COMPLEX`, and
|
|
1296
|
+
the fuzz size probe is adversarial-shaped instead of best-case.
|
|
1297
|
+
- F5 decide copies the context BEFORE validating (getter TOCTOU); F6 the
|
|
1298
|
+
log gate accepts only decide()-shaped decisions and serialization
|
|
1299
|
+
failures are typed; F7 duplicate JSON keys refuse (one hashed file, one
|
|
1300
|
+
meaning); F10 glob-looking scope entries refuse at load; F11 context
|
|
1301
|
+
strings cap at 4096 chars; F9 the schema drift guard now catalogs every
|
|
1302
|
+
known kernel-vs-schema divergence honestly.
|
|
1303
|
+
- Verify-pass round (N1–N4): the shadow-scan budget now charges every pair
|
|
1304
|
+
iteration including pre-filtered ones (the guard's own bypass was the
|
|
1305
|
+
last hole — distinct-agent mega-policies spun unbounded); ALL
|
|
1306
|
+
non-canonical scope entries refuse at load, not only globs; the log gate
|
|
1307
|
+
binds `approversMin` to `require_approval` exactly as decide() emits;
|
|
1308
|
+
the signer name obeys the 4096-char cap. Policies beyond ~20k rules must
|
|
1309
|
+
be split (bounded-analysis cost, documented).
|
|
1310
|
+
|
|
1311
|
+
### Fixed (stranger-test findings, 2026-10-15)
|
|
1312
|
+
|
|
1313
|
+
- README failure-path sample now shows the real recovery sequence: settle the
|
|
1314
|
+
open entry with a human-decided figure, then `clearReconciliation` — the
|
|
1315
|
+
previous snippet refused as written (`RECONCILIATION_ENTRIES_OPEN`).
|
|
1316
|
+
- Eval runner: execution modes from a ZIP/tarball download (no `.git`) now
|
|
1317
|
+
refuse once with `HARNESS_GIT_REQUIRED` (exit 2) instead of eight scattered
|
|
1318
|
+
crashes; `--validate` / `--dry-run` keep working without `.git`.
|
|
1319
|
+
- `sakk reconcile` on a ledger with no halt says `nothing to clear` instead
|
|
1320
|
+
of claiming a clear that never happened.
|
|
1321
|
+
- `sakk status` human output formats money to ≤ 6 decimals (the `1e-6`
|
|
1322
|
+
reserve guard no longer prints a float tail); `--json` keeps raw numbers.
|
|
1323
|
+
- Eval runner main-module detection now compares realpaths: invoked through a
|
|
1324
|
+
symlinked path (macOS `/var` → `/private/var`) the CLI previously became a
|
|
1325
|
+
silent exit-0 no-op — a false green. Regression proves a refusal fires.
|
|
1326
|
+
|
|
1327
|
+
### Added (release line)
|
|
1328
|
+
|
|
1329
|
+
- docs/AUDITOR_REPLAY.md: the one-page auditor manual — gold-set integrity,
|
|
1330
|
+
deterministic verdict reproduction, judging the judge, and the honest
|
|
1331
|
+
boundary of what is not yet replayable.
|
|
1332
|
+
|
|
1333
|
+
### Fixed (second-stranger docs pass, 2026-10-15)
|
|
1334
|
+
|
|
1335
|
+
A context-free AI reader walked the five public docs and audited them
|
|
1336
|
+
against the running code:
|
|
1337
|
+
|
|
1338
|
+
- AUDITOR_REPLAY over-claimed "byte-identical" reproduction — run-local
|
|
1339
|
+
fields (runId, durations) differ by construction. It now states precisely
|
|
1340
|
+
what reproduces (judgements and scoring metrics) and ships a **verified**
|
|
1341
|
+
canonical comparison command; defines "guardian test"; marks which
|
|
1342
|
+
evidence files are per-case conditional; aligns the ZIP wording.
|
|
1343
|
+
- README: Install section; `reserve()` throws-on-refusal shown in the
|
|
1344
|
+
sample; crash-between-reserve-and-settle recovery (late settlement by
|
|
1345
|
+
callId) and `estimatedMaxUsd` guidance documented; roadmap ship-state
|
|
1346
|
+
made consistent (eval harness ✅ in-repo, hash-chain/enterprise =
|
|
1347
|
+
roadmap); the two-process race test named.
|
|
1348
|
+
- CONTRIBUTING: quality pipeline rewritten without internal jargon (plain
|
|
1349
|
+
scope tiers, each gate explained); `npm run fuzz` added to the commands.
|
|
1350
|
+
- ARCHITECTURE: enterprise seam column explicitly marked forward-looking.
|
|
1351
|
+
- eval/README: `maintainer-label.json` (reviewer label) and the `PACKET.md` task
|
|
1352
|
+
brief defined at first use.
|
|
1353
|
+
|
|
1354
|
+
### Added
|
|
1355
|
+
|
|
1356
|
+
- Gold eval harness: 12 cases, deterministic scorer, fixture/gold isolation
|
|
1357
|
+
with a SHA-256 manifest, reviewer label gate, fail-closed real-run
|
|
1358
|
+
authorization chain, recorder-tested real adapter.
|
|
1359
|
+
- Mutation drill (`npm run drill`): four named mutants, each killed by its
|
|
1360
|
+
named guardian test, with a pristine control run.
|
|
1361
|
+
- CLI: `sakk init / status / reconcile` (`npx sakk …`), JSON output, exit 3
|
|
1362
|
+
while reconciliation is required — CI-gateable.
|
|
1363
|
+
- Community files: contributing guide, security policy, code of conduct,
|
|
1364
|
+
issue/PR templates, architecture notes.
|
|
1365
|
+
|
|
1366
|
+
### Added (v3 quality pipeline)
|
|
1367
|
+
|
|
1368
|
+
- CONSTITUTION.md, AGENTS.md (+ eval/AGENTS.md), docs/RULES.md (R-01…R-13
|
|
1369
|
+
with sources and the first attribution round), specs/ convention,
|
|
1370
|
+
session stop-gate, `npm run fuzz` ledger invariant fuzzing, and an
|
|
1371
|
+
automated per-PR adversarial review workflow (depth scales with the diff).
|
|
1372
|
+
|
|
1373
|
+
### Fixed
|
|
1374
|
+
|
|
1375
|
+
- Adversarial-review round 2 (the fix batch reviewed author-blind before
|
|
1376
|
+
merge): lock takeover rebuilt on a breaker mutex (judgement+removal in one
|
|
1377
|
+
critical section; crashed breakers fail closed to `sakk unlock`); release
|
|
1378
|
+
authority is a process-local registry; real-mode machine logs relocated
|
|
1379
|
+
out of the evidence store and the gate module copied local (log
|
|
1380
|
+
forgeability by the same-uid adversary is now a DOCUMENTED trade-off,
|
|
1381
|
+
cross-checked by runner verification); fuzz oracle corrected; work-root
|
|
1382
|
+
gate moved first with a real refusal test; --out guard catches crashed
|
|
1383
|
+
runs; CI review workflow is static and base-context only (never executes
|
|
1384
|
+
PR code with a secret).
|
|
1385
|
+
- Review batch D/E/F (16 findings, each with an attack-shaped regression
|
|
1386
|
+
test): atomic single-winner stale-lock takeover + release ownership check;
|
|
1387
|
+
runner-owned evidence paths no longer transit to the real-mode child;
|
|
1388
|
+
real-mode test evidence now reads the machine test log (honest real passes
|
|
1389
|
+
no longer structurally fail); non-object JSON stdout is a structured parse
|
|
1390
|
+
failure; settle overrun records + alerts + halts; flags is schema-required
|
|
1391
|
+
with a dual-source halt and entry-resolving named clearance; retry limits
|
|
1392
|
+
count tool machine logs; canary scan covers all run artifacts; strict case
|
|
1393
|
+
selector (empty/unknown refuse); corrupt log heads fail closed; --out reuse
|
|
1394
|
+
refuses; flag parsing rejects flag-shaped values; tmp-boundary separator.
|
|
1395
|
+
- Durability: ledger writes fsync before the atomic rename (and best-effort
|
|
1396
|
+
directory fsync) — the gate's decision survives power loss.
|
|
1397
|
+
- Cap comparison carries a conservative epsilon so float drift can never
|
|
1398
|
+
fill the cap exactly.
|
|
1399
|
+
- Stale locks from crashed holders on the same host are detected via owner
|
|
1400
|
+
metadata and broken automatically; `forceUnlock`/`sakk unlock --by <name>`
|
|
1401
|
+
cover what automation refuses to judge.
|
|
1402
|
+
- Re-settling a callId with a different amount (or a late unknown-cost flag)
|
|
1403
|
+
is refused with `SETTLE_MISMATCH` instead of being silently ignored.
|
|
1404
|
+
- The real-run authorization gate requires `SAKK_BUDGET_LEDGER_PATH`
|
|
1405
|
+
explicitly and never falls back to `SAKK_LEDGER_PATH`.
|
|
1406
|
+
- CI toolchain pinned (prettier exact version, actions by commit SHA);
|
|
1407
|
+
platform support declared (macOS/Linux).
|
|
1408
|
+
|
|
1409
|
+
### Added — the ledger, the first cut (2026-10-13)
|
|
1410
|
+
|
|
1411
|
+
- Fail-closed spend ledger (`src/ledger.mjs`): `initLedger`, `reserve`,
|
|
1412
|
+
`settle`, `markReconciliationRequired`, `clearReconciliation`, `readLedger`,
|
|
1413
|
+
`totalsOf`, `LedgerError` — reserve-before-spend, hard caps where reaching
|
|
1414
|
+
the cap refuses, duplicate `callId` refusal, reconciliation-required state
|
|
1415
|
+
that halts all paid calls, atomic lock + temp-rename writes, alert JSONL.
|
|
1416
|
+
- 23-test ledger suite including a real two-process race.
|
|
1417
|
+
|
|
1418
|
+
[Unreleased]: https://github.com/sakk-dev/sakk/compare/v0.4.0...HEAD
|
|
1419
|
+
[0.4.0]: https://github.com/sakk-dev/sakk/compare/v0.3.0...v0.4.0
|
|
1420
|
+
[0.3.0]: https://github.com/sakk-dev/sakk/compare/v0.1.0...v0.3.0
|
|
1421
|
+
[0.1.0]: https://github.com/sakk-dev/sakk/releases/tag/v0.1.0
|