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 CHANGED
@@ -92,7 +92,7 @@ console = Console() if HAS_RICH else None
92
92
  # Config
93
93
  CONFIG_DIR = ".c3"
94
94
  CONFIG_FILE = ".c3/config.json"
95
- __version__ = "2.61.0"
95
+ __version__ = "2.61.2"
96
96
 
97
97
 
98
98
  def _command_deps() -> CommandDeps:
@@ -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 — and navigate it when
381
- // polling /api/projects finds the port.
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, '&amp;').replace(/</g, '&lt;');
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
- `Starting ${p.name}…</body>`);
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) win.close();
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
- const url = 'http://127.0.0.1:' + row.port;
413
- if (win) { win.location = url; } else { window.open(url, '_blank'); }
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) { fail(`${p.name}: UI server did not report a port — check .c3/ui.log`); return; }
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.0
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=H4UM0e53AgRd9Mi-4WRt-ZHfWuo7ZBjJUQB-QEeOgx0,322864
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=Qbm4VncD4j0HA8urzR6vRTtm06OmlLz7DfL78BOY9b8,13262
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=PBHtRmh-bocIDkd_qlJmMoxQqQYTcDFFGmjmeSI560E,27219
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=YgrC4-GhuKb77jBokIP7mrMMWy2L391_DPizc3NtIno,10051
70
+ cli/tools/credentials.py,sha256=nUsunY09zXYAX-D5oEPD6B2WaWicMLJDNjjxbdce4gE,10566
71
71
  cli/tools/delegate.py,sha256=j82eh1UhvQ0k0lShynngmHiUFxEN1SCQG6LwOxoiYBM,54243
72
- cli/tools/edit.py,sha256=_3QosNcdZ_DaFRYn0Pa7qPsOuMaw7A9zcavb2HvgK2U,21493
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.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
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=4LAolC_fBjFgW-Lg8yMpoRwFh0dwi-raP1UMw5z3YbA,23270
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.0.dist-info/METADATA,sha256=wcocBgnJCm25L1f0A8sILEwYvL6gq1bFqiHDfFUWI_w,31631
244
- code_context_control-2.61.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
245
- code_context_control-2.61.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
246
- code_context_control-2.61.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
247
- code_context_control-2.61.0.dist-info/RECORD,,
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,,
@@ -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"]