code-context-control 2.61.0__py3-none-any.whl → 2.61.2__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_pretool_enforce.py +22 -0
- cli/hub_ui/components/project_card.js +57 -9
- cli/tools/credentials.py +8 -0
- cli/tools/edit.py +8 -0
- {code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/METADATA +1 -1
- {code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/RECORD +12 -12
- services/credential_store.py +58 -0
- {code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/WHEEL +0 -0
- {code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
cli/hook_pretool_enforce.py
CHANGED
|
@@ -78,6 +78,11 @@ _PREREQS = {
|
|
|
78
78
|
_ADVISORY_TOOLS = {"Read", "Grep", "Glob", "FindFiles", "SearchText"}
|
|
79
79
|
_BLOCKED_TOOLS = {"Edit", "Write", "MultiEdit"}
|
|
80
80
|
|
|
81
|
+
# Vault files no native write may touch, regardless of unlock state.
|
|
82
|
+
# Mirrors services.credential_store.VAULT_PROTECTED_FILES (parity-tested);
|
|
83
|
+
# duplicated because hooks must stay import-light.
|
|
84
|
+
_VAULT_FILES = frozenset({"config.json", "secrets.enc", "cred_state.json"})
|
|
85
|
+
|
|
81
86
|
# Redirect messages per native tool
|
|
82
87
|
_REDIRECTS = {
|
|
83
88
|
"Read": (
|
|
@@ -283,6 +288,23 @@ def run(payload: dict, project_path: Path | None = None) -> dict | None:
|
|
|
283
288
|
session_id = str(payload.get("session_id") or "")
|
|
284
289
|
base = project_path if project_path is not None else Path.cwd()
|
|
285
290
|
|
|
291
|
+
# Vault write-guard: the credential registry/state may never be modified
|
|
292
|
+
# by the agent — even with a warm c3 signal or sticky unlock (v2.61.2).
|
|
293
|
+
if tool_name in _BLOCKED_TOOLS:
|
|
294
|
+
target = Path(str(tool_input.get("file_path") or ""))
|
|
295
|
+
if target.name.lower() in _VAULT_FILES and target.parent.name.lower() == ".c3":
|
|
296
|
+
return {
|
|
297
|
+
"hookSpecificOutput": {
|
|
298
|
+
"hookEventName": "PreToolUse",
|
|
299
|
+
"permissionDecision": "deny",
|
|
300
|
+
"permissionDecisionReason": (
|
|
301
|
+
f"[c3:vault-protected] {target.name} belongs to the "
|
|
302
|
+
"credential vault and cannot be modified by the agent. "
|
|
303
|
+
"Ask the user to use the Credentials UI or `c3 creds` CLI."
|
|
304
|
+
),
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
286
308
|
# Session-scoped load: state written by a different session comes back
|
|
287
309
|
# empty, so stale unlocks degrade to the advisory path below.
|
|
288
310
|
state = load_enforcement_state(base, session_id=session_id)
|
|
@@ -377,24 +377,63 @@ function ProjectCard({ p, isChild, rollup, expanded, onToggleExpand, onChanged,
|
|
|
377
377
|
// Launch the UI server, then open its tab once the port shows up in the
|
|
378
378
|
// registry. launch_session cannot return the port (the detached child
|
|
379
379
|
// picks it), so open a placeholder tab synchronously — still inside the
|
|
380
|
-
// click gesture, so popup blockers allow it
|
|
381
|
-
//
|
|
380
|
+
// click gesture, so popup blockers allow it. The placeholder carries its
|
|
381
|
+
// own poll/redirect script (about:blank inherits this tab's origin, so
|
|
382
|
+
// its fetches are same-origin) and resolves even if this hub tab is
|
|
383
|
+
// closed, reloaded, or hung mid-launch — it must never depend on the
|
|
384
|
+
// opener surviving the launch window.
|
|
382
385
|
const start = async (e) => {
|
|
383
386
|
e.stopPropagation();
|
|
384
387
|
if (starting) return;
|
|
385
388
|
setStarting(true);
|
|
386
389
|
const win = window.open('', '_blank');
|
|
390
|
+
// armed = the placeholder polls and navigates itself; when false
|
|
391
|
+
// (write failed / popup blocked) the hub-side poll navigates instead.
|
|
392
|
+
let armed = false;
|
|
387
393
|
if (win) {
|
|
388
394
|
try {
|
|
395
|
+
const escHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<');
|
|
396
|
+
const jsStr = (s) => JSON.stringify(String(s)).replace(/</g, '\\u003c');
|
|
389
397
|
win.document.write(
|
|
390
|
-
`<title>Starting ${p.name}…</title>` +
|
|
398
|
+
`<title>Starting ${escHtml(p.name)}…</title>` +
|
|
391
399
|
'<body style="background:#0d1117;color:#8b949e;font-family:sans-serif;' +
|
|
392
|
-
'display:flex;align-items:center;justify-content:center;height:100vh">' +
|
|
393
|
-
|
|
400
|
+
'display:flex;align-items:center;justify-content:center;height:100vh;margin:0">' +
|
|
401
|
+
'<div style="text-align:center;max-width:560px;padding:0 20px">' +
|
|
402
|
+
'<div id="spin" style="margin:0 auto 14px;width:22px;height:22px;' +
|
|
403
|
+
'border:2px solid #21262d;border-top-color:#58a6ff;border-radius:50%;' +
|
|
404
|
+
'animation:s .8s linear infinite"></div>' +
|
|
405
|
+
'<style>@keyframes s{to{transform:rotate(360deg)}}</style>' +
|
|
406
|
+
`<div id="msg" style="color:#c9d1d9">Starting ${escHtml(p.name)}…</div>` +
|
|
407
|
+
'<div id="sub" style="margin-top:6px;font-size:12px">waiting for the UI server to report a port</div>' +
|
|
408
|
+
'</div><script>' +
|
|
409
|
+
`var ORIGIN=${jsStr(location.origin)},TARGET=${jsStr((p.path || '').toLowerCase())},NAME=${jsStr(p.name)},tries=0,dead=0;` +
|
|
410
|
+
'function fail(why){dead=1;document.getElementById("spin").style.display="none";' +
|
|
411
|
+
'document.getElementById("msg").textContent=NAME+": "+why;' +
|
|
412
|
+
'document.getElementById("sub").textContent=' +
|
|
413
|
+
'"Check the project\'s .c3/ui.log — you can close this tab.";}' +
|
|
414
|
+
'window.__c3fail=fail;' +
|
|
415
|
+
'function poll(){if(dead)return;' +
|
|
416
|
+
'fetch(ORIGIN+"/api/projects").then(function(r){return r.json()})' +
|
|
417
|
+
'.then(function(rows){if(dead)return;' +
|
|
418
|
+
'var row=(Array.isArray(rows)?rows:[]).find(function(r){' +
|
|
419
|
+
'return (r.path||"").toLowerCase()===TARGET});' +
|
|
420
|
+
'if(row&&row.port){location.replace("http://127.0.0.1:"+row.port);return}' +
|
|
421
|
+
'if(++tries>=20){fail("UI server did not report a port after 30s");return}' +
|
|
422
|
+
'setTimeout(poll,1500)}).catch(function(){if(dead)return;' +
|
|
423
|
+
'if(++tries>=20){fail("hub unreachable");return}setTimeout(poll,1500)})}' +
|
|
424
|
+
'setTimeout(poll,1200);<\/script>');
|
|
425
|
+
win.document.close();
|
|
426
|
+
armed = true;
|
|
394
427
|
} catch { }
|
|
395
428
|
}
|
|
396
429
|
const fail = (msg) => {
|
|
397
|
-
if (win)
|
|
430
|
+
if (win) {
|
|
431
|
+
// Surface the error inside the tab the user is looking at; close
|
|
432
|
+
// it only when the placeholder script isn't there to show it.
|
|
433
|
+
let shown = false;
|
|
434
|
+
try { if (armed && win.__c3fail) { win.__c3fail(msg); shown = true; } } catch { }
|
|
435
|
+
if (!shown) { try { win.close(); } catch { } }
|
|
436
|
+
}
|
|
398
437
|
notify(msg, 'err');
|
|
399
438
|
setStarting(false);
|
|
400
439
|
};
|
|
@@ -403,19 +442,28 @@ function ProjectCard({ p, isChild, rollup, expanded, onToggleExpand, onChanged,
|
|
|
403
442
|
if (!d.launched) { fail('Launch failed'); return; }
|
|
404
443
|
notify(`Starting ${p.name}…`);
|
|
405
444
|
setTimeout(onChanged, 1500);
|
|
445
|
+
// Bookkeeping poll: refresh the card and clear the busy state. Only
|
|
446
|
+
// navigates or closes the tab when the placeholder isn't armed.
|
|
406
447
|
const poll = async (tries) => {
|
|
407
448
|
let rows = [];
|
|
408
449
|
try { rows = await api.get('/api/projects'); } catch { }
|
|
409
450
|
const row = (Array.isArray(rows) ? rows : []).find(r =>
|
|
410
451
|
(r.path || '').toLowerCase() === (p.path || '').toLowerCase());
|
|
411
452
|
if (row && row.port) {
|
|
412
|
-
|
|
413
|
-
|
|
453
|
+
if (!armed) {
|
|
454
|
+
const url = 'http://127.0.0.1:' + row.port;
|
|
455
|
+
if (win) { win.location = url; } else { window.open(url, '_blank'); }
|
|
456
|
+
}
|
|
414
457
|
onChanged();
|
|
415
458
|
setStarting(false);
|
|
416
459
|
return;
|
|
417
460
|
}
|
|
418
|
-
if (tries >= 20) {
|
|
461
|
+
if (tries >= 20) {
|
|
462
|
+
if (!armed && win) { try { win.close(); } catch { } }
|
|
463
|
+
notify(`${p.name}: UI server did not report a port — check .c3/ui.log`, 'err');
|
|
464
|
+
setStarting(false);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
419
467
|
setTimeout(() => poll(tries + 1), 1500);
|
|
420
468
|
};
|
|
421
469
|
setTimeout(() => poll(0), 1200);
|
cli/tools/credentials.py
CHANGED
|
@@ -130,6 +130,14 @@ def _act_reveal(name: str, svc, project_path: str) -> str:
|
|
|
130
130
|
"or ask the user to enable agent_readable in the Credentials UI / "
|
|
131
131
|
f"`c3 creds set {name} --agent-readable`."
|
|
132
132
|
)
|
|
133
|
+
if not cs.verify_agent_readable(name, scope=entry["scope"], project_path=project_path):
|
|
134
|
+
return (
|
|
135
|
+
f"[creds:integrity] {name!r} is marked agent_readable in the registry, "
|
|
136
|
+
"but the flag's keyring attestation is missing or disagrees — the "
|
|
137
|
+
"registry may have been modified outside the credentials API. Ask the "
|
|
138
|
+
"user to re-confirm the flag (toggle it in the Credentials UI or "
|
|
139
|
+
"`c3 creds`); re-saving it restores the attestation."
|
|
140
|
+
)
|
|
133
141
|
value = cs.get_value(name, project_path=project_path, scope=entry["scope"])
|
|
134
142
|
if value is None:
|
|
135
143
|
return (
|
cli/tools/edit.py
CHANGED
|
@@ -13,6 +13,8 @@ import json
|
|
|
13
13
|
import threading
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
+
from services import credential_store as _cs
|
|
17
|
+
|
|
16
18
|
# Per-file locks — keyed by resolved absolute path string.
|
|
17
19
|
_file_locks: dict[str, threading.Lock] = {}
|
|
18
20
|
_locks_lock = threading.Lock()
|
|
@@ -237,6 +239,12 @@ def handle_edit(file_path: str, old_string: str, new_string: str,
|
|
|
237
239
|
path = Path(svc.project_path) / path
|
|
238
240
|
path = path.resolve()
|
|
239
241
|
|
|
242
|
+
# Vault write-guard: the credential registry/state is never agent-writable
|
|
243
|
+
# (covers create, edit, and batch modes — and c3_project's edit proxy).
|
|
244
|
+
guard = _cs.vault_guard_reason(path)
|
|
245
|
+
if guard:
|
|
246
|
+
return finalize("c3_edit", {"file": file_path}, guard, "vault-protected")
|
|
247
|
+
|
|
240
248
|
# Relative path for ledger + display (computed even for new files)
|
|
241
249
|
try:
|
|
242
250
|
rel = str(path.relative_to(Path(svc.project_path).resolve())).replace("\\", "/")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.61.
|
|
3
|
+
Version: 2.61.2
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Copilot, Cursor, Antigravity). Retrieve less, read less, edit safer — version the configs that shape your agent (CLAUDE.md, skills, hooks, MCP), and manage multi-project + sub-project hierarchies from a local hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=tJ4ZmXD5Y-JjFWb0uoR7ycYklYFUMnnjF2G7BO-wyBI,12600
|
|
3
|
-
cli/c3.py,sha256=
|
|
3
|
+
cli/c3.py,sha256=0m9SqPIVQKy7zbrGq9xOndjmKcu9VOuVFPjJHLuRrlk,322864
|
|
4
4
|
cli/docs.html,sha256=bNymSz7LatnWHjxSXL92QSUtGvB3jBzNHbOV-bRpAOo,142507
|
|
5
5
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
6
6
|
cli/hook_artifact.py,sha256=Se1CNBfoBFyvJQlRmYdNtdRXIkQp9zaF0O6ndRMo7ts,2198
|
|
@@ -12,7 +12,7 @@ cli/hook_edit_ledger.py,sha256=7YK-v5SfW7W5c7F_Ywcwo6UAuvkOR7hZqmWxVokrOAY,8531
|
|
|
12
12
|
cli/hook_edit_unlock.py,sha256=6xrnsbwmD6TuyADuP_Z-QfwM7sUrtOfyLL6R-gAm9iw,6492
|
|
13
13
|
cli/hook_filter.py,sha256=v8ZSznK5StTfYYqae1l4V2bm-e9sy2RFfpbNr5KwE2U,4988
|
|
14
14
|
cli/hook_ghost_files.py,sha256=plbedFgIJ5tXEE_8USUE39RmyODh1UjIUFNRbUo4R8w,10291
|
|
15
|
-
cli/hook_pretool_enforce.py,sha256=
|
|
15
|
+
cli/hook_pretool_enforce.py,sha256=ywHygBUUuSyjGlngq1oLeDZg2Q1_FOk5z2F3UNNeiEU,14386
|
|
16
16
|
cli/hook_prompt_recall.py,sha256=LHyubu36pf5h3hbLBe0yH-yGhAlwLJdsc7hfa7WB1h4,3471
|
|
17
17
|
cli/hook_read.py,sha256=ZWZldgqGSNz78COhlgETQ7yxdOW5vWIIHUet9gZKhxk,8176
|
|
18
18
|
cli/hook_session_stats.py,sha256=hZiX0r1SLOstu1PXvygWae0cIqHCcC3mb0SbFL6nCic,2083
|
|
@@ -52,7 +52,7 @@ cli/hub_ui/components/global_search.js,sha256=GPfq1EsZa73vXBNCVn4VhT3a7gYkaUblbc
|
|
|
52
52
|
cli/hub_ui/components/hub_credentials.js,sha256=fIXwU4vdfj-oD3bfgRxUIp8PhWcLjX42STuf-nn4Mjw,72700
|
|
53
53
|
cli/hub_ui/components/mcp_manager.js,sha256=KxA0LlpyNFDgtGxajoqwViJ0pJ0lsa1PqdCMNyyfhCo,17368
|
|
54
54
|
cli/hub_ui/components/modals.js,sha256=3maxEVYIg4McqM-RiOQyr0wz0U08_plGifxVHw2CShQ,52437
|
|
55
|
-
cli/hub_ui/components/project_card.js,sha256=
|
|
55
|
+
cli/hub_ui/components/project_card.js,sha256=HtGqEtALhe5rR0jlNclXKzCy75eLx0uGVgTkZsrJQ9U,30188
|
|
56
56
|
cli/hub_ui/components/project_tree.js,sha256=8zs2mfj_RsoxY1gDXmqonPPKKQnm1Ev9dizghg4zpHE,3831
|
|
57
57
|
cli/hub_ui/components/session_drawer.js,sha256=OEBvdF3PqRji9CEntolDMVplXF2NTG2xwmXCqkcb0Nw,7553
|
|
58
58
|
cli/hub_ui/components/settings_modal.js,sha256=KF2gQj7HG568N06k3HjTROEURC1wuBvBi05eJ_URLBk,8929
|
|
@@ -67,9 +67,9 @@ cli/tools/agent.py,sha256=Fp9XLrOqjSP7_Ihq7VXjYXcGoXjw17-Hx3JMvoOVnhc,48105
|
|
|
67
67
|
cli/tools/artifacts.py,sha256=98shRIs3cdC8r0E1gfsEOIDUopqSP1WpYwzFiWfdtg0,7491
|
|
68
68
|
cli/tools/bitbucket.py,sha256=_hgORCW9-IF5xZJaznoONVfaTGKaX8VcU7ALrBeLhWo,27518
|
|
69
69
|
cli/tools/compress.py,sha256=aZ4owwqaQLtkszDr_g7yv5KkvtQPuqaSyMk-aM_hsCY,10060
|
|
70
|
-
cli/tools/credentials.py,sha256=
|
|
70
|
+
cli/tools/credentials.py,sha256=nUsunY09zXYAX-D5oEPD6B2WaWicMLJDNjjxbdce4gE,10566
|
|
71
71
|
cli/tools/delegate.py,sha256=j82eh1UhvQ0k0lShynngmHiUFxEN1SCQG6LwOxoiYBM,54243
|
|
72
|
-
cli/tools/edit.py,sha256=
|
|
72
|
+
cli/tools/edit.py,sha256=6JDUIVWiKjbbdukEELTA9eISOQQSxGa1xzicYURvJvU,21834
|
|
73
73
|
cli/tools/edits.py,sha256=8zM01TzLmjm7ULQlCmXOmitlJd84zQHVzE0z7UHJUdA,5520
|
|
74
74
|
cli/tools/federate.py,sha256=wmC2QN7A6aay3cT7U9LDPYRCZKL1m_T6qFdZzpZmPx4,4650
|
|
75
75
|
cli/tools/filter.py,sha256=_yhjOncC1kb-aRxjT7pkSILcMC-yXarBMh7oNS057rQ,12083
|
|
@@ -102,7 +102,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
102
102
|
cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
|
|
103
103
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
104
104
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
105
|
-
code_context_control-2.61.
|
|
105
|
+
code_context_control-2.61.2.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
106
106
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
107
107
|
core/config.py,sha256=YmkcZwedz_lfDM0ZuI_f4xpe98ixPLcSQFcTCHgRiRg,19206
|
|
108
108
|
core/ide.py,sha256=V6VVMVsFdmmcsMyxikjQp7z9xa42CWiHKS-ya-MAcG4,6172
|
|
@@ -163,7 +163,7 @@ services/claude_md.py,sha256=HHJo3sKn1KM-DE5b5d3mSyf9PV60EUsQ8015HjkMAq4,45549
|
|
|
163
163
|
services/compressor.py,sha256=AK31TqHFpkOD_Lqbp9aEOkOx7zENUwpsQHQ9B2imEHo,33081
|
|
164
164
|
services/context_snapshot.py,sha256=2f_bxY3xX4ZC653ncYno8YSeSTFSavwBjrorytHA0Ns,16879
|
|
165
165
|
services/conversation_store.py,sha256=-E2H6f9pMSOHiBjsq7tFUjrFAwWYExf7LJkibi5MRMA,35650
|
|
166
|
-
services/credential_store.py,sha256=
|
|
166
|
+
services/credential_store.py,sha256=P2_zb_ESr3UXLzju9bLOdQRQDsvrXiL5ulYssztnJJE,25677
|
|
167
167
|
services/doc_index.py,sha256=ZEQD1kyGCAMm9AY4Kpi1zDWJTul-cAcAdi_4Md43Hpw,19205
|
|
168
168
|
services/e2e_benchmark.py,sha256=4pDXkOFjq5b2ZwDvMK0CWdUXQli5OE4cOh_OJs7W36c,132970
|
|
169
169
|
services/e2e_evaluator.py,sha256=FsB4vMzcFUcmicfIwRtA8JZ_XeMpuk2_-86F2iVNEDc,17783
|
|
@@ -240,8 +240,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
240
240
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
241
241
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
242
242
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
243
|
-
code_context_control-2.61.
|
|
244
|
-
code_context_control-2.61.
|
|
245
|
-
code_context_control-2.61.
|
|
246
|
-
code_context_control-2.61.
|
|
247
|
-
code_context_control-2.61.
|
|
243
|
+
code_context_control-2.61.2.dist-info/METADATA,sha256=G1VV47I4vb9Bp0A8cOdzUJTUgqfvb9FfaI2iEgJRF3E,31631
|
|
244
|
+
code_context_control-2.61.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
245
|
+
code_context_control-2.61.2.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
246
|
+
code_context_control-2.61.2.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
247
|
+
code_context_control-2.61.2.dist-info/RECORD,,
|
services/credential_store.py
CHANGED
|
@@ -135,6 +135,56 @@ def _account(realm_s: str, name: str) -> str:
|
|
|
135
135
|
return f"{realm_s}|{name}"
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def _flag_account(realm_s: str, name: str) -> str:
|
|
139
|
+
return f"{realm_s}|{name}::agent_readable"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Files the agent must never write through c3 tool surfaces: the vault
|
|
143
|
+
# registry (config.json) and its sidecar state. Editing these outside the
|
|
144
|
+
# credentials API is how a prompt-injected agent would grant itself reveal
|
|
145
|
+
# access. Mirrored in cli/hook_pretool_enforce.py (parity-tested).
|
|
146
|
+
VAULT_PROTECTED_FILES = frozenset({"config.json", "secrets.enc", "cred_state.json"})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def vault_guard_reason(path) -> str:
|
|
150
|
+
"""Non-empty refusal message when ``path`` is a vault file under .c3/."""
|
|
151
|
+
p = Path(path)
|
|
152
|
+
if p.name.lower() in VAULT_PROTECTED_FILES and p.parent.name.lower() == ".c3":
|
|
153
|
+
return (
|
|
154
|
+
f"[c3:vault-protected] {p.name} belongs to the credential vault "
|
|
155
|
+
"(registry/state) and cannot be modified by the agent. Changes go "
|
|
156
|
+
"through the Credentials UI or the `c3 creds` CLI — ask the user."
|
|
157
|
+
)
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _write_flag_attestation(realm_s: str, name: str, readable: bool) -> None:
|
|
162
|
+
"""Keyring copy of agent_readable — reveal trusts registry AND keyring.
|
|
163
|
+
|
|
164
|
+
A registry flag flipped by editing config.json directly (outside this
|
|
165
|
+
API) disagrees with the attestation, and reveal fails closed.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
_keyring_module().set_password(
|
|
169
|
+
KEYRING_SERVICE, _flag_account(realm_s, name), "1" if readable else "0")
|
|
170
|
+
except Exception:
|
|
171
|
+
pass # keyring-less env: verify_agent_readable fails closed anyway
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def verify_agent_readable(name: str, *, scope: str, project_path: str = ".") -> bool:
|
|
175
|
+
"""True only when the keyring attestation agrees the flag is enabled.
|
|
176
|
+
|
|
177
|
+
Fails closed: a missing attestation (pre-v2.61.2 entry, keyring failure,
|
|
178
|
+
or a registry edited outside this API) reads as not-readable.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
realm_s = realm(_norm_scope(scope, project_path), project_path)
|
|
182
|
+
return _keyring_module().get_password(
|
|
183
|
+
KEYRING_SERVICE, _flag_account(realm_s, name)) == "1"
|
|
184
|
+
except Exception:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
138
188
|
def _scope_dir(scope: str, project_path: str) -> Optional[Path]:
|
|
139
189
|
"""Base directory holding .c3/ for the scope; None when global is unresolvable."""
|
|
140
190
|
if scope == "global":
|
|
@@ -376,6 +426,7 @@ def set_credential(
|
|
|
376
426
|
section["entries"][name] = entry
|
|
377
427
|
config["credentials"] = section
|
|
378
428
|
_save_config(base, config)
|
|
429
|
+
_write_flag_attestation(realm_s, name, bool(agent_readable))
|
|
379
430
|
return dict(entry)
|
|
380
431
|
|
|
381
432
|
|
|
@@ -407,6 +458,9 @@ def update_metadata(name: str, *, scope: str, project_path: str = ".", **fields)
|
|
|
407
458
|
entry["updated"] = _utcnow()
|
|
408
459
|
config["credentials"] = section
|
|
409
460
|
_save_config(base, config)
|
|
461
|
+
if "agent_readable" in fields:
|
|
462
|
+
_write_flag_attestation(
|
|
463
|
+
realm(scope, project_path), name, bool(fields["agent_readable"]))
|
|
410
464
|
return dict(entry)
|
|
411
465
|
|
|
412
466
|
|
|
@@ -467,6 +521,10 @@ def delete_credential(name: str, *, scope: str, project_path: str = ".") -> bool
|
|
|
467
521
|
removed_value = True
|
|
468
522
|
except Exception:
|
|
469
523
|
pass
|
|
524
|
+
try:
|
|
525
|
+
_keyring_module().delete_password(KEYRING_SERVICE, _flag_account(realm_s, name))
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
470
528
|
config = _load_config(base)
|
|
471
529
|
section = _creds_section(config)
|
|
472
530
|
removed_entry = name in section["entries"]
|
|
File without changes
|
{code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.61.0.dist-info → code_context_control-2.61.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|