code-context-control 2.58.0__py3-none-any.whl → 2.59.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/guide/tools.html +41 -0
- cli/hub_server.py +258 -17
- cli/hub_ui/app.js +4 -2
- cli/hub_ui/components/drill_views.js +4 -48
- cli/hub_ui/components/hub_credentials.js +516 -0
- cli/hub_ui/components/topbar.js +1 -1
- {code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/METADATA +9 -1
- {code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/RECORD +14 -13
- services/credential_store.py +5 -0
- {code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
cli/guide/tools.html
CHANGED
|
@@ -125,6 +125,7 @@
|
|
|
125
125
|
<div class="sidebar-label">SCM</div>
|
|
126
126
|
<a href="#c3_task" class="sidebar-link"><span class="icon">✅</span> c3_task</a>
|
|
127
127
|
<a href="#c3_bitbucket" class="sidebar-link"><span class="icon">🪣</span> c3_bitbucket</a>
|
|
128
|
+
<a href="#c3_credentials" class="sidebar-link"><span class="icon">🔐</span> c3_credentials</a>
|
|
128
129
|
</div>
|
|
129
130
|
<div class="sidebar-section">
|
|
130
131
|
<div class="sidebar-label">Cross-project</div>
|
|
@@ -161,6 +162,7 @@
|
|
|
161
162
|
<a href="#c3_task" class="toc-item">c3_task <span class="cat">PM</span></a>
|
|
162
163
|
<a href="#c3_artifacts" class="toc-item">c3_artifacts <span class="cat">Config</span></a>
|
|
163
164
|
<a href="#c3_bitbucket" class="toc-item">c3_bitbucket <span class="cat">SCM</span></a>
|
|
165
|
+
<a href="#c3_credentials" class="toc-item">c3_credentials <span class="cat">Secrets</span></a>
|
|
164
166
|
<a href="#c3_project" class="toc-item">c3_project <span class="cat">Multi-project</span></a>
|
|
165
167
|
</div>
|
|
166
168
|
</div>
|
|
@@ -1239,6 +1241,45 @@ c3_bitbucket(action=<span class="str">'merge_pr'</span>, pr_id=<span class="num"
|
|
|
1239
1241
|
<p class="tool-desc"><strong>Audit trail:</strong> mutating actions (<code>merge_pr</code>, <code>create_branch</code>, <code>delete_branch</code>, webhook writes, etc.) are appended to the C3 edit ledger so the local audit log covers platform-side state changes too.</p>
|
|
1240
1242
|
</div>
|
|
1241
1243
|
</div>
|
|
1244
|
+
|
|
1245
|
+
<!-- c3_credentials -->
|
|
1246
|
+
<div class="tool-card" id="c3_credentials">
|
|
1247
|
+
<div class="tool-card-header">
|
|
1248
|
+
<span class="tool-name">c3_credentials</span>
|
|
1249
|
+
<div class="tag-row">
|
|
1250
|
+
<span class="badge badge-purple">secrets</span>
|
|
1251
|
+
<span class="badge">v2.58.0</span>
|
|
1252
|
+
</div>
|
|
1253
|
+
<span class="tool-tagline">Credential vault — the agent uses secrets by name, never by value</span>
|
|
1254
|
+
</div>
|
|
1255
|
+
<div class="tool-card-body">
|
|
1256
|
+
<p class="tool-desc">A user-managed secret store, <strong>global</strong> (<code>~/.c3</code>, visible in every project) or <strong>per-project</strong> (<code>.c3</code>, shadows the same-named global). Values live in the OS keyring (service <code>c3-creds</code>; values over ~1KB in a Fernet-encrypted <code>.c3/secrets.enc</code> whose random master key is itself a keyring entry) — never in config files, and <em>never in the model's context</em>. <strong>Injection-first:</strong> the agent addresses secrets by name and C3 decodes them only at the subprocess boundary — <code>c3_shell(env_creds='A,B')</code> injects env vars, <code>{{cred:NAME}}</code> inside <code>cmd</code> expands server-side, and <code>inject</code>-flagged entries auto-inject into every shell run; echoed values are redacted back to <code>[cred:NAME]</code>. Resolution is <strong>realm-atomic</strong>: a project-registered name resolves in the project realm or not at all, so a hostile repo's committed config can never siphon your global values (tested).</p>
|
|
1257
|
+
|
|
1258
|
+
<h4>Actions</h4>
|
|
1259
|
+
<table class="params-table">
|
|
1260
|
+
<thead><tr><th>Action</th><th>Group</th><th>Description</th></tr></thead>
|
|
1261
|
+
<tbody>
|
|
1262
|
+
<tr><td class="param-name">list</td><td>Read</td><td class="param-desc">Merged registry (project shadows global) — names, scope, type, flags, usage. Never values</td></tr>
|
|
1263
|
+
<tr><td class="param-name">describe</td><td>Read</td><td class="param-desc">Metadata + storage + live fingerprint (first 8 hex of SHA-256, computed on demand, never persisted)</td></tr>
|
|
1264
|
+
<tr><td class="param-name">check</td><td>Read</td><td class="param-desc">Resolvability probe — does the stored value decode?</td></tr>
|
|
1265
|
+
<tr><td class="param-name">set</td><td>Write</td><td class="param-desc">Create/replace an entry. <code>agent_readable</code> may be set at creation only — raising it on an existing entry is user-only</td></tr>
|
|
1266
|
+
<tr><td class="param-name">delete</td><td>Write</td><td class="param-desc">Remove value + registry entry (owning scope inferred when omitted)</td></tr>
|
|
1267
|
+
<tr><td class="param-name">reveal</td><td>Gated</td><td class="param-desc">The only value-returning action — refused unless the user enabled <code>agent_readable</code> on that entry</td></tr>
|
|
1268
|
+
</tbody>
|
|
1269
|
+
</table>
|
|
1270
|
+
|
|
1271
|
+
<h4>Examples</h4>
|
|
1272
|
+
<pre><code><span class="com"># What can I use? (names and metadata only)</span>
|
|
1273
|
+
c3_credentials(action=<span class="str">'list'</span>)
|
|
1274
|
+
c3_credentials(action=<span class="str">'check'</span>, name=<span class="str">'NPM_TOKEN'</span>)
|
|
1275
|
+
|
|
1276
|
+
<span class="com"># USE a secret without ever seeing it</span>
|
|
1277
|
+
c3_shell(cmd=<span class="str">'npm publish'</span>, env_creds=<span class="str">'NPM_TOKEN'</span>)
|
|
1278
|
+
c3_shell(cmd=<span class="str">'curl -H "Authorization: Bearer {{cred:API_KEY}}" https://api.example.com'</span>)</code></pre>
|
|
1279
|
+
|
|
1280
|
+
<p class="tool-desc"><strong>Surfaces:</strong> the <code>c3 creds</code> CLI (<code>set</code> / <code>get</code> / <code>list</code> / <code>rm</code> / <code>import .env</code>, <code>--global</code> for the shared scope), a per-project dashboard <strong>Credentials</strong> tab, and — since v2.59.0 — a top-level <strong>Credentials</strong> view in the Hub: a Global-vault manager plus a per-project browser with shadowing shown both ways ("shadows global" on project entries, "shadowed in N projects" on globals); the Hub's per-project drill panel manages too. No HTTP route ever returns a stored value (write-only wire contract, endpoint-sweep tested), every mutation is ledger-logged by name, the vault is hard-excluded from the Oracle Discovery API, and cross-project (<code>c3_project</code>) shells run with credentials disabled.</p>
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1242
1283
|
</div>
|
|
1243
1284
|
|
|
1244
1285
|
<!-- ═══════════════════════════════════════ CROSS-PROJECT ═════ -->
|
cli/hub_server.py
CHANGED
|
@@ -333,6 +333,7 @@ _HUB_JS_FILES = [
|
|
|
333
333
|
"hub_ui/components/session_drawer.js",
|
|
334
334
|
"hub_ui/components/drill_panel.js",
|
|
335
335
|
"hub_ui/components/drill_views.js",
|
|
336
|
+
"hub_ui/components/hub_credentials.js",
|
|
336
337
|
"hub_ui/components/drill_subprojects.js",
|
|
337
338
|
"hub_ui/components/drill_health.js",
|
|
338
339
|
"hub_ui/components/drill_tasks.js",
|
|
@@ -437,8 +438,8 @@ def api_hub_config_set():
|
|
|
437
438
|
cfg["projects_view"] = projects_view
|
|
438
439
|
if "main_view" in data:
|
|
439
440
|
main_view = str(data["main_view"]).strip().lower()
|
|
440
|
-
if main_view not in {"projects", "board"}:
|
|
441
|
-
return jsonify({"error": "main_view must be 'projects' or '
|
|
441
|
+
if main_view not in {"projects", "board", "creds"}:
|
|
442
|
+
return jsonify({"error": "main_view must be 'projects', 'board' or 'creds'"}), 400
|
|
442
443
|
cfg["main_view"] = main_view
|
|
443
444
|
if "oracle_url" in data:
|
|
444
445
|
cfg["oracle_url"] = str(data["oracle_url"]).strip()
|
|
@@ -1773,12 +1774,100 @@ def api_projects_config_get():
|
|
|
1773
1774
|
"defaults": {k: _config_defaults(k) for k in _CONFIG_READ_SECTIONS}})
|
|
1774
1775
|
|
|
1775
1776
|
|
|
1777
|
+
# ── Credentials (hub) ────────────────────────────────────────────────────────
|
|
1778
|
+
# Write-only wire contract, hub edition: values go IN over POST and are never
|
|
1779
|
+
# returned by any hub route (no reveal exists here). `credentials` stays out of
|
|
1780
|
+
# _CONFIG_WRITE_SECTIONS — these dedicated routes are the only hub write path.
|
|
1781
|
+
|
|
1782
|
+
_CRED_PUBLIC_FIELDS = ("scope", "type", "value_len", "env_var", "inject",
|
|
1783
|
+
"agent_readable", "description", "storage", "created",
|
|
1784
|
+
"updated")
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
def _cred_entry_public(name, entry, *, usage=None, shadows_global=None):
|
|
1788
|
+
"""Explicit allowlist serializer — structurally cannot emit a value."""
|
|
1789
|
+
rec = {"name": name}
|
|
1790
|
+
for key in _CRED_PUBLIC_FIELDS:
|
|
1791
|
+
rec[key] = entry.get(key, "")
|
|
1792
|
+
rec["value_len"] = entry.get("value_len", 0)
|
|
1793
|
+
rec["inject"] = bool(entry.get("inject"))
|
|
1794
|
+
rec["agent_readable"] = bool(entry.get("agent_readable"))
|
|
1795
|
+
if usage is not None:
|
|
1796
|
+
rec["last_used"] = (usage.get(name) or {}).get("last_used", "")
|
|
1797
|
+
rec["use_count"] = (usage.get(name) or {}).get("use_count", 0)
|
|
1798
|
+
if shadows_global is not None:
|
|
1799
|
+
rec["shadows_global"] = bool(shadows_global)
|
|
1800
|
+
return rec
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
def _resolve_cred_target(path: str, scope: str, *, mutation: bool):
|
|
1804
|
+
"""Resolve a credentials request target to (project, store_path, error).
|
|
1805
|
+
|
|
1806
|
+
scope='global' with no path targets the shared vault (~/.c3) directly;
|
|
1807
|
+
project scope requires a registered path. Mutations on a path without a
|
|
1808
|
+
.c3/ dir get 409 needs_init so the hub can't scatter .c3 dirs around."""
|
|
1809
|
+
from services import credential_store as cred_store
|
|
1810
|
+
if scope not in ("project", "global"):
|
|
1811
|
+
return None, "", (jsonify({"error": "scope must be 'project' or 'global'"}), 400)
|
|
1812
|
+
if not path:
|
|
1813
|
+
if scope == "project":
|
|
1814
|
+
return None, "", (jsonify({"error": "path is required for project scope"}), 400)
|
|
1815
|
+
home = cred_store.global_base()
|
|
1816
|
+
if home is None:
|
|
1817
|
+
return None, "", (jsonify({"error": "global scope unresolvable (no home dir)"}), 500)
|
|
1818
|
+
return None, str(home), None
|
|
1819
|
+
try:
|
|
1820
|
+
resolved = _resolve_project_path(path)
|
|
1821
|
+
except ValueError as e:
|
|
1822
|
+
return None, "", (jsonify({"error": str(e)}), 404)
|
|
1823
|
+
if mutation and not (resolved / ".c3").is_dir():
|
|
1824
|
+
return None, "", (jsonify({"error": "not initialized", "needs_init": True}), 409)
|
|
1825
|
+
return resolved, str(resolved), None
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def _hub_cred_audit(action: str, name: str, scope: str, project) -> None:
|
|
1829
|
+
"""Names only — never values. Failure-safe. Project mutations audit to the
|
|
1830
|
+
target project's ActivityLog + EditLedger; global-scope mutations also land
|
|
1831
|
+
in ~/.c3/activity_log.jsonl so the shared vault keeps its own trail."""
|
|
1832
|
+
if project is not None:
|
|
1833
|
+
try:
|
|
1834
|
+
from services.activity_log import ActivityLog
|
|
1835
|
+
ActivityLog(str(project)).log("cred_action", {
|
|
1836
|
+
"kind": "creds", "action": action, "name": name,
|
|
1837
|
+
"scope": scope, "via": "hub",
|
|
1838
|
+
})
|
|
1839
|
+
except Exception:
|
|
1840
|
+
pass
|
|
1841
|
+
try:
|
|
1842
|
+
from services.edit_ledger import EditLedger
|
|
1843
|
+
EditLedger(str(project)).log_edit(
|
|
1844
|
+
file=f"cred://{name}", change_type=f"cred_{action}",
|
|
1845
|
+
summary=f"{action} {name} ({scope}) via Hub",
|
|
1846
|
+
tags=["creds", action],
|
|
1847
|
+
detail={"kind": "creds", "action": action, "name": name,
|
|
1848
|
+
"scope": scope},
|
|
1849
|
+
)
|
|
1850
|
+
except Exception:
|
|
1851
|
+
pass
|
|
1852
|
+
if scope == "global" or project is None:
|
|
1853
|
+
try:
|
|
1854
|
+
from services import credential_store as cred_store
|
|
1855
|
+
from services.activity_log import ActivityLog
|
|
1856
|
+
home = cred_store.global_base()
|
|
1857
|
+
if home is not None:
|
|
1858
|
+
ActivityLog(str(home)).log("cred_action", {
|
|
1859
|
+
"kind": "creds", "action": action, "name": name,
|
|
1860
|
+
"scope": scope, "via": "hub",
|
|
1861
|
+
})
|
|
1862
|
+
except Exception:
|
|
1863
|
+
pass
|
|
1864
|
+
|
|
1865
|
+
|
|
1776
1866
|
@app.route("/api/projects/credentials", methods=["GET"])
|
|
1777
1867
|
def api_projects_credentials():
|
|
1778
|
-
"""
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
(`credentials` is deliberately absent from _CONFIG_WRITE_SECTIONS)."""
|
|
1868
|
+
"""Masked credential registry for a project (global entries + project
|
|
1869
|
+
shadows). Values never transit the hub outbound — the allowlist serializer
|
|
1870
|
+
returns metadata, usage and shadow info only."""
|
|
1782
1871
|
path = (request.args.get("path") or "").strip()
|
|
1783
1872
|
if not path:
|
|
1784
1873
|
return jsonify({"error": "path is required"}), 400
|
|
@@ -1787,22 +1876,174 @@ def api_projects_credentials():
|
|
|
1787
1876
|
except ValueError as e:
|
|
1788
1877
|
return jsonify({"error": str(e)}), 404
|
|
1789
1878
|
from services import credential_store as cred_store
|
|
1879
|
+
usage = cred_store.read_usage_state(str(resolved))
|
|
1880
|
+
home = cred_store.global_base()
|
|
1881
|
+
global_names = set(cred_store.list_entries(str(home))) if home else set()
|
|
1790
1882
|
entries = []
|
|
1791
1883
|
for name, entry in cred_store.list_entries(str(resolved)).items():
|
|
1792
|
-
entries.append(
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
"value_len": entry.get("value_len", 0),
|
|
1797
|
-
"env_var": entry.get("env_var", ""),
|
|
1798
|
-
"inject": bool(entry.get("inject")),
|
|
1799
|
-
"agent_readable": bool(entry.get("agent_readable")),
|
|
1800
|
-
"description": entry.get("description", ""),
|
|
1801
|
-
"updated": entry.get("updated", ""),
|
|
1802
|
-
})
|
|
1884
|
+
entries.append(_cred_entry_public(
|
|
1885
|
+
name, entry, usage=usage,
|
|
1886
|
+
shadows_global=(entry.get("scope") == "project"
|
|
1887
|
+
and name in global_names)))
|
|
1803
1888
|
return jsonify({"path": str(resolved), "entries": entries})
|
|
1804
1889
|
|
|
1805
1890
|
|
|
1891
|
+
@app.route("/api/projects/credentials", methods=["POST"])
|
|
1892
|
+
def api_projects_credentials_set():
|
|
1893
|
+
"""Create/update an entry from the hub. `value` optional — metadata-only
|
|
1894
|
+
update when absent, touching ONLY the keys present in the payload; a
|
|
1895
|
+
submitted value is stored and never echoed back. scope='global' with no
|
|
1896
|
+
`path` targets the shared vault directly."""
|
|
1897
|
+
from services import credential_store as cred_store
|
|
1898
|
+
data = request.get_json(force=True) or {}
|
|
1899
|
+
name = str(data.get("name") or "").strip()
|
|
1900
|
+
scope = str(data.get("scope") or "project").strip().lower()
|
|
1901
|
+
project, store_path, err = _resolve_cred_target(
|
|
1902
|
+
str(data.get("path") or "").strip(), scope, mutation=True)
|
|
1903
|
+
if err:
|
|
1904
|
+
return err
|
|
1905
|
+
value = str(data.get("value") or "")
|
|
1906
|
+
ctype = str(data.get("type") or data.get("ctype") or "token")
|
|
1907
|
+
try:
|
|
1908
|
+
if value:
|
|
1909
|
+
entry = cred_store.set_credential(
|
|
1910
|
+
name, value, scope=scope, project_path=store_path, ctype=ctype,
|
|
1911
|
+
description=str(data.get("description") or ""),
|
|
1912
|
+
env_var=str(data.get("env_var") or ""),
|
|
1913
|
+
agent_readable=bool(data.get("agent_readable")),
|
|
1914
|
+
inject=bool(data.get("inject")))
|
|
1915
|
+
else:
|
|
1916
|
+
# Metadata-only update: touch ONLY the keys present in the payload
|
|
1917
|
+
# so a single-field toggle can't clobber the others.
|
|
1918
|
+
fields = {}
|
|
1919
|
+
for key in ("description", "env_var"):
|
|
1920
|
+
if key in data:
|
|
1921
|
+
fields[key] = str(data[key] or "")
|
|
1922
|
+
for key in ("agent_readable", "inject"):
|
|
1923
|
+
if key in data:
|
|
1924
|
+
fields[key] = bool(data[key])
|
|
1925
|
+
if "type" in data or "ctype" in data:
|
|
1926
|
+
fields["type"] = ctype
|
|
1927
|
+
entry = cred_store.update_metadata(
|
|
1928
|
+
name, scope=scope, project_path=store_path, **fields)
|
|
1929
|
+
except cred_store.CredentialError as exc:
|
|
1930
|
+
return jsonify({"error": str(exc)}), 400
|
|
1931
|
+
except RuntimeError as exc:
|
|
1932
|
+
return jsonify({"error": str(exc)}), 500
|
|
1933
|
+
_hub_cred_audit("set" if value else "update", name, scope, project)
|
|
1934
|
+
return jsonify({"entry": _cred_entry_public(name, {**entry, "scope": scope})})
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
@app.route("/api/projects/credentials/import", methods=["POST"])
|
|
1938
|
+
def api_projects_credentials_import():
|
|
1939
|
+
"""Import KEY=VALUE lines (.env paste). Values are stored, never echoed."""
|
|
1940
|
+
from services import credential_store as cred_store
|
|
1941
|
+
data = request.get_json(force=True) or {}
|
|
1942
|
+
scope = str(data.get("scope") or "project").strip().lower()
|
|
1943
|
+
project, store_path, err = _resolve_cred_target(
|
|
1944
|
+
str(data.get("path") or "").strip(), scope, mutation=True)
|
|
1945
|
+
if err:
|
|
1946
|
+
return err
|
|
1947
|
+
try:
|
|
1948
|
+
result = cred_store.import_env(
|
|
1949
|
+
str(data.get("text") or ""), scope=scope, project_path=store_path,
|
|
1950
|
+
overwrite=bool(data.get("overwrite")))
|
|
1951
|
+
except cred_store.CredentialError as exc:
|
|
1952
|
+
return jsonify({"error": str(exc)}), 400
|
|
1953
|
+
except RuntimeError as exc:
|
|
1954
|
+
return jsonify({"error": str(exc)}), 500
|
|
1955
|
+
for created in result["created"]:
|
|
1956
|
+
_hub_cred_audit("set", created, scope, project)
|
|
1957
|
+
return jsonify(result)
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
@app.route("/api/projects/credentials/<name>", methods=["DELETE"])
|
|
1961
|
+
def api_projects_credentials_delete(name):
|
|
1962
|
+
"""Delete an entry (value + registry). Scope inferred from the owning
|
|
1963
|
+
realm when omitted."""
|
|
1964
|
+
from services import credential_store as cred_store
|
|
1965
|
+
scope = str(request.args.get("scope") or "").strip().lower()
|
|
1966
|
+
path = str(request.args.get("path") or "").strip()
|
|
1967
|
+
project, store_path, err = _resolve_cred_target(
|
|
1968
|
+
path, scope or ("project" if path else "global"), mutation=True)
|
|
1969
|
+
if err:
|
|
1970
|
+
return err
|
|
1971
|
+
if not scope:
|
|
1972
|
+
entry = cred_store.get_entry(name, project_path=store_path)
|
|
1973
|
+
scope = (entry.get("scope") or "project") if entry else \
|
|
1974
|
+
("project" if path else "global")
|
|
1975
|
+
try:
|
|
1976
|
+
removed = cred_store.delete_credential(
|
|
1977
|
+
name, scope=scope, project_path=store_path)
|
|
1978
|
+
except cred_store.CredentialError as exc:
|
|
1979
|
+
return jsonify({"error": str(exc)}), 400
|
|
1980
|
+
if removed:
|
|
1981
|
+
_hub_cred_audit("delete", name, scope, project)
|
|
1982
|
+
return jsonify({"removed": bool(removed), "scope": scope})
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
@app.route("/api/projects/credentials/<name>/check", methods=["POST"])
|
|
1986
|
+
def api_projects_credentials_check(name):
|
|
1987
|
+
"""Resolvability probe — returns a fingerprint, never the value."""
|
|
1988
|
+
from services import credential_store as cred_store
|
|
1989
|
+
data = request.get_json(silent=True) or {}
|
|
1990
|
+
path = str(data.get("path") or "").strip()
|
|
1991
|
+
project, store_path, err = _resolve_cred_target(
|
|
1992
|
+
path, "project" if path else "global", mutation=False)
|
|
1993
|
+
if err:
|
|
1994
|
+
return err
|
|
1995
|
+
entry = cred_store.get_entry(name, project_path=store_path)
|
|
1996
|
+
if not entry:
|
|
1997
|
+
return jsonify({"error": f"no credential named '{name}'"}), 404
|
|
1998
|
+
return jsonify({
|
|
1999
|
+
"name": name,
|
|
2000
|
+
"scope": entry["scope"],
|
|
2001
|
+
"storage": entry.get("storage", "keyring"),
|
|
2002
|
+
"resolvable": cred_store.get_value(name, project_path=store_path) is not None,
|
|
2003
|
+
"fingerprint": cred_store.fingerprint(name, project_path=store_path),
|
|
2004
|
+
})
|
|
2005
|
+
|
|
2006
|
+
|
|
2007
|
+
@app.route("/api/hub/credentials/overview", methods=["GET"])
|
|
2008
|
+
def api_hub_credentials_overview():
|
|
2009
|
+
"""Cross-project credential inventory: the global vault plus each
|
|
2010
|
+
registered project's project-scoped entries, with shadow info both ways.
|
|
2011
|
+
Metadata only — the allowlist serializer structurally cannot emit a value."""
|
|
2012
|
+
from services import credential_store as cred_store
|
|
2013
|
+
home = cred_store.global_base()
|
|
2014
|
+
global_entries = cred_store.list_entries(str(home)) if home else {}
|
|
2015
|
+
shadowed_in = {name: [] for name in global_entries}
|
|
2016
|
+
projects_out = []
|
|
2017
|
+
for p in _pm().list_projects():
|
|
2018
|
+
ppath = str(p.get("path") or "")
|
|
2019
|
+
row = {"name": p.get("name") or "", "path": ppath,
|
|
2020
|
+
"initialized": True, "error": None, "entries": []}
|
|
2021
|
+
try:
|
|
2022
|
+
if not (Path(ppath) / ".c3").is_dir():
|
|
2023
|
+
row["initialized"] = False
|
|
2024
|
+
else:
|
|
2025
|
+
usage = cred_store.read_usage_state(ppath)
|
|
2026
|
+
for name, entry in cred_store.list_entries(ppath).items():
|
|
2027
|
+
if entry.get("scope") != "project":
|
|
2028
|
+
continue
|
|
2029
|
+
row["entries"].append(_cred_entry_public(
|
|
2030
|
+
name, entry, usage=usage,
|
|
2031
|
+
shadows_global=name in global_entries))
|
|
2032
|
+
if name in shadowed_in:
|
|
2033
|
+
shadowed_in[name].append(
|
|
2034
|
+
{"name": row["name"], "path": ppath})
|
|
2035
|
+
except Exception as e: # per-row isolation, like /api/search/global
|
|
2036
|
+
row["error"] = str(e)
|
|
2037
|
+
projects_out.append(row)
|
|
2038
|
+
global_usage = cred_store.read_usage_state(str(home)) if home else {}
|
|
2039
|
+
global_out = [
|
|
2040
|
+
{**_cred_entry_public(name, entry, usage=global_usage),
|
|
2041
|
+
"shadowed_in": shadowed_in.get(name, [])}
|
|
2042
|
+
for name, entry in global_entries.items()
|
|
2043
|
+
]
|
|
2044
|
+
return jsonify({"global": {"entries": global_out}, "projects": projects_out})
|
|
2045
|
+
|
|
2046
|
+
|
|
1806
2047
|
@app.route("/api/projects/config", methods=["PUT"])
|
|
1807
2048
|
def api_projects_config_put():
|
|
1808
2049
|
"""Whitelisted section write: deep-merge, atomic replace, audited on the target."""
|
cli/hub_ui/app.js
CHANGED
|
@@ -17,7 +17,7 @@ function App() {
|
|
|
17
17
|
const [version, setVersion] = useState('');
|
|
18
18
|
const [projects, setProjects] = useState([]);
|
|
19
19
|
const [loaded, setLoaded] = useState(false);
|
|
20
|
-
const [mainView, setMainView] = useState('projects'); // projects | board
|
|
20
|
+
const [mainView, setMainView] = useState('projects'); // projects | board | creds
|
|
21
21
|
const [filter, setFilter] = useState('all'); // all | active | idle | tag:<x>
|
|
22
22
|
const [search, setSearch] = useState('');
|
|
23
23
|
const [view, setView] = useState('list'); // list | grid
|
|
@@ -44,7 +44,7 @@ function App() {
|
|
|
44
44
|
if (cfg.projects_view === 'grid') setView('grid');
|
|
45
45
|
if (cfg.sidebar_collapsed != null) setSidebarCollapsed(!!cfg.sidebar_collapsed);
|
|
46
46
|
if (cfg.sidebar_group) setFilter(cfg.sidebar_group);
|
|
47
|
-
if (cfg.main_view === 'board') setMainView(
|
|
47
|
+
if (cfg.main_view === 'board' || cfg.main_view === 'creds') setMainView(cfg.main_view);
|
|
48
48
|
} catch { }
|
|
49
49
|
try { const v = await api.get('/api/version'); setVersion(v.c3_version || ''); } catch { }
|
|
50
50
|
}, []);
|
|
@@ -112,6 +112,8 @@ function App() {
|
|
|
112
112
|
}}>
|
|
113
113
|
{mainView === 'board' ? (
|
|
114
114
|
<TaskBoard projects={projects} onOpenDrill={openDrill} />
|
|
115
|
+
) : mainView === 'creds' ? (
|
|
116
|
+
<HubCredentials projects={projects} onOpenDrill={openDrill} />
|
|
115
117
|
) : (
|
|
116
118
|
<React.Fragment>
|
|
117
119
|
<SummaryBar projects={projects} search={search} setSearch={setSearch}
|
|
@@ -282,55 +282,11 @@ function DrillLedger({ project }) {
|
|
|
282
282
|
);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
// ──
|
|
286
|
-
//
|
|
287
|
-
//
|
|
285
|
+
// ── Credentials ────────────────────────────────────────────────
|
|
286
|
+
// Full management via the shared CredsManager (hub_credentials.js).
|
|
287
|
+
// Write-only wire: values are submitted inbound-only and never returned.
|
|
288
288
|
function DrillCredentials({ project }) {
|
|
289
|
-
|
|
290
|
-
const [err, setErr] = useState(null);
|
|
291
|
-
|
|
292
|
-
const load = async () => {
|
|
293
|
-
setErr(null);
|
|
294
|
-
try {
|
|
295
|
-
const d = await api.get('/api/projects/credentials?path=' + encodeURIComponent(project.path));
|
|
296
|
-
setData(d);
|
|
297
|
-
} catch (e) { setErr(e.message); }
|
|
298
|
-
};
|
|
299
|
-
useEffect(() => { setData(null); load(); }, [project.path]);
|
|
300
|
-
|
|
301
|
-
if (err) return <DrillMsg text={'Failed to load credentials: ' + err} color={T.error} />;
|
|
302
|
-
if (!data) return <DrillMsg text="Loading credentials…" />;
|
|
303
|
-
|
|
304
|
-
const entries = data.entries || [];
|
|
305
|
-
return (
|
|
306
|
-
<div className="fade-up">
|
|
307
|
-
<div style={{ fontSize: 11, color: T.textMuted, lineHeight: 1.5, marginBottom: 12 }}>
|
|
308
|
-
Read-only view — values never transit the hub. Manage entries (and the
|
|
309
|
-
agent_readable / inject flags) in this project's own UI Credentials tab
|
|
310
|
-
or via <span className="mono">c3 creds</span>.
|
|
311
|
-
</div>
|
|
312
|
-
{entries.length === 0 ? (
|
|
313
|
-
<DrillMsg text="No credentials registered for this project." />
|
|
314
|
-
) : entries.map((entry) => (
|
|
315
|
-
<div key={entry.scope + '|' + entry.name} style={{
|
|
316
|
-
display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0',
|
|
317
|
-
borderBottom: `1px solid ${T.border}`, fontSize: 12,
|
|
318
|
-
}}>
|
|
319
|
-
<I name="lock" size={12} color={T.textMuted} />
|
|
320
|
-
<span className="mono" style={{ fontWeight: 600, color: T.text }}>{entry.name}</span>
|
|
321
|
-
<Badge color={entry.scope === 'global' ? T.accent : T.ok}>{entry.scope}</Badge>
|
|
322
|
-
<Badge color={T.textMuted}>{entry.type}</Badge>
|
|
323
|
-
<span className="mono" style={{ color: T.textDim }}>len={entry.value_len}</span>
|
|
324
|
-
{!!entry.inject && <Badge color={T.warn}>inject</Badge>}
|
|
325
|
-
{!!entry.agent_readable && <Badge color={T.error}>agent_readable</Badge>}
|
|
326
|
-
<span style={{
|
|
327
|
-
flex: 1, color: T.textMuted, fontSize: 11, textAlign: 'right',
|
|
328
|
-
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
329
|
-
}}>{entry.description}</span>
|
|
330
|
-
</div>
|
|
331
|
-
))}
|
|
332
|
-
</div>
|
|
333
|
-
);
|
|
289
|
+
return <CredsManager path={project.path} projectName={project.name} />;
|
|
334
290
|
}
|
|
335
291
|
|
|
336
292
|
function DrillSessions({ project }) {
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
// ─── Hub credentials: global vault + cross-project manager ─────
|
|
2
|
+
// CredsManager is the shared manager (also used by the drill Credentials tab);
|
|
3
|
+
// HubCredentials is the top-level mainView='creds' page.
|
|
4
|
+
// Write-only wire: values are submitted inbound-only and never returned by
|
|
5
|
+
// any hub route — rows show length + fingerprint, never the secret itself.
|
|
6
|
+
|
|
7
|
+
const HUB_CREDS_EMPTY_FORM = {
|
|
8
|
+
name: '', value: '', scope: 'project', type: 'token',
|
|
9
|
+
description: '', env_var: '', agent_readable: false, inject: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// path=null → the global vault (~/.c3): scope locked to 'global'.
|
|
13
|
+
// path=string → that project's merged view (global entries + project shadows).
|
|
14
|
+
function CredsManager({ path, projectName, onChanged }) {
|
|
15
|
+
const isGlobal = !path;
|
|
16
|
+
const [entries, setEntries] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState('');
|
|
19
|
+
const [busy, setBusy] = useState(false);
|
|
20
|
+
const [form, setForm] = useState(null); // null = closed; {…} = create/edit
|
|
21
|
+
const [editing, setEditing] = useState(false);
|
|
22
|
+
const [checks, setChecks] = useState({}); // name -> {resolvable, fingerprint}
|
|
23
|
+
const [importOpen, setImportOpen] = useState(false);
|
|
24
|
+
const [importText, setImportText] = useState('');
|
|
25
|
+
const [importScope, setImportScope] = useState(isGlobal ? 'global' : 'project');
|
|
26
|
+
|
|
27
|
+
const withPath = (obj) => (path ? Object.assign({ path }, obj) : obj);
|
|
28
|
+
const pathQuery = path ? '&path=' + encodeURIComponent(path) : '';
|
|
29
|
+
|
|
30
|
+
const load = useCallback(async () => {
|
|
31
|
+
try {
|
|
32
|
+
if (path) {
|
|
33
|
+
const data = await api.get('/api/projects/credentials?path=' + encodeURIComponent(path));
|
|
34
|
+
setEntries((data && data.entries) || []);
|
|
35
|
+
} else {
|
|
36
|
+
const data = await api.get('/api/hub/credentials/overview');
|
|
37
|
+
setEntries((((data || {}).global) || {}).entries || []);
|
|
38
|
+
}
|
|
39
|
+
setError('');
|
|
40
|
+
} catch (e) { setError(String(e)); }
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}, [path]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => { setLoading(true); setChecks({}); load(); }, [load]);
|
|
45
|
+
|
|
46
|
+
const done = (msg) => { notify(msg); load(); if (onChanged) onChanged(); };
|
|
47
|
+
|
|
48
|
+
const saveForm = async () => {
|
|
49
|
+
if (!form || !form.name.trim()) return;
|
|
50
|
+
setBusy(true);
|
|
51
|
+
try {
|
|
52
|
+
const payload = withPath({
|
|
53
|
+
name: form.name.trim(), scope: isGlobal ? 'global' : form.scope,
|
|
54
|
+
type: form.type, description: form.description, env_var: form.env_var,
|
|
55
|
+
agent_readable: !!form.agent_readable, inject: !!form.inject,
|
|
56
|
+
});
|
|
57
|
+
if (form.value) payload.value = form.value; // blank on edit = keep stored value
|
|
58
|
+
const resp = await api.post('/api/projects/credentials', payload);
|
|
59
|
+
if (resp && resp.error) { setError(resp.error); }
|
|
60
|
+
else {
|
|
61
|
+
setForm(null);
|
|
62
|
+
done(`Saved '${payload.name}' (${payload.scope})`);
|
|
63
|
+
}
|
|
64
|
+
} catch (e) { setError(String(e)); }
|
|
65
|
+
setBusy(false);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const removeEntry = async (entry) => {
|
|
69
|
+
if (!window.confirm(`Delete credential '${entry.name}' (${entry.scope})? The stored value is destroyed.`)) return;
|
|
70
|
+
setBusy(true);
|
|
71
|
+
try {
|
|
72
|
+
await api.del(`/api/projects/credentials/${encodeURIComponent(entry.name)}?scope=${entry.scope}${pathQuery}`);
|
|
73
|
+
done(`Deleted '${entry.name}'`);
|
|
74
|
+
} catch (e) { setError(String(e)); }
|
|
75
|
+
setBusy(false);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const checkEntry = async (entry) => {
|
|
79
|
+
try {
|
|
80
|
+
const data = await api.post(
|
|
81
|
+
`/api/projects/credentials/${encodeURIComponent(entry.name)}/check`, withPath({}));
|
|
82
|
+
setChecks(prev => Object.assign({}, prev, { [entry.name]: data }));
|
|
83
|
+
} catch (e) { setError(String(e)); }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const toggleFlag = async (entry, field) => {
|
|
87
|
+
if (field === 'agent_readable' && !entry.agent_readable) {
|
|
88
|
+
if (!window.confirm(
|
|
89
|
+
`Enable agent_readable for '${entry.name}'?\n\nThe agent will be able to read this value into its context ` +
|
|
90
|
+
'and conversation transcripts. Keep it off to allow injection-only use.'
|
|
91
|
+
)) return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const resp = await api.post('/api/projects/credentials', withPath({
|
|
95
|
+
name: entry.name, scope: entry.scope, [field]: !entry[field],
|
|
96
|
+
}));
|
|
97
|
+
if (resp && resp.error) setError(resp.error); else { load(); if (onChanged) onChanged(); }
|
|
98
|
+
} catch (e) { setError(String(e)); }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const runImport = async () => {
|
|
102
|
+
if (!importText.trim()) return;
|
|
103
|
+
setBusy(true);
|
|
104
|
+
try {
|
|
105
|
+
const resp = await api.post('/api/projects/credentials/import',
|
|
106
|
+
withPath({ text: importText, scope: isGlobal ? 'global' : importScope }));
|
|
107
|
+
if (resp && resp.error) setError(resp.error);
|
|
108
|
+
else {
|
|
109
|
+
setImportText(''); setImportOpen(false);
|
|
110
|
+
done(`Imported ${(resp.created || []).length}, skipped ${(resp.skipped || []).length}`);
|
|
111
|
+
}
|
|
112
|
+
} catch (e) { setError(String(e)); }
|
|
113
|
+
setBusy(false);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const inputStyle = drillFieldStyle({ width: '100%', boxSizing: 'border-box' });
|
|
117
|
+
const labelStyle = { fontSize: 11, color: T.textMuted, marginBottom: 4, display: 'block' };
|
|
118
|
+
|
|
119
|
+
const openCreate = () => {
|
|
120
|
+
setForm(Object.assign({}, HUB_CREDS_EMPTY_FORM, isGlobal ? { scope: 'global' } : {}));
|
|
121
|
+
setEditing(false);
|
|
122
|
+
};
|
|
123
|
+
const openEdit = (entry) => {
|
|
124
|
+
setForm({
|
|
125
|
+
name: entry.name, value: '', scope: entry.scope, type: entry.type || 'token',
|
|
126
|
+
description: entry.description || '', env_var: entry.env_var || '',
|
|
127
|
+
agent_readable: !!entry.agent_readable, inject: !!entry.inject,
|
|
128
|
+
});
|
|
129
|
+
setEditing(true);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const fmtWhen = (iso) => iso ? String(iso).replace('T', ' ').replace(/\+.*$/, '') : '—';
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="fade-up">
|
|
136
|
+
{/* Toolbar */}
|
|
137
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
|
138
|
+
<span style={{ fontSize: 12, color: T.textMuted }}>
|
|
139
|
+
{isGlobal ? 'Shared vault — entries visible in every C3 project.'
|
|
140
|
+
: `Merged view for ${projectName || 'this project'} — project entries shadow same-named globals.`}
|
|
141
|
+
</span>
|
|
142
|
+
<div style={{ flex: 1 }} />
|
|
143
|
+
<button className="btn" onClick={() => setImportOpen(!importOpen)} style={{
|
|
144
|
+
background: T.surfaceAlt, color: T.text, border: `1px solid ${T.border}`,
|
|
145
|
+
padding: '5px 11px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
|
146
|
+
}}>Import .env</button>
|
|
147
|
+
<button className="btn" onClick={openCreate} style={{
|
|
148
|
+
background: T.accent, color: '#fff', border: 'none',
|
|
149
|
+
padding: '5px 11px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
|
150
|
+
}}>+ Add credential</button>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{error && (
|
|
154
|
+
<div style={{
|
|
155
|
+
padding: '8px 12px', borderRadius: 6, marginBottom: 10, fontSize: 12,
|
|
156
|
+
background: `${T.error}22`, color: T.error, border: `1px solid ${T.error}55`,
|
|
157
|
+
}}>{error}</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* .env import */}
|
|
161
|
+
{importOpen && (
|
|
162
|
+
<div style={{
|
|
163
|
+
border: `1px solid ${T.border}`, borderRadius: 8, padding: 12,
|
|
164
|
+
marginBottom: 12, background: T.surface,
|
|
165
|
+
}}>
|
|
166
|
+
<span style={labelStyle}>Paste KEY=VALUE lines (comments and `export` prefixes are tolerated)</span>
|
|
167
|
+
<textarea rows={5} value={importText} onChange={e => setImportText(e.target.value)}
|
|
168
|
+
style={Object.assign({}, inputStyle, { fontFamily: 'monospace', resize: 'vertical' })}
|
|
169
|
+
autoComplete="off" spellCheck={false} />
|
|
170
|
+
<div style={{ display: 'flex', gap: 10, marginTop: 8, alignItems: 'center' }}>
|
|
171
|
+
{!isGlobal && (
|
|
172
|
+
<select value={importScope} onChange={e => setImportScope(e.target.value)}
|
|
173
|
+
style={Object.assign({}, inputStyle, { width: 150 })}>
|
|
174
|
+
<option value="project">project scope</option>
|
|
175
|
+
<option value="global">global scope</option>
|
|
176
|
+
</select>
|
|
177
|
+
)}
|
|
178
|
+
<button className="btn" disabled={busy} onClick={runImport} style={{
|
|
179
|
+
background: T.accent, color: '#fff', border: 'none',
|
|
180
|
+
padding: '5px 13px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
|
181
|
+
}}>Import</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{/* Create / edit form */}
|
|
187
|
+
{form && (
|
|
188
|
+
<div style={{
|
|
189
|
+
border: `1px solid ${T.accent}55`, borderRadius: 8, padding: 12,
|
|
190
|
+
marginBottom: 12, background: T.surface,
|
|
191
|
+
}}>
|
|
192
|
+
<div style={{ fontSize: 13, fontWeight: 600, color: T.text, marginBottom: 10 }}>
|
|
193
|
+
{editing ? `Edit '${form.name}'` : (isGlobal ? 'New global credential' : 'New credential')}
|
|
194
|
+
</div>
|
|
195
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
|
196
|
+
<div>
|
|
197
|
+
<span style={labelStyle}>Name (env-var safe)</span>
|
|
198
|
+
<input value={form.name} disabled={editing}
|
|
199
|
+
onChange={e => setForm(Object.assign({}, form, { name: e.target.value }))}
|
|
200
|
+
style={inputStyle} autoComplete="off" spellCheck={false} />
|
|
201
|
+
</div>
|
|
202
|
+
<div>
|
|
203
|
+
<span style={labelStyle}>Scope</span>
|
|
204
|
+
{isGlobal ? (
|
|
205
|
+
<div style={Object.assign({}, inputStyle, { color: T.textMuted })}>global (all C3 projects)</div>
|
|
206
|
+
) : (
|
|
207
|
+
<select value={form.scope} disabled={editing}
|
|
208
|
+
onChange={e => setForm(Object.assign({}, form, { scope: e.target.value }))}
|
|
209
|
+
style={inputStyle}>
|
|
210
|
+
<option value="project">project (this project only)</option>
|
|
211
|
+
<option value="global">global (all C3 projects)</option>
|
|
212
|
+
</select>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
<div style={{ gridColumn: '1 / -1' }}>
|
|
216
|
+
<span style={labelStyle}>
|
|
217
|
+
Value{editing ? ' (leave blank to keep the stored value)' : ''}
|
|
218
|
+
</span>
|
|
219
|
+
{form.type === 'multiline' ? (
|
|
220
|
+
<textarea rows={4} value={form.value}
|
|
221
|
+
onChange={e => setForm(Object.assign({}, form, { value: e.target.value }))}
|
|
222
|
+
style={Object.assign({}, inputStyle, { fontFamily: 'monospace', resize: 'vertical' })}
|
|
223
|
+
autoComplete="new-password" spellCheck={false} />
|
|
224
|
+
) : (
|
|
225
|
+
<input type="password" value={form.value}
|
|
226
|
+
onChange={e => setForm(Object.assign({}, form, { value: e.target.value }))}
|
|
227
|
+
style={inputStyle} autoComplete="new-password" />
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
<div>
|
|
231
|
+
<span style={labelStyle}>Type</span>
|
|
232
|
+
<select value={form.type}
|
|
233
|
+
onChange={e => setForm(Object.assign({}, form, { type: e.target.value }))}
|
|
234
|
+
style={inputStyle}>
|
|
235
|
+
<option value="token">token — single secret</option>
|
|
236
|
+
<option value="env">env — env-style value</option>
|
|
237
|
+
<option value="multiline">multiline — .env blob / PEM</option>
|
|
238
|
+
</select>
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
241
|
+
<span style={labelStyle}>Env var at injection (default: name)</span>
|
|
242
|
+
<input value={form.env_var}
|
|
243
|
+
onChange={e => setForm(Object.assign({}, form, { env_var: e.target.value }))}
|
|
244
|
+
style={inputStyle} autoComplete="off" spellCheck={false} />
|
|
245
|
+
</div>
|
|
246
|
+
<div style={{ gridColumn: '1 / -1' }}>
|
|
247
|
+
<span style={labelStyle}>Description</span>
|
|
248
|
+
<input value={form.description}
|
|
249
|
+
onChange={e => setForm(Object.assign({}, form, { description: e.target.value }))}
|
|
250
|
+
style={inputStyle} autoComplete="off" />
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div style={{ display: 'flex', gap: 18, marginTop: 10, fontSize: 12, color: T.text }}>
|
|
254
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
|
255
|
+
<input type="checkbox" checked={!!form.inject}
|
|
256
|
+
onChange={e => setForm(Object.assign({}, form, { inject: e.target.checked }))} />
|
|
257
|
+
auto-inject into every c3_shell run
|
|
258
|
+
</label>
|
|
259
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
|
260
|
+
<input type="checkbox" checked={!!form.agent_readable}
|
|
261
|
+
onChange={e => setForm(Object.assign({}, form, { agent_readable: e.target.checked }))} />
|
|
262
|
+
agent_readable
|
|
263
|
+
</label>
|
|
264
|
+
</div>
|
|
265
|
+
{form.agent_readable && (
|
|
266
|
+
<div style={{
|
|
267
|
+
marginTop: 8, padding: '6px 10px', borderRadius: 6, fontSize: 11,
|
|
268
|
+
background: `${T.warn}22`, color: T.warn, border: `1px solid ${T.warn}55`,
|
|
269
|
+
}}>
|
|
270
|
+
⚠ The agent will be able to reveal this value into its context and
|
|
271
|
+
conversation transcripts. Leave off for injection-only use.
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
<div style={{ display: 'flex', gap: 10, marginTop: 12 }}>
|
|
275
|
+
<button className="btn" disabled={busy || !form.name.trim() || (!editing && !form.value)}
|
|
276
|
+
onClick={saveForm} style={{
|
|
277
|
+
background: T.accent, color: '#fff', border: 'none',
|
|
278
|
+
padding: '5px 15px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
|
279
|
+
opacity: (busy || !form.name.trim() || (!editing && !form.value)) ? 0.5 : 1,
|
|
280
|
+
}}>Save</button>
|
|
281
|
+
<button className="btn" onClick={() => setForm(null)} style={{
|
|
282
|
+
background: T.surfaceAlt, color: T.text, border: `1px solid ${T.border}`,
|
|
283
|
+
padding: '5px 15px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
|
284
|
+
}}>Cancel</button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{/* Entry list */}
|
|
290
|
+
{loading ? (
|
|
291
|
+
<div style={{ color: T.textMuted, fontSize: 13 }}>Loading…</div>
|
|
292
|
+
) : entries.length === 0 ? (
|
|
293
|
+
<div style={{
|
|
294
|
+
border: `1px dashed ${T.border}`, borderRadius: 8, padding: 26,
|
|
295
|
+
textAlign: 'center', color: T.textMuted, fontSize: 13,
|
|
296
|
+
}}>
|
|
297
|
+
{isGlobal
|
|
298
|
+
? <span>No global credentials yet. Add one here or run{' '}
|
|
299
|
+
<span className="mono">c3 creds set NAME --global</span>.</span>
|
|
300
|
+
: <span>No credentials registered for this project.</span>}
|
|
301
|
+
</div>
|
|
302
|
+
) : (
|
|
303
|
+
<div style={{ border: `1px solid ${T.border}`, borderRadius: 8, overflow: 'hidden' }}>
|
|
304
|
+
{entries.map((entry, i) => {
|
|
305
|
+
const chk = checks[entry.name];
|
|
306
|
+
const shadowedIn = entry.shadowed_in || [];
|
|
307
|
+
return (
|
|
308
|
+
<div key={`${entry.scope}|${entry.name}`} style={{
|
|
309
|
+
display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8,
|
|
310
|
+
padding: '8px 12px', borderTop: i === 0 ? 'none' : `1px solid ${T.border}`,
|
|
311
|
+
background: i % 2 ? T.surfaceAlt : T.surface, fontSize: 12,
|
|
312
|
+
}}>
|
|
313
|
+
<I name="lock" size={13} color={T.textMuted} />
|
|
314
|
+
<span className="mono" style={{ fontWeight: 600, color: T.text, minWidth: 120 }}>
|
|
315
|
+
{entry.name}
|
|
316
|
+
</span>
|
|
317
|
+
<Badge color={entry.scope === 'global' ? T.accent : T.ok}>{entry.scope}</Badge>
|
|
318
|
+
{!!entry.shadows_global && (
|
|
319
|
+
<span title="This project's value wins over the global entry of the same name">
|
|
320
|
+
<Badge color={T.warn}>shadows global</Badge>
|
|
321
|
+
</span>
|
|
322
|
+
)}
|
|
323
|
+
{shadowedIn.length > 0 && (
|
|
324
|
+
<span title={'Shadowed by a project-scoped entry in: '
|
|
325
|
+
+ shadowedIn.map(s => s.name || s.path).join(', ')}>
|
|
326
|
+
<Badge color={T.warn}>shadowed in {shadowedIn.length}</Badge>
|
|
327
|
+
</span>
|
|
328
|
+
)}
|
|
329
|
+
<Badge color={T.textMuted}>{entry.type || 'token'}</Badge>
|
|
330
|
+
<span className="mono" style={{ color: T.textMuted }}>
|
|
331
|
+
•••• len={entry.value_len}
|
|
332
|
+
</span>
|
|
333
|
+
{entry.env_var && (
|
|
334
|
+
<span className="mono" style={{ color: T.textMuted }}>→ ${entry.env_var}</span>
|
|
335
|
+
)}
|
|
336
|
+
{!!entry.inject && <Badge color={T.warn}>inject</Badge>}
|
|
337
|
+
{!!entry.agent_readable && <Badge color={T.error}>agent_readable</Badge>}
|
|
338
|
+
{chk && (
|
|
339
|
+
<span className="mono" style={{
|
|
340
|
+
color: chk.resolvable ? T.ok : T.error, fontSize: 11,
|
|
341
|
+
}}>
|
|
342
|
+
{chk.resolvable ? `✓ ${chk.fingerprint}` : '✗ unresolvable'}
|
|
343
|
+
</span>
|
|
344
|
+
)}
|
|
345
|
+
<div style={{ flex: 1 }} />
|
|
346
|
+
<span style={{ color: T.textMuted, fontSize: 11 }}>
|
|
347
|
+
used {entry.use_count || 0}× · {fmtWhen(entry.last_used)}
|
|
348
|
+
</span>
|
|
349
|
+
<span title="Verify the value resolves" onClick={() => checkEntry(entry)}
|
|
350
|
+
style={{ cursor: 'pointer', color: T.textMuted }}>
|
|
351
|
+
<I name="refresh" size={13} />
|
|
352
|
+
</span>
|
|
353
|
+
<span title={entry.inject ? 'Disable auto-inject' : 'Auto-inject into every c3_shell run'}
|
|
354
|
+
onClick={() => toggleFlag(entry, 'inject')}
|
|
355
|
+
style={{ cursor: 'pointer', color: entry.inject ? T.warn : T.textMuted }}>
|
|
356
|
+
<I name="zap" size={13} />
|
|
357
|
+
</span>
|
|
358
|
+
<span title={entry.agent_readable
|
|
359
|
+
? 'Revoke agent reveal access'
|
|
360
|
+
: 'Allow the agent to reveal this value (into its context!)'}
|
|
361
|
+
onClick={() => toggleFlag(entry, 'agent_readable')}
|
|
362
|
+
style={{ cursor: 'pointer', color: entry.agent_readable ? T.error : T.textMuted }}>
|
|
363
|
+
<I name="eye" size={13} />
|
|
364
|
+
</span>
|
|
365
|
+
<span title="Edit metadata / replace value" onClick={() => openEdit(entry)}
|
|
366
|
+
style={{ cursor: 'pointer', color: T.textMuted }}>
|
|
367
|
+
<I name="edit" size={13} />
|
|
368
|
+
</span>
|
|
369
|
+
<span title="Delete" onClick={() => removeEntry(entry)}
|
|
370
|
+
style={{ cursor: 'pointer', color: T.error }}>
|
|
371
|
+
<I name="trash" size={13} />
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
})}
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Top-level mainView='creds' page: Global vault | Projects sub-tabs.
|
|
383
|
+
function HubCredentials({ projects, onOpenDrill }) {
|
|
384
|
+
const [sub, setSub] = useState('global');
|
|
385
|
+
const [ov, setOv] = useState(null);
|
|
386
|
+
const [ovErr, setOvErr] = useState(null);
|
|
387
|
+
const [expanded, setExpanded] = useState({}); // path -> bool
|
|
388
|
+
|
|
389
|
+
const loadOverview = async () => {
|
|
390
|
+
setOvErr(null);
|
|
391
|
+
try { setOv(await api.get('/api/hub/credentials/overview')); }
|
|
392
|
+
catch (e) { setOvErr(e.message); }
|
|
393
|
+
};
|
|
394
|
+
useEffect(() => { if (sub === 'projects' && !ov) loadOverview(); }, [sub, ov]);
|
|
395
|
+
|
|
396
|
+
const norm = (s) => String(s || '').replace(/\\/g, '/').toLowerCase();
|
|
397
|
+
const findProject = (row) => (projects || []).find(p => norm(p.path) === norm(row.path));
|
|
398
|
+
|
|
399
|
+
const subBtn = (id, label) => (
|
|
400
|
+
<button key={id} onClick={() => setSub(id)} style={{
|
|
401
|
+
display: 'inline-flex', alignItems: 'center', gap: 6, height: 28,
|
|
402
|
+
padding: '0 12px', border: 'none', cursor: 'pointer', fontSize: 12,
|
|
403
|
+
background: sub === id ? T.accentDim : 'transparent',
|
|
404
|
+
color: sub === id ? T.accent : T.textMuted,
|
|
405
|
+
fontWeight: sub === id ? 700 : 400,
|
|
406
|
+
}}>{label}</button>
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div className="fade-up" style={{ maxWidth: 1100 }}>
|
|
411
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
|
412
|
+
<I name="lock" size={18} color={T.accent} />
|
|
413
|
+
<span style={{ fontSize: 15, fontWeight: 600, color: T.text }}>Credentials</span>
|
|
414
|
+
<div style={{
|
|
415
|
+
display: 'inline-flex', marginLeft: 10, border: `1px solid ${T.border}`,
|
|
416
|
+
borderRadius: 6, overflow: 'hidden',
|
|
417
|
+
}}>
|
|
418
|
+
{subBtn('global', 'Global vault')}
|
|
419
|
+
{subBtn('projects', 'Projects')}
|
|
420
|
+
</div>
|
|
421
|
+
<div style={{ flex: 1 }} />
|
|
422
|
+
{sub === 'projects' && (
|
|
423
|
+
<button className="btn" onClick={loadOverview} title="Refresh" style={{
|
|
424
|
+
background: T.surfaceAlt, color: T.text, border: `1px solid ${T.border}`,
|
|
425
|
+
padding: '5px 11px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
|
426
|
+
}}>Refresh</button>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
<div style={{ fontSize: 11, color: T.textMuted, marginBottom: 14, lineHeight: 1.5 }}>
|
|
430
|
+
Values live in the OS keyring (large values in an encrypted sidecar), are
|
|
431
|
+
submitted inbound-only and are <b>never</b> returned by any hub route. Agents
|
|
432
|
+
use them by name via <span className="mono">c3_shell env_creds</span> or{' '}
|
|
433
|
+
<span className="mono">{'{{cred:NAME}}'}</span> — decoded only at the subprocess
|
|
434
|
+
boundary. <b>Global</b> entries are visible in every C3 project;{' '}
|
|
435
|
+
<b>project</b> entries shadow same-named globals in their project.
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{sub === 'global' ? (
|
|
439
|
+
<CredsManager path={null} onChanged={() => setOv(null)} />
|
|
440
|
+
) : ovErr ? (
|
|
441
|
+
<div style={{
|
|
442
|
+
padding: '8px 12px', borderRadius: 6, fontSize: 12,
|
|
443
|
+
background: `${T.error}22`, color: T.error, border: `1px solid ${T.error}55`,
|
|
444
|
+
}}>Failed to load overview: {ovErr}</div>
|
|
445
|
+
) : !ov ? (
|
|
446
|
+
<div style={{ color: T.textMuted, fontSize: 13 }}>Loading…</div>
|
|
447
|
+
) : (ov.projects || []).length === 0 ? (
|
|
448
|
+
<div style={{
|
|
449
|
+
border: `1px dashed ${T.border}`, borderRadius: 8, padding: 26,
|
|
450
|
+
textAlign: 'center', color: T.textMuted, fontSize: 13,
|
|
451
|
+
}}>No projects registered in the hub.</div>
|
|
452
|
+
) : (
|
|
453
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
454
|
+
{(ov.projects || []).map(row => {
|
|
455
|
+
const open = !!expanded[row.path];
|
|
456
|
+
const proj = findProject(row);
|
|
457
|
+
const muted = !row.initialized || row.error;
|
|
458
|
+
return (
|
|
459
|
+
<div key={row.path} style={{
|
|
460
|
+
border: `1px solid ${T.border}`, borderRadius: 8,
|
|
461
|
+
background: T.surface, overflow: 'hidden',
|
|
462
|
+
}}>
|
|
463
|
+
<div onClick={() => !muted && setExpanded(prev =>
|
|
464
|
+
Object.assign({}, prev, { [row.path]: !open }))}
|
|
465
|
+
style={{
|
|
466
|
+
display: 'flex', alignItems: 'center', gap: 10, padding: '9px 14px',
|
|
467
|
+
cursor: muted ? 'default' : 'pointer', opacity: muted ? 0.55 : 1,
|
|
468
|
+
fontSize: 12,
|
|
469
|
+
}}>
|
|
470
|
+
<span style={{ color: T.textMuted, fontSize: 10, width: 10 }}>
|
|
471
|
+
{muted ? '' : (open ? '▾' : '▸')}
|
|
472
|
+
</span>
|
|
473
|
+
<span style={{ fontWeight: 600, color: T.text }}>{row.name || row.path}</span>
|
|
474
|
+
<span className="mono" style={{
|
|
475
|
+
color: T.textDim, fontSize: 11, overflow: 'hidden',
|
|
476
|
+
textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 340,
|
|
477
|
+
}}>{row.path}</span>
|
|
478
|
+
<div style={{ flex: 1 }} />
|
|
479
|
+
{row.error ? (
|
|
480
|
+
<Badge color={T.error}>error</Badge>
|
|
481
|
+
) : !row.initialized ? (
|
|
482
|
+
<Badge color={T.textMuted}>not initialized</Badge>
|
|
483
|
+
) : (
|
|
484
|
+
<React.Fragment>
|
|
485
|
+
<span style={{ color: T.textMuted, fontSize: 11 }}>
|
|
486
|
+
{row.entries.length} project {row.entries.length === 1 ? 'entry' : 'entries'}
|
|
487
|
+
{row.entries.filter(e => e.shadows_global).length > 0 &&
|
|
488
|
+
` · ${row.entries.filter(e => e.shadows_global).length} shadowing global`}
|
|
489
|
+
</span>
|
|
490
|
+
{proj && onOpenDrill && (
|
|
491
|
+
<button className="btn" onClick={(e) => { e.stopPropagation(); onOpenDrill(proj, 'creds'); }}
|
|
492
|
+
style={{
|
|
493
|
+
background: T.surfaceAlt, color: T.text, border: `1px solid ${T.border}`,
|
|
494
|
+
padding: '3px 9px', borderRadius: 6, fontSize: 11, cursor: 'pointer',
|
|
495
|
+
}}>Open drill</button>
|
|
496
|
+
)}
|
|
497
|
+
</React.Fragment>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
{row.error && (
|
|
501
|
+
<div style={{ padding: '0 14px 9px 34px', fontSize: 11, color: T.error }}>{row.error}</div>
|
|
502
|
+
)}
|
|
503
|
+
{open && !muted && (
|
|
504
|
+
<div style={{ padding: '4px 14px 14px', borderTop: `1px solid ${T.border}` }}>
|
|
505
|
+
<CredsManager path={row.path} projectName={row.name}
|
|
506
|
+
onChanged={loadOverview} />
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
})}
|
|
512
|
+
</div>
|
|
513
|
+
)}
|
|
514
|
+
</div>
|
|
515
|
+
);
|
|
516
|
+
}
|
cli/hub_ui/components/topbar.js
CHANGED
|
@@ -38,7 +38,7 @@ function TopBar({ version, activeCount, darkMode, hubConfig, mainView, setMainVi
|
|
|
38
38
|
display: 'inline-flex', marginLeft: 10, border: `1px solid ${T.border}`,
|
|
39
39
|
borderRadius: 6, overflow: 'hidden',
|
|
40
40
|
}}>
|
|
41
|
-
{[['projects', 'Projects', 'layers'], ['board', 'Tasks', 'check']].map(([id, label, icon]) => (
|
|
41
|
+
{[['projects', 'Projects', 'layers'], ['board', 'Tasks', 'check'], ['creds', 'Credentials', 'lock']].map(([id, label, icon]) => (
|
|
42
42
|
<button key={id} onClick={() => setMainView(id)} style={{
|
|
43
43
|
display: 'inline-flex', alignItems: 'center', gap: 6, height: 28,
|
|
44
44
|
padding: '0 12px', border: 'none', cursor: 'pointer', fontSize: 12,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.59.0
|
|
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
|
|
@@ -358,6 +358,14 @@ global secrets (realm-atomic resolution, tested), cross-project shells run
|
|
|
358
358
|
with credentials disabled, and the vault is hard-excluded from the Oracle
|
|
359
359
|
Discovery API.
|
|
360
360
|
|
|
361
|
+
Since **v2.59.0** the Hub has a top-level **Credentials** view: manage the
|
|
362
|
+
global vault (`~/.c3`) and every registered project's entries from one place —
|
|
363
|
+
create / edit / delete / `.env` import / flag toggles, with shadowing shown
|
|
364
|
+
both ways ("shadows global" on project entries, "shadowed in N projects" on
|
|
365
|
+
globals). The write-only wire contract extends to the hub: values are
|
|
366
|
+
submitted inbound-only and **no hub route ever returns a stored value** (there
|
|
367
|
+
is no `reveal` on the hub at all).
|
|
368
|
+
|
|
361
369
|
### Jira — Cloud + Data Center (v2.56.0)
|
|
362
370
|
|
|
363
371
|
`c3_jira` connects to Jira Cloud (REST v3, email + API token) or self-hosted
|
|
@@ -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=aa7hBkW_gt95oqoy4xQhChHtOdL7cmamdrL_cCrMgX0,322126
|
|
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
|
|
@@ -18,7 +18,7 @@ cli/hook_read.py,sha256=ZWZldgqGSNz78COhlgETQ7yxdOW5vWIIHUet9gZKhxk,8176
|
|
|
18
18
|
cli/hook_session_stats.py,sha256=hZiX0r1SLOstu1PXvygWae0cIqHCcC3mb0SbFL6nCic,2083
|
|
19
19
|
cli/hook_terse_advisor.py,sha256=W_zaTfX2akgX-ifuUTsLu2H1atM8XmWt6jpPoeU7Pm0,6252
|
|
20
20
|
cli/hub.html,sha256=Hl-XPZGT1mMiKrbX9c5OsEw6mXEumwIB3vp1WlWaplM,183966
|
|
21
|
-
cli/hub_server.py,sha256=
|
|
21
|
+
cli/hub_server.py,sha256=jfBcqiQlcmwPMmMdNHbaN52l7Qzq-mMkDiy_JI6-Vs0,124815
|
|
22
22
|
cli/hub_ui.html,sha256=nnGqWhWGCLfNisGUnoAlQFcBr6wPNsq-AEOR7uFtVkE,2942
|
|
23
23
|
cli/mcp_proxy.py,sha256=92htuT-p0j-cDTbyqlIJpGoQ85_Aw7UuB8L_Toi_u20,17511
|
|
24
24
|
cli/mcp_server.py,sha256=mt9ED7TKWttX5yV6fyiRSPAkx0_RcelyTENr67sgCcg,45144
|
|
@@ -35,9 +35,9 @@ cli/guide/getting-started.html,sha256=5iS-_iAP1PboU9pFbMsfh8C9Du9usqOfIyawOc-X4H
|
|
|
35
35
|
cli/guide/index.html,sha256=mUDUBX590TTappvrKT7_eXi0P8G-3YvHjGk9pCcwWzQ,21350
|
|
36
36
|
cli/guide/oracle.html,sha256=3HSyPB25mH83IwUeKjetykSOK0jwcui7OJkFoJLLWjg,22214
|
|
37
37
|
cli/guide/shared.css,sha256=Mmm7W6aYxrkDq2RPOcyXiQFYGR-yB3auwX1uIWi6C74,15967
|
|
38
|
-
cli/guide/tools.html,sha256=
|
|
38
|
+
cli/guide/tools.html,sha256=F1Qd38Q4UnSVMcfxJDtDQEfVrHmhK2aqSqKtNNx-jGs,79614
|
|
39
39
|
cli/guide/workflow.html,sha256=gFUHUAVES4EG5nB3GARqQz_f62qvHCNfNsX4pvSgcPs,59044
|
|
40
|
-
cli/hub_ui/app.js,sha256=
|
|
40
|
+
cli/hub_ui/app.js,sha256=5W54CwLr60HL-HmKXT2GK8xFzLedZyMqWDYGDY9VtP0,6368
|
|
41
41
|
cli/hub_ui/state.js,sha256=nlZtBU6WNJCMLiHRuj6AX5vkwWDvrcVjnK0oSaQALqI,6081
|
|
42
42
|
cli/hub_ui/components/add_project.js,sha256=j5m0mX0gePcEYQW_0vodJkw5v8Vn2lQqPAI0miIGdv4,3182
|
|
43
43
|
cli/hub_ui/components/config_editor.js,sha256=xew4RoxFt3j3r0UTkVc3-7y2Z1cOjx3dgRZMKSsCFhg,18211
|
|
@@ -46,8 +46,9 @@ cli/hub_ui/components/drill_health.js,sha256=Jpe-53gbN0WiV8Ao58WATk6UwT_yer85lvc
|
|
|
46
46
|
cli/hub_ui/components/drill_panel.js,sha256=y7m52_3c6fs_RuZGyx3_gWcLWSVsNuCKlfkGv8HHXlI,8495
|
|
47
47
|
cli/hub_ui/components/drill_subprojects.js,sha256=wdPNTIMrrLLQI5Qxi3xk-rZFMDO8pFT_HnbfzI4RCOI,23452
|
|
48
48
|
cli/hub_ui/components/drill_tasks.js,sha256=1iG8Kx9vCoCIHFDXCVtGiw_HwYff3SuScM90tj0cFRQ,14245
|
|
49
|
-
cli/hub_ui/components/drill_views.js,sha256=
|
|
49
|
+
cli/hub_ui/components/drill_views.js,sha256=XtWP6CohPfmw0ZQoUANx4l77pz0_K6FS1XwgxsZNXYU,15397
|
|
50
50
|
cli/hub_ui/components/global_search.js,sha256=GPfq1EsZa73vXBNCVn4VhT3a7gYkaUblbcXOFCe7PJQ,11395
|
|
51
|
+
cli/hub_ui/components/hub_credentials.js,sha256=807HpYffR_uqkRExxLjfX8ziox_3jwoCq1bc1tC4LkY,24798
|
|
51
52
|
cli/hub_ui/components/mcp_manager.js,sha256=KxA0LlpyNFDgtGxajoqwViJ0pJ0lsa1PqdCMNyyfhCo,17368
|
|
52
53
|
cli/hub_ui/components/modals.js,sha256=3maxEVYIg4McqM-RiOQyr0wz0U08_plGifxVHw2CShQ,52437
|
|
53
54
|
cli/hub_ui/components/project_card.js,sha256=PBHtRmh-bocIDkd_qlJmMoxQqQYTcDFFGmjmeSI560E,27219
|
|
@@ -58,7 +59,7 @@ cli/hub_ui/components/sidebar.js,sha256=RH0OlqCINFNCIXgns-wgge5gG8vkn5wyfRT87qdm
|
|
|
58
59
|
cli/hub_ui/components/summary_bar.js,sha256=JnMq01Oum8DwwUOhFnX5mr8K7I-ERdPlbaBOR8QT72o,2993
|
|
59
60
|
cli/hub_ui/components/task_board.js,sha256=QiWKmj1NkhkloGlfzZj_tWd7Sgz3xSxwVLp8I5OfIiM,15952
|
|
60
61
|
cli/hub_ui/components/toasts.js,sha256=caj_x3npcM_wMv9E_xEde88jyfXI-jt8RrDQW_yXz5k,3376
|
|
61
|
-
cli/hub_ui/components/topbar.js,sha256=
|
|
62
|
+
cli/hub_ui/components/topbar.js,sha256=vgQpNB77iJRjd5KTX3BJwvqN0pwd46tiVpKFaMUFoIU,4379
|
|
62
63
|
cli/tools/__init__.py,sha256=HO6eVKzDm1KPqsKTHY6lfIsnFnv3C7Bd9o3Q_V88idg,127
|
|
63
64
|
cli/tools/_helpers.py,sha256=Yo_ol8bFT2R5ayjT54pXLVZhW_PgpBVoM8I7pDoomCI,7823
|
|
64
65
|
cli/tools/agent.py,sha256=fdt5kmw9O-AEKlYaij-PPmCJ4sI7bmdej56V4NGWaqI,48145
|
|
@@ -100,7 +101,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
100
101
|
cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
|
|
101
102
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
102
103
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
103
|
-
code_context_control-2.
|
|
104
|
+
code_context_control-2.59.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
104
105
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
105
106
|
core/config.py,sha256=YmkcZwedz_lfDM0ZuI_f4xpe98ixPLcSQFcTCHgRiRg,19206
|
|
106
107
|
core/ide.py,sha256=V6VVMVsFdmmcsMyxikjQp7z9xa42CWiHKS-ya-MAcG4,6172
|
|
@@ -161,7 +162,7 @@ services/claude_md.py,sha256=Mh5qiWGNCcA1rKpl1lPC3PtcCaNxMMFMjhpacohOIcs,44001
|
|
|
161
162
|
services/compressor.py,sha256=AK31TqHFpkOD_Lqbp9aEOkOx7zENUwpsQHQ9B2imEHo,33081
|
|
162
163
|
services/context_snapshot.py,sha256=2f_bxY3xX4ZC653ncYno8YSeSTFSavwBjrorytHA0Ns,16879
|
|
163
164
|
services/conversation_store.py,sha256=-E2H6f9pMSOHiBjsq7tFUjrFAwWYExf7LJkibi5MRMA,35650
|
|
164
|
-
services/credential_store.py,sha256=
|
|
165
|
+
services/credential_store.py,sha256=4LAolC_fBjFgW-Lg8yMpoRwFh0dwi-raP1UMw5z3YbA,23270
|
|
165
166
|
services/doc_index.py,sha256=ZEQD1kyGCAMm9AY4Kpi1zDWJTul-cAcAdi_4Md43Hpw,19205
|
|
166
167
|
services/e2e_benchmark.py,sha256=4pDXkOFjq5b2ZwDvMK0CWdUXQli5OE4cOh_OJs7W36c,132970
|
|
167
168
|
services/e2e_evaluator.py,sha256=FsB4vMzcFUcmicfIwRtA8JZ_XeMpuk2_-86F2iVNEDc,17783
|
|
@@ -237,8 +238,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
237
238
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
238
239
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
239
240
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
240
|
-
code_context_control-2.
|
|
241
|
-
code_context_control-2.
|
|
242
|
-
code_context_control-2.
|
|
243
|
-
code_context_control-2.
|
|
244
|
-
code_context_control-2.
|
|
241
|
+
code_context_control-2.59.0.dist-info/METADATA,sha256=--W3obwi4LV8oMlP8tLSGAarOEPTsuPDA8Qe0nG3qJ0,30130
|
|
242
|
+
code_context_control-2.59.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
243
|
+
code_context_control-2.59.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
244
|
+
code_context_control-2.59.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
245
|
+
code_context_control-2.59.0.dist-info/RECORD,,
|
services/credential_store.py
CHANGED
|
@@ -98,6 +98,11 @@ def _global_base() -> Optional[Path]:
|
|
|
98
98
|
return None
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def global_base() -> Optional[Path]:
|
|
102
|
+
"""Public accessor for the global-scope base directory (home)."""
|
|
103
|
+
return _global_base()
|
|
104
|
+
|
|
105
|
+
|
|
101
106
|
def _project_is_home(project_path: str) -> bool:
|
|
102
107
|
home = _global_base()
|
|
103
108
|
if home is None:
|
|
File without changes
|
{code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.58.0.dist-info → code_context_control-2.59.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|