release: v2.66.0 — tool discipline is a knob you can turn - #60
Merged
Conversation
C3 had four independent gates, and only three were adjustable. The fourth — the PreToolUse hook that hard-denies native Edit/Write unless a c3_* call ran first — was hardcoded, read no config, and was registered regardless of the permission tier. So selecting `permissive`, documented as "all tools and shell commands pre-approved", still had every native Edit refused by the hook. The one knob that looked like it should help did not reach the layer doing the blocking. Added - `c3 enforce [strict|advisory|off]` plus an `enforcement` config section. Deliberately separate from `c3 access`: path policy is a security boundary, tool discipline is a workflow preference, and that split is what makes it safe to loosen the second without touching the first. At every mode — including `off` — Access Guard rules, the credential-vault write guard and agent locks still enforce. The vault guard keys off a fixed tool set so neither a mode nor a `blocked_tools` override can reach it. - Discipline tab in both UIs: per-project in `c3 ui`, cross-project in the Hub. - `c3 access stats` — the denial logging docs/access-guard.md §3 specified but never shipped. Ranks denials by layer and names the command that clears each. - Permission tiers now derive a discipline mode (standard->advisory, permissive->off, c3-strict/read-only->strict). An explicit `c3 enforce` choice records set_by=user and a later tier change defers to it. - `c3 init`: Step 5/5 prompt, plus `--enforcement` for scripted installs. Fixed - `_atomic_write_json` wrote enforcement state without fsync and abandoned its temp file when os.replace raised. Observed here: a truncated enforcement_state.json, 10 orphaned .c3/*.tmp<pid> files, 58 hook errors over two days. The failure is self-worsening — corrupt state loads empty, dropping every sticky unlock, which makes enforcement feel more aggressive. Now fsyncs before publishing, retries os.replace on Windows sharing violations, and always cleans up the temp file. Upgrade safety: no `enforcement` section resolves to `strict`, and nothing is derived at read time, so upgrading cannot change how an existing project behaves. Everything fails closed — unknown mode, malformed section or unparseable JSON all resolve to strict with a visible warning. 61 new tests; 1816 passing. Claude-Session: https://claude.ai/code/session_01NLRxC5agGV92NFfaEQFz6F
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
C3 had four independent gates and only three were adjustable:
c3 permissions)c3 access)Edit/Writevsc3_editlocksconfig)cli/hook_pretool_enforce.pyhardcoded_BLOCKED_TOOLS, a 10-minute signalTTL and the lookback window, read no config and no env var, and
install-mcpregistered it regardless of tier. So a user selecting
permissive—documented as "all tools and shell commands pre-approved" — still had every
native
Edithard-denied. The knob that looked like it should help didn'treach the layer doing the blocking.
Two things made it worse, neither of them policy:
docs/access-guard.md§3 specified "coalesced per(rule, tool, session) with a hit counter". Never implemented, so friction was
unmeasurable.
The design
Layer C is a workflow preference; Layer B is a security boundary. They were
conflated. Splitting them is what makes it safe to loosen the first.
c3 enforce [strict|advisory|off], plus anenforcementsection resolvedproject → global →
strict.At every mode including
off, these still enforce: Access Guard pathrules, the credential-vault write guard, agent locks. The vault guard keys off
a fixed tool set rather than the configurable
blocked_tools, so neither amode nor a config override can open a native write path to the vault.
Asserted per-mode in
TestSecurityBoundariesSurviveEveryMode.The edit ledger is captured PostToolUse and is unaffected by the mode. What
strictbuys overadvisoryisc3_edit's pre-edit snapshot — that's thewhole trade-off, and both UIs say so before you switch.
Also added
c3 ui) and cross-project(Hub). Mode picker, provenance, what stays enforced, and the ranked denial
table with the fix per row. In the Hub, unreadable projects are listed under
"Not reporting" rather than shown as
strict— "we don't know" and "runningstrict" are different claims.
c3 access stats— the missing §3 telemetry, covering both layers andnaming the right lever per row (
c3 enforce advisoryvsc3 access removevs
c3 access builtin disable).standard→advisory,permissive→off,c3-strict/read-only→strict. An explicitc3 enforcerecordsset_by: userand a later tier change defers to it.c3 init— Step 5/5 prompt, and--enforcementfor scripted installs.Fixed
_atomic_write_jsonwrote enforcement state withoutfsyncand abandoned itstemp file when
os.replaceraised. Observed on Windows: a truncatedenforcement_state.json, 10 orphaned.c3/*.tmp<pid>files, 58 hook errorsover two days. The failure is quietly self-worsening — corrupt state loads
empty, dropping every sticky unlock, making enforcement more aggressive,
which reads as "the guard got worse". Now fsyncs before publishing, retries
os.replaceon Windows sharing violations, and always removes the temp file.c3 initsweeps orphans whose owning PID is gone, never one belonging to alive process.
Upgrade safety
No
enforcementsection resolves tostrict, and nothing is derived at readtime — upgrading cannot change how an existing project behaves. Verified
against the live registry: all 47 registered projects report
mode=strict, scope=default.Everything fails closed: unknown mode, malformed section, unparseable JSON, or
a
blocked_toolsentry naming an ungoverned tool all resolve tostrictwitha visible
[c3:enforcement-config]warning.Verification
whole bundle with green Python tests. Both concatenated bundles (30-file Hub,
20-file project UI) were compiled through
@babel/standaloneand checked forduplicate top-level declarations.
twine checkpasses on sdist + wheel; confirmed the new JS and service filesland in the wheel (
ui/components/*.js,hub_ui/components/*.jsglobs).Docs:
docs/enforcement.md.docs/access-guard.mdgets an implementation noteunder §3 — the frozen spec itself is unchanged.
https://claude.ai/code/session_01NLRxC5agGV92NFfaEQFz6F