code-context-control 2.76.2__py3-none-any.whl → 2.77.0__py3-none-any.whl
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.
- cli/c3.py +1 -1
- cli/hook_access_guard.py +103 -37
- {code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/METADATA +1 -1
- {code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/RECORD +8 -8
- {code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
cli/hook_access_guard.py
CHANGED
|
@@ -10,6 +10,7 @@ Bash coverage is a best-effort, existence-gated token scan — advisory by
|
|
|
10
10
|
design (docs/access-guard.md §3/§6); it catches an agent naming a denied
|
|
11
11
|
path, not an adversary hiding one.
|
|
12
12
|
"""
|
|
13
|
+
import os
|
|
13
14
|
import re
|
|
14
15
|
import sys
|
|
15
16
|
from pathlib import Path
|
|
@@ -84,6 +85,23 @@ _DOUBLE_COLON_RE = re.compile(r"::(?!\$)")
|
|
|
84
85
|
_GIT_REVSPEC_RE = re.compile(r"^(?P<rev>[^:\s]+):(?P<path>[^:\s]+)$")
|
|
85
86
|
_GIT_CMD_RE = re.compile(r"^\s*(?:[\w.\-]*[/\\])?git(?:\.exe)?\b", re.IGNORECASE)
|
|
86
87
|
|
|
88
|
+
# Where one command ends and the next begins. Anchoring "is this git?" to the
|
|
89
|
+
# start of the whole string was wrong in the most ordinary way possible:
|
|
90
|
+
# `cd /repo && git show origin/main:pyproject.toml` starts with `cd`, so the
|
|
91
|
+
# revspec rewrite never fired for the shape people actually type. Caught by a
|
|
92
|
+
# live probe after release, not by the unit tests — which passed the command as
|
|
93
|
+
# `git show …` because that is how the test author writes it, not how a shell
|
|
94
|
+
# call arrives.
|
|
95
|
+
_SEGMENT_SPLIT = re.compile(r"&&|\|\||[;|\n]")
|
|
96
|
+
|
|
97
|
+
# `cd <dir>` — the segment that moves the ground under every later one. Handles
|
|
98
|
+
# a quoted target, since project paths on this platform contain spaces, and the
|
|
99
|
+
# Windows `/d` flag.
|
|
100
|
+
_CD_RE = re.compile(
|
|
101
|
+
r"""^\s*cd\s+(?:/d\s+)?(?P<dir>"[^"]+"|'[^']+'|\S+)\s*$""",
|
|
102
|
+
re.IGNORECASE,
|
|
103
|
+
)
|
|
104
|
+
|
|
87
105
|
|
|
88
106
|
def _is_network_token(tok: str) -> bool:
|
|
89
107
|
"""True for URLs and IPv6 literals/CIDRs — never for an ADS spelling."""
|
|
@@ -190,6 +208,20 @@ def _target(tool_input: dict) -> str:
|
|
|
190
208
|
)
|
|
191
209
|
|
|
192
210
|
|
|
211
|
+
def _cd_target(segment: str, cwd: str) -> str | None:
|
|
212
|
+
"""The directory a `cd` segment moves to, resolved against ``cwd``."""
|
|
213
|
+
m = _CD_RE.match(segment or "")
|
|
214
|
+
if not m:
|
|
215
|
+
return None
|
|
216
|
+
target = m.group("dir").strip("\"'")
|
|
217
|
+
if not target or target.startswith("-"):
|
|
218
|
+
return None
|
|
219
|
+
try:
|
|
220
|
+
return os.path.abspath(os.path.join(cwd, os.path.expanduser(target)))
|
|
221
|
+
except (OSError, ValueError):
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
193
225
|
def _scan_shell(cmd: str, base: str):
|
|
194
226
|
"""(denial, token) for the first confident deny-rule hit, else (None, '').
|
|
195
227
|
|
|
@@ -201,44 +233,78 @@ def _scan_shell(cmd: str, base: str):
|
|
|
201
233
|
outright, as are URLs and IPv6 literals: all of them trip the ADS spelling
|
|
202
234
|
check, which is exempt from existence-gating, so a token naming nothing on
|
|
203
235
|
disk would otherwise hard-deny (#50).
|
|
236
|
+
|
|
237
|
+
Scanning is per **command segment**, not per whole string. A segment is what
|
|
238
|
+
sits between `&&`, `||`, `;`, `|` or a newline, and each one answers "am I a
|
|
239
|
+
git command?" for itself — so the revspec rewrite applies to the tokens of
|
|
240
|
+
the git segment and to no others. `cat notes.txt:hidden && git status` must
|
|
241
|
+
not have its first token reinterpreted just because a later segment is git.
|
|
204
242
|
"""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
tok
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
budget = _MAX_TOKENS
|
|
244
|
+
cwd = base
|
|
245
|
+
for segment in _SEGMENT_SPLIT.split(cmd or ""):
|
|
246
|
+
if budget <= 0:
|
|
247
|
+
break
|
|
248
|
+
is_git = bool(_GIT_CMD_RE.match(segment))
|
|
249
|
+
for raw in _TOKEN_SPLIT.split(segment)[:budget]:
|
|
250
|
+
budget -= 1
|
|
251
|
+
tok = raw.strip("\"'`;,()")
|
|
252
|
+
if not tok or tok.startswith("-"):
|
|
253
|
+
continue
|
|
254
|
+
# A revspec's path half need not contain a separator — `HEAD:.env`
|
|
255
|
+
# has none, so it used to be dropped here before any rule saw it.
|
|
256
|
+
if ("/" not in tok and "\\" not in tok and not tok.startswith(".")
|
|
257
|
+
and not (is_git and _GIT_REVSPEC_RE.match(tok))):
|
|
258
|
+
continue
|
|
259
|
+
# Order matters only for readability: a URL is also syntax-free, so
|
|
260
|
+
# either check alone would skip 'https://x'. Both are kept because
|
|
261
|
+
# they answer different questions — "is this a network literal" and
|
|
262
|
+
# "is this a path at all" — and the second is the one that
|
|
263
|
+
# generalizes.
|
|
264
|
+
if not _looks_like_a_path(tok) or _is_network_token(tok):
|
|
265
|
+
continue
|
|
266
|
+
# `tests/x.py::TestThing` is a node id, not a stream spelling.
|
|
267
|
+
if _is_scope_token(tok):
|
|
268
|
+
continue
|
|
269
|
+
# `git show <rev>:<path>` — judge the PATH, not the whole token.
|
|
270
|
+
# This is the one place the scan rewrites what it checks rather than
|
|
271
|
+
# skipping it, because skipping would let `git show HEAD:.env`
|
|
272
|
+
# through.
|
|
273
|
+
revspec_path = _git_revspec_path(segment, tok) if is_git else None
|
|
274
|
+
from_revspec = revspec_path is not None
|
|
275
|
+
if from_revspec:
|
|
276
|
+
tok = revspec_path
|
|
277
|
+
if re.match(r"^/[a-z]/", tok): # MSYS /c/foo → C:/foo
|
|
278
|
+
tok = f"{tok[1]}:{tok[2:]}"
|
|
279
|
+
# Resolve against the cwd the command will actually run in, not the
|
|
280
|
+
# session root. `cd /elsewhere && cat .env` used to compute the
|
|
281
|
+
# denial correctly and then throw it away, because the existence
|
|
282
|
+
# gate looked for `.env` under the project root where it does not
|
|
283
|
+
# live (#82). The rule base stays `base` — policy is the project's;
|
|
284
|
+
# only the path being judged follows the shell.
|
|
285
|
+
probe = tok
|
|
286
|
+
if not os.path.isabs(probe):
|
|
287
|
+
try:
|
|
288
|
+
probe = os.path.join(cwd, probe)
|
|
289
|
+
except (OSError, ValueError):
|
|
290
|
+
probe = tok
|
|
291
|
+
denial = ag.check(probe, "read", base)
|
|
292
|
+
if denial and denial.kind == "deny":
|
|
293
|
+
try:
|
|
294
|
+
exists = Path(probe).exists()
|
|
295
|
+
except OSError:
|
|
296
|
+
exists = False
|
|
297
|
+
# A revspec names a path in HISTORY, so the working tree is the
|
|
298
|
+
# wrong place to ask whether it is real. `git show HEAD~5:.env`
|
|
299
|
+
# reads a denied file whether or not that file exists today, and
|
|
300
|
+
# existence-gating it would be a hole this rewrite opened.
|
|
301
|
+
if exists or from_revspec or denial.rule.startswith("<"):
|
|
302
|
+
return denial, tok
|
|
303
|
+
# AFTER the segment's own tokens: `cd x` moves the ground for what
|
|
304
|
+
# follows, not for its own argument.
|
|
305
|
+
moved = _cd_target(segment, cwd)
|
|
306
|
+
if moved is not None:
|
|
307
|
+
cwd = moved
|
|
242
308
|
return None, ""
|
|
243
309
|
|
|
244
310
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.77.0
|
|
4
4
|
Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=03fDyLvfzF832M7L5B2DgR8KMWXIObpbYxSC3LbhQ04,17229
|
|
3
|
-
cli/c3.py,sha256=
|
|
3
|
+
cli/c3.py,sha256=vepPfESCu5S1EizKddSybSgWYC0zOrQ7r73dVLB2xNQ,368519
|
|
4
4
|
cli/docs.html,sha256=bNymSz7LatnWHjxSXL92QSUtGvB3jBzNHbOV-bRpAOo,142507
|
|
5
5
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
6
|
-
cli/hook_access_guard.py,sha256=
|
|
6
|
+
cli/hook_access_guard.py,sha256=UGXZ6-3gVNxjVasAuCV9eUOBwqDHl3YnuDc3XVTpvL0,15942
|
|
7
7
|
cli/hook_artifact.py,sha256=Se1CNBfoBFyvJQlRmYdNtdRXIkQp9zaF0O6ndRMo7ts,2198
|
|
8
8
|
cli/hook_auto_snapshot.py,sha256=yGFqRANtw8qNqRNr-oCgpuyaUyTkSzIOFxXt5kauq4I,5271
|
|
9
9
|
cli/hook_c3_signal.py,sha256=FPvs-9nD9ZiKA8r2tmusgj79yT9w9F173cRDKn-PYeI,2149
|
|
@@ -112,7 +112,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
112
112
|
cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
|
|
113
113
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
114
114
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
115
|
-
code_context_control-2.
|
|
115
|
+
code_context_control-2.77.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
116
116
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
117
117
|
core/config.py,sha256=YmkcZwedz_lfDM0ZuI_f4xpe98ixPLcSQFcTCHgRiRg,19206
|
|
118
118
|
core/ide.py,sha256=V6VVMVsFdmmcsMyxikjQp7z9xa42CWiHKS-ya-MAcG4,6172
|
|
@@ -265,8 +265,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
265
265
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
266
266
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
267
267
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
268
|
-
code_context_control-2.
|
|
269
|
-
code_context_control-2.
|
|
270
|
-
code_context_control-2.
|
|
271
|
-
code_context_control-2.
|
|
272
|
-
code_context_control-2.
|
|
268
|
+
code_context_control-2.77.0.dist-info/METADATA,sha256=XFIYGme_wszePZIjWKSWokSb3m4RqUwDXfF4k3Rqwk8,25648
|
|
269
|
+
code_context_control-2.77.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
270
|
+
code_context_control-2.77.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
271
|
+
code_context_control-2.77.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
272
|
+
code_context_control-2.77.0.dist-info/RECORD,,
|
|
File without changes
|
{code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.76.2.dist-info → code_context_control-2.77.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|