Repository navigation
feat(guard): rule-scoped approvals, per-project builtin modes, non-web logins (2.118.0) - #156
Merged
Merged
Conversation
…b logins (2.118.0) Three gaps in the human-control layer of the guard system. Every approval covered ONE file. `mode='session'` existed end to end but was still bound to one (session, layer, rule, tool, op, path_key) tuple, and no surface ever sent it. New `scope="rule"` grants relax exactly two of the nine conditions — tool, to a declared op-class allowlist; path, to the set the rule glob describes, evaluated by the same compiler against the same (canon, rel) pair that produced the denial. Unlimited uses, their own TTL ceiling, and an idle window, because there is no session-END signal to hang a long grant on. find/consume refuse forbidden_target on every call, so a `**/.c3/**` rule grant covers .c3/notes.txt and never override_grants.json. Builtin guards were adjustable globally only, so one repo needing .env meant turning it off machine-wide. The reason given was "a project scope may only tighten" — but that threat is answered by the second key, not the scope: the config half has never been able to change a builtin. Both keys are now realm-bound (builtin_mode|proj|<path>|<glob>), so a cloned repo carries the config and changes nothing. The `login` credential kind was website-only. `canonical_target` now takes a 19-scheme allowlist (cleartext refused by name) plus private_key/passphrase. `canonical_origin` still reads back but ONLY for an https target, so a browser broker fails closed on an ssh:// entry. Also fixed: the request store could lose a row (unsynchronised load-mutate-save over one machine-global file); rule_covers matched only the canonical path, so project-relative globs matched nothing; `c3 creds set --type login` echoed the password to the terminal via plain input() since v2.90.0; and the session benchmark measured symbol narrowing against whichever file was largest, which is not necessarily one with symbols. Specs amended in the same commit: access-guard §1, confirm-guard §5/§7, override-requests §3.1 and new §4.1. Claude-Session: https://claude.ai/code/session_01JjAQC4Uv3aBnv9oQLPMBMy
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.
Three gaps in the human-control layer of the guard system. Independent changes, one release.
1. One approval covered one file
mode='session'already existed end to end but was still bound to one exact(session, layer, rule, tool, op, path_key)tuple, capped at 50 uses and 15 minutes —and no surface ever sent it: the hub hardcoded "Approve once",
c3 override approvehad no flag.
New grant
scope="rule"(docs/override-requests.md §4.1). Relaxes exactly two of §4'snine conditions:
TOOL_CLASSES). An unclassed tool is neverwidened;
c3_artifactsandc3_projectare deliberately unclassed.same
(canon, rel)pair that produced the denial. A rule grant covers exactly thefiles that rule would have blocked.
Replacing what it drops: unlimited uses, its own TTL ceiling (4h default / 8h hard — a
callgrant keeps its 15 minutes), and an idle window. The idle window is load-bearing:there is no session-END signal to hang a long grant on, so it dies once the conversation
stops using it.
What it still cannot reach.
find/consumerefuseforbidden_target()on everycall, not just at mint. Verified live through the hook: with a
**/.c3/**rule grantactive,
.c3/notes.txt→ allow, andoverride_grants.json/config.json/overrides.jsonl/cred_state.json/secrets.enc→ deny, all five.Minting needs
override.allow_rule_grants(default off, AND-merged, counted as widening),a globbable rule, a rule that covers the refusal, and the glob retyped on every layer —
including
access_confirm, which stays one-tap foronce.Visible and killable:
GET/DELETE /api/hub/grants, an Active grants strip in theAccess tab, and a context line that says "covers every path <rule> matches, this
session" rather than a use count.
2. Builtin guards were adjustable globally only
One repo needing
.envmeant turning it off for every project on the machine. The statedreason was "a project scope may only ever tighten" — but that threat is answered by the
second key, not the scope: the config half has never been able to change a builtin.
Both keys are now realm-bound —
builtin_mode|proj|<normcased path>|<glob>, the samerealm-atomic construction as
credential_store. A project mode replaces the global one forthat glob, loosening included; a cloned repo carries the config and changes nothing.
Verified on this repo: identical config in a sibling project → still denied. Deleting the
keyring attestation by hand, config untouched → immediately re-enforced.
Tier-0 and
FORBIDDEN_TARGET_NAMESare unchanged at every scope, sobuiltin_mode: {"**/.c3/**": "allow"}can never become self-granting.Compatibility: a pre-2.118.0 C3 reading a project config with either key treats that
scope as corrupt ⇒ deny-all. Fail-closed, and observed live during verification.
3.
logincredentials were website-onlycanonical_targettakes a 19-scheme allowlist (https, ssh, sftp, rdp, smb, winrm, ldaps,postgres, mysql, mongodb, redis, …); cleartext schemes are refused by name and told
which TLS variant to use. Adds
private_key(PEM-checked, own size allowance) andpassphrase; one of password/private_key is required.The origin-pinning property is preserved, not weakened.
canonical_originstill readsback — but only when the target is https — so a browser broker gets
Nonefor anssh://entry and fails closed. Pre-existing entries need no migration.Fixes found along the way
~/.c3/oracle/override_requests.jsonis one filefor every project and session on the box; every mutator did an unsynchronised
load→mutate→save, and the tmp name was a fixed
.tmp. Now a single-row commit under thegrants store's lock, per-process tmp.
rule_coversmatched only the canonical path, so project-relative globs likesecrets/**matched nothing. Caught by its own test before shipping.c3 creds set --type loginechoed the password to the terminal via plaininput()since v2.90.0. Both browser UIs masked it; nothing asserted the CLI. The parity test now
derives the must-mask set from
_SCHEMAS._select_sampleorders by size alone, sobug_investigationtook whatever was largest —here, this release's own CHANGELOG. Reported −15.2%, a correct measurement of a workload
nobody runs. Now picks the first sampled file that exposes symbols: −15.2% → +77.9%,
symbol read 29,438 → 34 tokens.
Verification
ruffclean · 3655 passed, 6 skipped, 0 failed.Live probes for all three (real hook, real hub HTTP routes, real keyring, real vault):
rule grant spans a sibling file and no other session; revoke and idle expiry both end it;
project builtin mode allows here and not in a sibling with identical config; an
ssh://login stores to the Fernet sidecar, refuses reveal, and yields
Noneforcanonical_origin. All probe state restored.Specs amended in the same commit: access-guard §1, confirm-guard §5/§7, override-requests
§3.1 and new §4.1.
https://claude.ai/code/session_01JjAQC4Uv3aBnv9oQLPMBMy