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 +184 -6
- cli/commands/parser.py +42 -0
- cli/guide/access.html +2 -1
- cli/guide/bitbucket.html +1 -0
- cli/guide/credentials.html +1 -0
- cli/guide/getting-started.html +1 -0
- cli/guide/index.html +9 -0
- cli/guide/masking.html +400 -0
- cli/guide/oracle.html +1 -0
- cli/guide/tools.html +1 -0
- cli/guide/workflow.html +1 -0
- cli/server.py +112 -2
- cli/tools/compress.py +34 -6
- cli/tools/read.py +28 -3
- cli/tools/search.py +23 -10
- cli/ui/components/access.js +353 -10
- {code_context_control-2.62.0.dist-info → code_context_control-2.63.0.dist-info}/METADATA +1 -1
- {code_context_control-2.62.0.dist-info → code_context_control-2.63.0.dist-info}/RECORD +32 -28
- services/access_guard.py +419 -30
- services/auto_memory.py +25 -7
- services/claude_md.py +1 -0
- services/compressor.py +51 -2
- services/file_memory.py +23 -0
- services/indexer.py +40 -1
- services/mask_activation.py +295 -0
- services/mask_mirror.py +341 -0
- services/mask_presets.py +293 -0
- services/memory.py +71 -1
- {code_context_control-2.62.0.dist-info → code_context_control-2.63.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.62.0.dist-info → code_context_control-2.63.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.62.0.dist-info → code_context_control-2.63.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.62.0.dist-info → code_context_control-2.63.0.dist-info}/top_level.txt +0 -0
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.
|
|
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
|
-
|
|
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
|
-
|
|
6025
|
-
if
|
|
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">♥ Sponsor</a>
|
|
80
81
|
</nav>
|
|
81
|
-
<span class="header-version">v2.
|
|
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">♥ Sponsor</a>
|
cli/guide/credentials.html
CHANGED
|
@@ -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">♥ Sponsor</a>
|
cli/guide/getting-started.html
CHANGED
|
@@ -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">♥ 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">♥ 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>
|