code-context-control 2.62.0__py3-none-any.whl → 2.63.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 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.62.0"
95
+ __version__ = "2.63.0"
96
96
 
97
97
 
98
98
  def _compress_file_cli(compressor, path, mode="smart", **kw):
@@ -4670,6 +4670,7 @@ back to native tools as the task progresses.
4670
4670
  - **Delegate**: `c3_delegate(task, backend='ollama|codex|gemini|claude|auto')` — offload to other models
4671
4671
  - **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.
4672
4672
  - **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`.
4673
+ - **Masked paths** (v2.63.0+): content prefixed `[c3-mask:transformed]` is a policy-transformed VIEW, not the file. Values may be synthetic, rows withheld, bodies stripped — don't copy literals out of it or infer completeness from it. Masked paths are read-only and refuse shell/git/validate/impact/filter/delegate (`[c3-mask:unsupported]`). That's policy, not a transient error — report the block instead of routing around it.
4673
4674
 
4674
4675
  ## Self-Check
4675
4676
  If you haven't called a c3_* tool in several turns during active development, re-engage
@@ -5918,7 +5919,8 @@ def cmd_access(args):
5918
5919
  """Access Guard rule management — human-only mutation surface (spec §1)."""
5919
5920
  sub = getattr(args, "access_cmd", None)
5920
5921
  if not sub:
5921
- print("Usage: c3 access {list,add,remove,check} [args]")
5922
+ print("Usage: c3 access {list,add,remove,check,mask} [args]")
5923
+ print(" c3 access mask {add,rm,status,activate,preview} [args]")
5922
5924
  return
5923
5925
 
5924
5926
  project_path = getattr(args, "project_path", ".") or "."
@@ -5931,6 +5933,8 @@ def cmd_access(args):
5931
5933
  _access_cmd_remove(args, project_path)
5932
5934
  elif sub == "check":
5933
5935
  _access_cmd_check(args, project_path)
5936
+ elif sub == "mask":
5937
+ _access_cmd_mask(args, project_path)
5934
5938
  else:
5935
5939
  print(f"Unknown access subcommand: {sub}")
5936
5940
 
@@ -5973,13 +5977,24 @@ def _access_cmd_list(args, project_path: str) -> None:
5973
5977
  for scope in ("builtin", "global", "project"):
5974
5978
  sec = scopes.get(scope) or {}
5975
5979
  rules = [(k, g) for k in ("deny", "read_only") for g in sec.get(k, [])]
5976
- print(f"[{scope}] ({notes[scope]}) — {len(rules)} rule(s)")
5980
+ masks = sec.get("mask") or []
5981
+ print(f"[{scope}] ({notes[scope]}) — {len(rules) + len(masks)} rule(s)")
5977
5982
  for kind, glob in rules:
5978
5983
  print(f" {kind:<10} {glob}")
5984
+ for entry in masks:
5985
+ params = entry.get("params") or {}
5986
+ detail = ", ".join(f"{k}={v}" for k, v in sorted(params.items()))
5987
+ print(f" {'mask':<10} {entry.get('glob')} -> "
5988
+ f"{entry.get('preset')}" + (f"({detail})" if detail else ""))
5979
5989
  if sec.get("corrupt"):
5980
5990
  print(" [warn] access section invalid — scope fails closed "
5981
5991
  "(deny-all); fix config.json 'access' by hand")
5982
5992
  print()
5993
+ from services import mask_activation
5994
+ summary = mask_activation.summary_line(project_path)
5995
+ if summary:
5996
+ print(summary)
5997
+ print()
5983
5998
  print(access_guard.COVERAGE_MATRIX)
5984
5999
 
5985
6000
 
@@ -6021,11 +6036,174 @@ def _access_cmd_check(args, project_path: str) -> None:
6021
6036
  from services import access_guard
6022
6037
 
6023
6038
  op = getattr(args, "op", "read") or "read"
6024
- denial = access_guard.check(args.target, op, project_path)
6025
- if denial is None:
6039
+ v = access_guard.verdict(args.target, op, project_path)
6040
+ if v.masked:
6041
+ rule = v.mask_rule
6042
+ print(f"[MASKED] {op} served as a transformed view: {args.target}")
6043
+ print(f" rule '{rule.glob}' ({rule.scope} scope) -> {rule.preset}")
6044
+ print(" Preview what the agent sees: "
6045
+ f"c3 access mask preview {args.target}")
6046
+ elif v.denial is None:
6026
6047
  print(f"[OK] {op} allowed: {args.target}")
6027
6048
  else:
6028
- print(access_guard.refusal(denial, args.target, op))
6049
+ print(access_guard.refusal(v.denial, args.target, op))
6050
+
6051
+
6052
+ # ── mask subcommands ────────────────────────────────────────────────────────
6053
+
6054
+ def _parse_mask_params(raw: str, preset: str) -> dict:
6055
+ """`count=20,strategy=first` / `columns=email,name` -> typed params."""
6056
+ from services import access_guard
6057
+
6058
+ schema = (access_guard.MASK_PRESETS.get(preset) or {}).get("params", {})
6059
+ params: dict = {}
6060
+ for chunk in (raw or "").split(","):
6061
+ chunk = chunk.strip()
6062
+ if not chunk:
6063
+ continue
6064
+ if "=" not in chunk:
6065
+ # bare value continues the previous list param (columns=a,b,c)
6066
+ last = next((k for k in reversed(list(params))
6067
+ if schema.get(k) is list), None)
6068
+ if last:
6069
+ params[last].append(chunk)
6070
+ continue
6071
+ raise ValueError(f"cannot parse param '{chunk}' — use key=value")
6072
+ key, value = (part.strip() for part in chunk.split("=", 1))
6073
+ expected = schema.get(key)
6074
+ if expected is int:
6075
+ params[key] = int(value)
6076
+ elif expected is list:
6077
+ params[key] = [value]
6078
+ else:
6079
+ params[key] = value
6080
+ return params
6081
+
6082
+
6083
+ def _access_cmd_mask_add(args, project_path: str) -> None:
6084
+ from services import access_guard, mask_activation
6085
+
6086
+ scope = _access_scope(args)
6087
+ try:
6088
+ params = _parse_mask_params(getattr(args, "params", "") or "",
6089
+ args.preset)
6090
+ result = access_guard.set_mask_rule(args.glob, args.preset, params,
6091
+ scope, project_path)
6092
+ except ValueError as exc:
6093
+ print(f"[error] {exc}")
6094
+ return
6095
+ _access_audit("mask-add", result["glob"], args.preset, scope, project_path)
6096
+ verb = "Replaced" if result["replaced"] else "Added"
6097
+ print(f"[OK] {verb} mask rule '{result['glob']}' -> {args.preset} "
6098
+ f"(scope={scope})")
6099
+ print()
6100
+ print(mask_activation.summary_line(project_path))
6101
+ print("Run `c3 access mask activate` to purge pre-mask derived artifacts.")
6102
+
6103
+
6104
+ def _access_cmd_mask_remove(args, project_path: str) -> None:
6105
+ from services import access_guard
6106
+
6107
+ scope = _access_scope(args)
6108
+ try:
6109
+ result = access_guard.remove_mask_rule(args.glob, scope, project_path)
6110
+ except ValueError as exc:
6111
+ print(f"[error] {exc}")
6112
+ return
6113
+ if result["removed"]:
6114
+ _access_audit("mask-remove", result["glob"], "mask", scope,
6115
+ project_path)
6116
+ print(f"[OK] Removed mask rule '{result['glob']}' (scope={scope})")
6117
+ else:
6118
+ print(f"[error] no mask rule matching '{result['glob']}' "
6119
+ f"in {scope} scope")
6120
+
6121
+
6122
+ def _access_cmd_mask_status(args, project_path: str) -> None:
6123
+ from services import mask_activation
6124
+
6125
+ st = mask_activation.status(project_path)
6126
+ print(f"rules : {st['rule_count']}")
6127
+ print(f"status : {st['status']}")
6128
+ print(f"stale : {st['stale']}")
6129
+ if st["corrupt_scopes"]:
6130
+ print(f"corrupt : {', '.join(st['corrupt_scopes'])}")
6131
+ report = st.get("last_report") or {}
6132
+ if report:
6133
+ print(f"last run : {report.get('views_built', 0)} view(s) built, "
6134
+ f"{len(report.get('failures') or [])} failure(s)")
6135
+ for failure in (report.get("failures") or [])[:10]:
6136
+ print(f" [fail] {failure['path']}: {failure['detail']}")
6137
+ print()
6138
+ print(mask_activation.summary_line(project_path) or "no mask rules")
6139
+
6140
+
6141
+ def _access_cmd_mask_activate(args, project_path: str) -> None:
6142
+ from services import mask_activation
6143
+
6144
+ print("Activating mask rules (purge -> build -> validate)...")
6145
+ report = mask_activation.activate(
6146
+ project_path, rebuild_index=bool(getattr(args, "reindex", False)))
6147
+ print(f" files matched : {report['files']}")
6148
+ print(f" views built : {report['views_built']}")
6149
+ print(f" cache entries wiped: {report['cache_entries_removed']}")
6150
+ print(f" index dropped : {report['index_dropped']}")
6151
+ print(f" file memory dropped: {report['file_memory_dropped']}")
6152
+ facts = report.get("facts") or {}
6153
+ print(f" facts purged : {facts.get('purged', 0)} "
6154
+ f"(+{facts.get('unknown_purged', 0)} unknown-provenance)")
6155
+ for failure in report.get("failures") or []:
6156
+ print(f" [fail] {failure['path']}: {failure['detail']}")
6157
+ _access_audit("mask-activate", f"{report['files']} file(s)", "mask",
6158
+ "project", project_path)
6159
+ print()
6160
+ print(mask_activation.summary_line(project_path))
6161
+
6162
+
6163
+ def _access_cmd_mask_preview(args, project_path: str) -> None:
6164
+ """Show exactly what the agent will see. Human surface only."""
6165
+ from pathlib import Path as _Path
6166
+
6167
+ from services import access_guard, mask_mirror
6168
+
6169
+ target = _Path(args.target)
6170
+ if not target.is_absolute():
6171
+ target = _Path(project_path) / args.target
6172
+ v = access_guard.verdict(str(target), "read", project_path)
6173
+ if v.denial:
6174
+ print(access_guard.refusal(v.denial, str(target), "read"))
6175
+ return
6176
+ if not v.masked:
6177
+ print(f"[=] Not masked: {args.target} — the agent sees the real file.")
6178
+ return
6179
+ try:
6180
+ view = mask_mirror.render_for_path(target, project_path)
6181
+ except mask_mirror.MaskUnavailable as exc:
6182
+ print(f"[error] {exc.message}")
6183
+ return
6184
+ print(f"rule : '{v.mask_rule.glob}' ({v.mask_rule.scope} scope)")
6185
+ print(f"preset : {v.mask_rule.preset} {v.mask_rule.params_dict or ''}")
6186
+ print(f"stats : {view.stats}")
6187
+ print("-" * 70)
6188
+ print(view.with_header(args.target))
6189
+
6190
+
6191
+ def _access_cmd_mask(args, project_path: str) -> None:
6192
+ sub = getattr(args, "mask_cmd", None) or "status"
6193
+ handlers = {
6194
+ "add": _access_cmd_mask_add,
6195
+ "rm": _access_cmd_mask_remove,
6196
+ "remove": _access_cmd_mask_remove,
6197
+ "status": _access_cmd_mask_status,
6198
+ "activate": _access_cmd_mask_activate,
6199
+ "preview": _access_cmd_mask_preview,
6200
+ }
6201
+ handler = handlers.get(sub)
6202
+ if handler is None:
6203
+ print(f"[error] unknown mask subcommand '{sub}' — expected one of: "
6204
+ f"{', '.join(sorted(handlers))}")
6205
+ return
6206
+ handler(args, project_path)
6029
6207
 
6030
6208
 
6031
6209
  def cmd_jira(args):
cli/commands/parser.py CHANGED
@@ -483,6 +483,48 @@ def build_parser(version: str, parse_cli_ide_arg):
483
483
  help="Operation to evaluate (default: read)")
484
484
  ac_check.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
485
485
 
486
+ # ── Mask Guard (v2.63.0, docs/mask-guard.md) ────────────────────────
487
+ # Masking exposes a path but transforms what the agent sees. Rules are
488
+ # human-only, like the rest of Access Guard.
489
+ ac_mask = access_subs.add_parser(
490
+ "mask", help="Mask Guard — expose a path but transform what the agent sees")
491
+ mask_subs = ac_mask.add_subparsers(dest="mask_cmd")
492
+
493
+ am_add = mask_subs.add_parser("add", help="Add or replace a mask rule")
494
+ am_add.add_argument("glob", help="POSIX glob, e.g. 'data/**' or '*.csv'")
495
+ am_add.add_argument("--preset", required=True,
496
+ choices=["redact_secrets", "redact_columns",
497
+ "sample_rows", "signatures_only"],
498
+ help="Deterministic transform applied to matching files")
499
+ am_add.add_argument("--params", default="",
500
+ help="Preset params, e.g. 'count=20,strategy=first' "
501
+ "or 'columns=email,name'")
502
+ am_add.add_argument("--global", dest="use_global", action="store_true",
503
+ help="Store in the global scope (~/.c3)")
504
+ am_add.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
505
+
506
+ am_rm = mask_subs.add_parser("rm", help="Remove a mask rule")
507
+ am_rm.add_argument("glob", help="The stored glob to remove")
508
+ am_rm.add_argument("--global", dest="use_global", action="store_true",
509
+ help="Remove from the global scope (~/.c3)")
510
+ am_rm.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
511
+
512
+ am_status = mask_subs.add_parser(
513
+ "status", help="Activation state — whether masking is actually in effect")
514
+ am_status.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
515
+
516
+ am_act = mask_subs.add_parser(
517
+ "activate",
518
+ help="Purge pre-mask derived artifacts, then build + validate views")
519
+ am_act.add_argument("--reindex", action="store_true",
520
+ help="Also rebuild the code index in the same pass")
521
+ am_act.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
522
+
523
+ am_prev = mask_subs.add_parser(
524
+ "preview", help="Show exactly what the agent sees for one path")
525
+ am_prev.add_argument("target", help="Path to preview (absolute or project-relative)")
526
+ am_prev.add_argument("--path", dest="project_path", default=".", help="Project directory (default: current)")
527
+
486
528
  # ── Oracle Discovery API (v2.32.0) ──────────────────────────────────
487
529
  p_oracle = subparsers.add_parser(
488
530
  "oracle",
cli/guide/access.html CHANGED
@@ -74,11 +74,12 @@
74
74
  <a href="tools.html">Tools</a>
75
75
  <a href="credentials.html">Credentials</a>
76
76
  <a href="access.html" class="active">Access</a>
77
+ <a href="masking.html">Masking</a>
77
78
  <a href="bitbucket.html">Bitbucket</a>
78
79
  <a href="oracle.html">Oracle</a>
79
80
  <a href="https://github.com/sponsors/drknowhow" target="_blank" rel="noopener" style="color:#EA4AAA">&#9829; Sponsor</a>
80
81
  </nav>
81
- <span class="header-version">v2.62.0</span>
82
+ <span class="header-version">v2.63.0</span>
82
83
  </header>
83
84
 
84
85
  <!-- Sidebar -->
cli/guide/bitbucket.html CHANGED
@@ -79,6 +79,7 @@
79
79
  <a href="tools.html">Tools</a>
80
80
  <a href="credentials.html">Credentials</a>
81
81
  <a href="access.html">Access</a>
82
+ <a href="masking.html">Masking</a>
82
83
  <a href="bitbucket.html" class="active">Bitbucket</a>
83
84
  <a href="oracle.html">Oracle</a>
84
85
  <a href="https://github.com/sponsors/drknowhow" target="_blank" rel="noopener" style="color:#EA4AAA">&#9829; Sponsor</a>
@@ -88,6 +88,7 @@
88
88
  <a href="tools.html">Tools</a>
89
89
  <a href="credentials.html" class="active">Credentials</a>
90
90
  <a href="access.html">Access</a>
91
+ <a href="masking.html">Masking</a>
91
92
  <a href="bitbucket.html">Bitbucket</a>
92
93
  <a href="oracle.html">Oracle</a>
93
94
  <a href="https://github.com/sponsors/drknowhow" target="_blank" rel="noopener" style="color:#EA4AAA">&#9829; Sponsor</a>
@@ -22,6 +22,7 @@
22
22
  <a href="tools.html">Tools</a>
23
23
  <a href="credentials.html">Credentials</a>
24
24
  <a href="access.html">Access</a>
25
+ <a href="masking.html">Masking</a>
25
26
  <a href="bitbucket.html">Bitbucket</a>
26
27
  <a href="oracle.html">Oracle</a>
27
28
  <a href="https://github.com/sponsors/drknowhow" target="_blank" rel="noopener" style="color:#EA4AAA">&#9829; Sponsor</a>
cli/guide/index.html CHANGED
@@ -224,6 +224,7 @@
224
224
  <a href="tools.html">Tools</a>
225
225
  <a href="credentials.html">Credentials</a>
226
226
  <a href="access.html">Access</a>
227
+ <a href="masking.html">Masking</a>
227
228
  <a href="bitbucket.html">Bitbucket</a>
228
229
  <a href="oracle.html">Oracle</a>
229
230
  <a href="https://github.com/sponsors/drknowhow" target="_blank" rel="noopener" style="color:#EA4AAA">&#9829; Sponsor</a>
@@ -262,6 +263,9 @@
262
263
  <a href="access.html" class="sidebar-link">
263
264
  <span class="icon">🛡️</span> Access Guard
264
265
  </a>
266
+ <a href="masking.html" class="sidebar-link">
267
+ <span class="icon">🎭</span> Mask Guard
268
+ </a>
265
269
  <a href="bitbucket.html" class="sidebar-link">
266
270
  <span class="icon">🪣</span> Bitbucket
267
271
  </a>
@@ -330,6 +334,11 @@
330
334
  <div class="nav-card-title">Access Guard</div>
331
335
  <div class="nav-card-desc">Paths agents must never read or write — deny/read-only globs enforced at every C3 surface, with honest coverage limits (v2.62.0+)</div>
332
336
  </a>
337
+ <a href="masking.html" class="nav-card">
338
+ <span class="nav-card-icon">🎭</span>
339
+ <div class="nav-card-title">Mask Guard</div>
340
+ <div class="nav-card-desc">Expose a path but control what the agent sees — redact secrets, pseudonymise columns, sample rows, signatures only. Deterministic, read-only, fail-closed (v2.63.0+)</div>
341
+ </a>
333
342
  <a href="bitbucket.html" class="nav-card">
334
343
  <span class="nav-card-icon">🪣</span>
335
344
  <div class="nav-card-title">Bitbucket Data Center</div>