code-context-control 2.136.0__py3-none-any.whl → 2.139.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 +15 -2
- cli/commands/parser.py +2 -1
- cli/guide/tools.html +11 -2
- cli/hook_access_guard.py +71 -3
- cli/mcp_server.py +5 -2
- cli/tools/delegate.py +373 -20
- cli/tools/status.py +3 -1
- {code_context_control-2.136.0.dist-info → code_context_control-2.139.0.dist-info}/METADATA +1 -1
- {code_context_control-2.136.0.dist-info → code_context_control-2.139.0.dist-info}/RECORD +19 -17
- core/config.py +13 -1
- services/bench/delegate_eval.py +34 -3
- services/bench/delegate_write_eval.py +370 -0
- services/claude_md.py +1 -1
- services/delegate_write.py +243 -0
- services/telemetry.py +5 -1
- {code_context_control-2.136.0.dist-info → code_context_control-2.139.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.136.0.dist-info → code_context_control-2.139.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.136.0.dist-info → code_context_control-2.139.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.136.0.dist-info → code_context_control-2.139.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
|
@@ -91,7 +91,7 @@ console = Console() if HAS_RICH else None
|
|
|
91
91
|
# Config
|
|
92
92
|
CONFIG_DIR = ".c3"
|
|
93
93
|
CONFIG_FILE = ".c3/config.json"
|
|
94
|
-
__version__ = "2.
|
|
94
|
+
__version__ = "2.139.0"
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
def _compress_file_cli(compressor, path, mode="smart", **kw):
|
|
@@ -1583,6 +1583,19 @@ def cmd_delegate_eval(args):
|
|
|
1583
1583
|
|
|
1584
1584
|
targets = [t.strip() for t in (args.targets or "").split(",") if t.strip()]
|
|
1585
1585
|
case_ids = [c.strip() for c in (args.cases or "").split(",") if c.strip()]
|
|
1586
|
+
if args.suite == "write":
|
|
1587
|
+
# Write mode (docs/delegate-write.md): graded on the files left behind.
|
|
1588
|
+
from services.bench import delegate_write_eval as dwe
|
|
1589
|
+
if not targets:
|
|
1590
|
+
raise RuntimeError("--suite write needs --targets (e.g. claude:small,claude:medium, or gold)")
|
|
1591
|
+
try:
|
|
1592
|
+
overrides = json.loads(args.config) if getattr(args, "config", None) else None
|
|
1593
|
+
report = dwe.run_write_suite(targets, case_ids=case_ids or None, record=args.record,
|
|
1594
|
+
delegate_overrides=overrides)
|
|
1595
|
+
except ValueError as e:
|
|
1596
|
+
raise RuntimeError(str(e))
|
|
1597
|
+
print(json.dumps(report, indent=2) if args.json else dwe.render(report))
|
|
1598
|
+
return
|
|
1586
1599
|
overrides = json.loads(args.config) if getattr(args, "config", None) else None
|
|
1587
1600
|
if overrides is not None and not isinstance(overrides, dict):
|
|
1588
1601
|
raise RuntimeError("--config must be a JSON object")
|
|
@@ -5065,7 +5078,7 @@ back to native tools as the task progresses.
|
|
|
5065
5078
|
- **Filter**: `c3_filter(text=...)` — for terminal output >10 lines
|
|
5066
5079
|
- **Shell**: `c3_shell(cmd, timeout=60)` — structured shell exec (tests/git/build). Auto-filters output, logs git mutations to the ledger. Native Bash for interactive/TTY only
|
|
5067
5080
|
- **Memory**: `c3_memory(action='recall')` — full recall. `index` + `fetch` for token-efficient two-step retrieval
|
|
5068
|
-
- **Delegate**: `c3_delegate(task, task_type, context|file_path)` — your own provider one tier down (default `backend='host'`, `tier='small'`; Claude Code: Haiku) for bounded answers: summarize a diff/log, explain a function, triage a traceback. `scout=true` lets it look files up itself. Multi-step work: the `c3-scout` (haiku) / `c3-worker` (sonnet) subagents
|
|
5081
|
+
- **Delegate**: `c3_delegate(task, task_type, context|file_path)` — your own provider one tier down (default `backend='host'`, `tier='small'`; Claude Code: Haiku) for bounded answers: summarize a diff/log, explain a function, triage a traceback. `scout=true` lets it look files up itself; `write_paths='a.py,b.py'` has Sonnet make a change you specified in only those files and return the diff. Multi-step work: the `c3-scout` (haiku) / `c3-worker` (sonnet) subagents
|
|
5069
5082
|
- **Local CI** (v2.79.0+): `c3_ci(action='inspect|run|rerun|failures')` — run THIS repo's real `.github/workflows` here instead of pushing for feedback. `run` executes in `needs` order; `failures` gives {file,line,message}; `rerun` retries only what failed. Only `FULL_CI_PASS` means safe to push — `PARTIAL_PASS` means something did not run (other OS, unsupported action, or your selection).
|
|
5070
5083
|
- **Bitbucket** (v2.30.0+, when `c3 bitbucket login` has run): `c3_bitbucket(action='list_prs|get_pr|merge_pr|...')` — self-hosted Bitbucket Data Center / Server. Token in OS keyring; mutating actions auto-log to the edit ledger.
|
|
5071
5084
|
- **Cross-project** (v2.31.0+): `c3_project(action='list|scan|search|read|edit|...', project='<name|path>')` — discover and operate on OTHER c3-installed projects. Reads run freely; writes (edit/shell/memory) need `allow_write=true`.
|
cli/commands/parser.py
CHANGED
|
@@ -92,7 +92,8 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
92
92
|
p_delegate_eval = subparsers.add_parser(
|
|
93
93
|
"delegate-eval", help="Grade c3_delegate answers per backend/tier (docs/delegate-eval.md)")
|
|
94
94
|
p_delegate_eval.add_argument("--suite", default="gold",
|
|
95
|
-
help="'gold' (bundled)
|
|
95
|
+
help="'gold' (bundled), 'write' (write mode, docs/delegate-write.md) "
|
|
96
|
+
"or a .jsonl path")
|
|
96
97
|
p_delegate_eval.add_argument("--targets", default="",
|
|
97
98
|
help="Comma-separated backend[:tier] list, e.g. 'claude:small,claude:medium'")
|
|
98
99
|
p_delegate_eval.add_argument("--cases", default="", help="Comma-separated case ids (default: all)")
|
cli/guide/tools.html
CHANGED
|
@@ -940,7 +940,7 @@ c3_status(view=<span class="str">'sessions'</span>) <span class="com">#
|
|
|
940
940
|
<td class="param-name">tier</td>
|
|
941
941
|
<td class="param-type">string</td>
|
|
942
942
|
<td class="param-default">"small"</td>
|
|
943
|
-
<td class="param-desc"><code>small</code> · <code>medium</code> · <code>large</code> · <code>default</code>. Claude: Haiku / Sonnet / Opus. Codex and Grok: reasoning effort low / medium / high on the account's model. Gemini: flash-lite / flash / pro.</td>
|
|
943
|
+
<td class="param-desc"><code>small</code> · <code>medium</code> · <code>large</code> · <code>default</code>. Claude: Haiku (thinking capped at 1024 tokens) / Sonnet / Opus; a scout defaults to Sonnet, which finds things in far fewer turns. Codex and Grok: reasoning effort low / medium / high on the account's model. Gemini: flash-lite / flash / pro.</td>
|
|
944
944
|
</tr>
|
|
945
945
|
<tr>
|
|
946
946
|
<td class="param-name">model</td>
|
|
@@ -954,6 +954,12 @@ c3_status(view=<span class="str">'sessions'</span>) <span class="com">#
|
|
|
954
954
|
<td class="param-default">false</td>
|
|
955
955
|
<td class="param-desc">Let the delegate Read/Grep/Glob the project itself, for lookups when you cannot name the files. Claude runs <code>--restricted</code> with Access Guard's read denies; Codex uses its read-only sandbox.</td>
|
|
956
956
|
</tr>
|
|
957
|
+
<tr>
|
|
958
|
+
<td class="param-name">write_paths</td>
|
|
959
|
+
<td class="param-type">string</td>
|
|
960
|
+
<td class="param-default">""</td>
|
|
961
|
+
<td class="param-desc">Write mode (Claude only, defaults to Sonnet). Comma-separated project-relative globs the delegate may edit or create. You decide the change; it makes it with Read/Grep/Glob/Edit/Write, no commands. You get back the diff, and each change is in the edit ledger. Access Guard applies; confirm-held, locked, <code>.git/</code> and <code>.c3/</code> paths are refused. Review the diff and run the tests yourself.</td>
|
|
962
|
+
</tr>
|
|
957
963
|
</tbody>
|
|
958
964
|
</table>
|
|
959
965
|
|
|
@@ -965,7 +971,10 @@ c3_delegate(task=<span class="str">'Summarize this diff for a changelog in three
|
|
|
965
971
|
c3_delegate(task=<span class="str">'What does resolve_host return inside a grok child?'</span>, file_path=<span class="str">'core/host.py'</span>, task_type=<span class="str">'explain'</span>, tier=<span class="str">'medium'</span>)
|
|
966
972
|
|
|
967
973
|
<span class="com"># A lookup the delegate does itself</span>
|
|
968
|
-
c3_delegate(task=<span class="str">'Which functions call charge_card, and where?'</span>, scout=<span class="kw">True</span>)
|
|
974
|
+
c3_delegate(task=<span class="str">'Which functions call charge_card, and where?'</span>, scout=<span class="kw">True</span>)
|
|
975
|
+
|
|
976
|
+
<span class="com"># A change you have decided, made by Sonnet inside a write set</span>
|
|
977
|
+
c3_delegate(task=<span class="str">'Rename calc_total to order_total in its definition and every use; no alias'</span>, write_paths=<span class="str">'inv/orders.py, inv/report.py, tests/test_orders.py'</span>)</code></pre>
|
|
969
978
|
|
|
970
979
|
<div class="callout callout-info">
|
|
971
980
|
<span class="callout-icon">ℹ️</span>
|
cli/hook_access_guard.py
CHANGED
|
@@ -503,14 +503,77 @@ def run(payload: dict, project_path: Path | None = None,
|
|
|
503
503
|
return None
|
|
504
504
|
|
|
505
505
|
|
|
506
|
+
def _worker_refusal(rel: str, why: str) -> dict:
|
|
507
|
+
return _deny(f"[c3-delegate:refused] {rel}: {why}. Do not write it another way; "
|
|
508
|
+
"leave it and say in your report what change it needed.")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def worker_run(payload: dict, project_path: Path, state_dir: Path) -> dict | None:
|
|
512
|
+
"""PreToolUse for a ``c3_delegate`` write-mode worker (see services/delegate_write).
|
|
513
|
+
|
|
514
|
+
Reads go through ``run`` exactly as for a scout. A write must pass, in
|
|
515
|
+
order: canonicalization inside the project, never ``.git/`` or ``.c3/``,
|
|
516
|
+
never the credential vault, Access Guard with NO grants and NO filed
|
|
517
|
+
requests (a confirm hold is a refusal here — a headless worker cannot
|
|
518
|
+
wait for a human, and the caller can make that edit itself), the caller's
|
|
519
|
+
write set, and another agent's lock. Then the pre-image is saved before
|
|
520
|
+
the write is allowed. Raises on a broken spec; main() turns that into a
|
|
521
|
+
deny.
|
|
522
|
+
"""
|
|
523
|
+
from services import delegate_write as dw
|
|
524
|
+
|
|
525
|
+
tool = normalize_tool_name(payload.get("tool_name", ""))
|
|
526
|
+
if tool not in _WRITE_TOOLS:
|
|
527
|
+
return run(payload, project_path)
|
|
528
|
+
spec = dw.load_spec(state_dir)
|
|
529
|
+
fp = _target(payload.get("tool_input", {}) or {})
|
|
530
|
+
if not fp:
|
|
531
|
+
return _deny("[c3-delegate:refused] a write with no target path")
|
|
532
|
+
base = str(project_path)
|
|
533
|
+
canon, rel, denial = ag.canonicalize(fp, base)
|
|
534
|
+
if denial:
|
|
535
|
+
return _worker_refusal(fp, f"path refused ({denial.reason})")
|
|
536
|
+
if not rel:
|
|
537
|
+
return _worker_refusal(fp, "outside the project")
|
|
538
|
+
if dw.forbidden(rel):
|
|
539
|
+
return _worker_refusal(rel, "under .git/ or .c3/, never delegate-writable")
|
|
540
|
+
try:
|
|
541
|
+
from services import credential_store as _cs
|
|
542
|
+
vault = _cs.vault_guard_reason(fp)
|
|
543
|
+
except Exception as exc: # fail closed: an unloadable vault check refuses
|
|
544
|
+
vault = f"vault check unavailable ({type(exc).__name__})"
|
|
545
|
+
if vault:
|
|
546
|
+
return _worker_refusal(rel, "credential vault file")
|
|
547
|
+
op = "write" if os.path.exists(fp) else "create"
|
|
548
|
+
denial = ag.check(fp, op, base)
|
|
549
|
+
if denial:
|
|
550
|
+
_record(denial, tool, op, fp, base, str(payload.get("session_id") or ""))
|
|
551
|
+
return _worker_refusal(rel, f"Access Guard {denial.kind} rule '{denial.rule}' "
|
|
552
|
+
f"({denial.scope})")
|
|
553
|
+
if not dw.in_write_set(rel, spec["write_paths"]):
|
|
554
|
+
return _worker_refusal(rel, "outside the write set the caller gave ("
|
|
555
|
+
+ ", ".join(spec["write_paths"]) + ")")
|
|
556
|
+
try:
|
|
557
|
+
from services import agent_locks
|
|
558
|
+
holder = agent_locks.check(fp, base, str(spec.get("session_id") or ""))
|
|
559
|
+
except Exception:
|
|
560
|
+
holder = None
|
|
561
|
+
if holder:
|
|
562
|
+
return _worker_refusal(rel, f"locked by {holder.get('agent_id', 'another agent')}")
|
|
563
|
+
dw.snapshot(state_dir, fp, canon)
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
|
|
506
567
|
def main() -> None:
|
|
507
|
-
"""Standalone PreToolUse entry for a ``c3_delegate`` scout run.
|
|
568
|
+
"""Standalone PreToolUse entry for a ``c3_delegate`` scout or worker run.
|
|
508
569
|
|
|
509
570
|
A scout is ``claude -p --restricted`` with Read/Grep/Glob and no C3 MCP
|
|
510
571
|
server, so the dispatcher's discipline hook (hook_pretool_enforce) would
|
|
511
572
|
refuse every native read. Only the access guard runs here. It prints a
|
|
512
573
|
deny and nothing else: grants and search footers are for an agent that
|
|
513
574
|
can act on them. Any failure denies (fail closed).
|
|
575
|
+
|
|
576
|
+
``--worker-state DIR``: a write-mode worker; see ``worker_run``.
|
|
514
577
|
"""
|
|
515
578
|
import argparse
|
|
516
579
|
import json
|
|
@@ -520,6 +583,7 @@ def main() -> None:
|
|
|
520
583
|
ensure_utf8_stdio()
|
|
521
584
|
parser = argparse.ArgumentParser()
|
|
522
585
|
parser.add_argument("--project", required=True)
|
|
586
|
+
parser.add_argument("--worker-state", default="")
|
|
523
587
|
args = parser.parse_args()
|
|
524
588
|
try:
|
|
525
589
|
raw = sys.stdin.read()
|
|
@@ -527,9 +591,13 @@ def main() -> None:
|
|
|
527
591
|
if not isinstance(payload, dict):
|
|
528
592
|
raise ValueError("hook payload must be a JSON object")
|
|
529
593
|
# access_guard canonicalizes the base itself (no direct resolve here).
|
|
530
|
-
|
|
594
|
+
if args.worker_state:
|
|
595
|
+
out = worker_run(payload, Path(args.project), Path(args.worker_state))
|
|
596
|
+
else:
|
|
597
|
+
out = run(payload, Path(args.project))
|
|
531
598
|
except Exception as exc:
|
|
532
|
-
|
|
599
|
+
who = "worker" if args.worker_state else "scout"
|
|
600
|
+
out = _deny(f"[c3-access:error] {who} guard failed ({type(exc).__name__}: {exc}); "
|
|
533
601
|
"refusing the call")
|
|
534
602
|
if isinstance(out, dict) and isinstance(out.get("hookSpecificOutput"), dict):
|
|
535
603
|
print(json.dumps({"hookSpecificOutput": out["hookSpecificOutput"]}))
|
cli/mcp_server.py
CHANGED
|
@@ -806,11 +806,13 @@ async def c3_delegate(task: str, task_type: str = "ask", context: str = "",
|
|
|
806
806
|
file_path: str = "", backend: str = "host",
|
|
807
807
|
allow_write_delegation: bool = False,
|
|
808
808
|
tier: str = "", model: str = "", scout: bool = False,
|
|
809
|
+
write_paths: str = "",
|
|
809
810
|
ctx: Context = None) -> str:
|
|
810
811
|
"""OFFLOAD to another model — use when the subtask is local-model-sized or needs a different perspective.
|
|
811
812
|
backend: host (default: your own provider, one tier down — Claude Code->claude, Codex->codex, Grok->grok, Antigravity->gemini) | claude|codex|gemini|grok|ollama|auto. task_type: auto, summarize, explain, review, ask, test, diagnose, ping (live auth check), available, codex_check, gemini_check, grok_check, codex_resume.
|
|
812
|
-
tier: small (default) | medium | large | default. claude: haiku/sonnet/opus; codex/grok: reasoning effort; gemini: flash-lite/flash/pro. model overrides the tier. Context + file_path are packed by C3 under Access Guard; a file over 8k tokens goes as its map, so for line-level questions on a big file paste the excerpt or use scout=true.
|
|
813
|
+
tier: small (default; scout=true defaults to medium) | medium | large | default. claude: haiku/sonnet/opus; codex/grok: reasoning effort; gemini: flash-lite/flash/pro. model overrides the tier. Context + file_path are packed by C3 under Access Guard; a file over 8k tokens goes as its map, so for line-level questions on a big file paste the excerpt or use scout=true.
|
|
813
814
|
scout=true: the delegate may Read/Grep/Glob the project itself (claude: read-only, guard-denied paths refused; codex: its read-only sandbox) — for lookups when you cannot name the files.
|
|
815
|
+
write_paths: comma-separated project-relative globs the delegate may edit or create (write mode; claude only, tier defaults to medium). You decide the change: say what to change, where, and the intended behaviour. It cannot run commands. You get back the diff (each change is in the edit ledger): review it and run the tests yourself. Access Guard applies; confirm-held, locked, .git/ and .c3/ paths are refused.
|
|
814
816
|
grok runs read-only in a temp dir unless delegate.grok_allow_write=true (then --yolo in the project, loading its trusted .grok MCP servers and hooks).
|
|
815
817
|
allow_write_delegation: explicit user opt-in for write-capable backends (gemini/grok write mode/codex_resume) while Access Guard rules are active; codex is pinned read-only instead."""
|
|
816
818
|
svc = _svc(ctx)
|
|
@@ -828,7 +830,8 @@ async def c3_delegate(task: str, task_type: str = "ask", context: str = "",
|
|
|
828
830
|
try:
|
|
829
831
|
return await asyncio.to_thread(handle_delegate, task, task_type, context,
|
|
830
832
|
file_path, svc, finalize, backend,
|
|
831
|
-
allow_write_delegation, tier, model, scout
|
|
833
|
+
allow_write_delegation, tier, model, scout,
|
|
834
|
+
write_paths)
|
|
832
835
|
finally:
|
|
833
836
|
svc._agent_progress_cb = None
|
|
834
837
|
|