Skip to content

Commit eb4740e

Browse files
authored
Merge pull request #106 from agentveil-protocol/codex/public-role-visibility-foundation
feat(mcp-proxy): surface role presets
2 parents c43f31e + f663e90 commit eb4740e

3 files changed

Lines changed: 166 additions & 4 deletions

File tree

packages/agentveil-mcp-proxy/agentveil_mcp_proxy/cli.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,7 @@ def doctor_proxy(
13941394
)
13951395
for warning in warnings:
13961396
print(f"WARN: {warning}", file=out)
1397+
_print_role_presets_discoverability_hint(out)
13971398
return 0
13981399
except ProxyCliError as exc:
13991400
if output_json:
@@ -2798,6 +2799,16 @@ def _with_path_args(parts: list[str]) -> str:
27982799
return doctor_cmd, client_cmd
27992800

28002801

2802+
_ROLE_PRESETS_DISCOVERABILITY_HINT = (
2803+
"Role presets: available. Inspect with "
2804+
"`agentveil-mcp-proxy role doctor --preset reviewer`."
2805+
)
2806+
2807+
2808+
def _print_role_presets_discoverability_hint(out: TextIO) -> None:
2809+
print(_ROLE_PRESETS_DISCOVERABILITY_HINT, file=out)
2810+
2811+
28012812
def _add_passphrase_args(parser: argparse.ArgumentParser) -> None:
28022813
parser.add_argument("--passphrase", default=None, help="MCP proxy identity passphrase")
28032814
parser.add_argument("--passphrase-file", type=Path, default=None, help="Read passphrase from file")
@@ -3538,7 +3549,7 @@ def run_redirect_approval_demo_cli(
35383549
"\n"
35393550
"Advanced:\n"
35403551
" approval-center, client-config, client-doctor, client-run, configure-downstream,\n"
3541-
" connect, control, disconnect, downstream, evidence-summary, explain, hook,\n"
3552+
" connect, control, disconnect, downstream, evidence-summary, explain, hook, role,\n"
35423553
" install-claude-hook, permission-doctor, reissue-grant, register, smoke,\n"
35433554
" status-claude-hook, templates, uninstall-claude-hook, wizard\n"
35443555
"\n"
@@ -4187,6 +4198,35 @@ def build_parser() -> argparse.ArgumentParser:
41874198
)
41884199
_add_json_arg(explain_role)
41894200

4201+
role = subparsers.add_parser(
4202+
"role",
4203+
help="Inspect static role preset action-boundary behavior",
4204+
description=(
4205+
"Inspect what a role preset may read, block, or require approval for. "
4206+
"Roles map to action-boundary decisions; they are not inferred policy."
4207+
),
4208+
formatter_class=argparse.RawDescriptionHelpFormatter,
4209+
epilog=(
4210+
"Examples:\n"
4211+
" agentveil-mcp-proxy role doctor --preset reviewer\n"
4212+
" agentveil-mcp-proxy explain role --preset reviewer"
4213+
),
4214+
)
4215+
role_subparsers = role.add_subparsers(dest="role_action", required=True)
4216+
role_doctor = role_subparsers.add_parser(
4217+
"doctor",
4218+
# claim-check: allow "blocked" is bounded role-doctor action-family status text.
4219+
help="Show allowed, blocked, and approval-required action families by role preset",
4220+
)
4221+
_add_common_path_args(role_doctor)
4222+
role_doctor.add_argument(
4223+
"--preset",
4224+
choices=list(ROLE_PRESET_NAMES),
4225+
default=None,
4226+
help="Inspect one preset; default reads role_preset from config or the preset set",
4227+
)
4228+
_add_json_arg(role_doctor)
4229+
41904230
templates = subparsers.add_parser(
41914231
"templates",
41924232
help="Print copy-paste runnable starter commands for review/build/readonly agents",
@@ -6479,6 +6519,7 @@ def main(argv: list[str] | None = None) -> int:
64796519
)
64806520
print(f"Next: {doctor_cmd}")
64816521
print(f" {client_cmd}")
6522+
_print_role_presets_discoverability_hint(sys.stdout)
64826523
return 0
64836524
if args.command == "doctor":
64846525
return doctor_proxy(
@@ -6784,6 +6825,15 @@ def main(argv: list[str] | None = None) -> int:
67846825
preset=args.preset,
67856826
output_json=args.json_output,
67866827
)
6828+
if args.command == "role":
6829+
if args.role_action != "doctor":
6830+
raise ProxyCliError("role action must be doctor")
6831+
return explain_role_proxy(
6832+
home=args.home,
6833+
config_path=args.config,
6834+
preset=args.preset,
6835+
output_json=args.json_output,
6836+
)
67876837
if args.command == "templates":
67886838
if args.templates_action != "print":
67896839
raise ProxyCliError("templates action must be print")

packages/agentveil-mcp-proxy/agentveil_mcp_proxy/role_doctor.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,10 @@ def build_role_preset_guide(preset_name: str) -> RolePresetGuide:
403403
approval_required_action_families=POLICY_APPROVAL_ACTION_FAMILIES,
404404
blocked_action_families=MUTATION_ACTION_FAMILIES,
405405
summary=(
406-
f"{_preset_label(preset.name)} may read and inspect tools; "
406+
f"{_preset_label(preset.name)} may read and inspect; "
407407
# claim-check: allow "blocked" is bounded role-doctor status text.
408-
"mutation and command actions are blocked by role authority."
408+
"file changes and shell commands are blocked at the action boundary. "
409+
"Hand implementation work to an Implementer or Build agent."
409410
),
410411
)
411412
return RolePresetGuide(
@@ -453,7 +454,11 @@ def format_role_doctor_report(report: Mapping[str, Any]) -> str:
453454
"""Render human-readable role doctor output."""
454455

455456
if "presets" in report:
456-
lines = ["Role doctor: preset capabilities", ""]
457+
lines = [
458+
"Role doctor: static action-boundary map for role presets.",
459+
"Roles provide context for allow, approval, and deny decisions.",
460+
"",
461+
]
457462
for preset_report in report["presets"]:
458463
lines.append(format_role_doctor_report(preset_report))
459464
lines.append("")
@@ -471,6 +476,10 @@ def format_role_doctor_report(report: Mapping[str, Any]) -> str:
471476
# claim-check: allow "Blocked" is the literal bounded role-doctor label.
472477
f"Blocked action families: {', '.join(report['blocked_action_families']) or 'none'}",
473478
f"Summary: {report['summary']}",
479+
(
480+
"Action boundary: role authority helps decide whether each tool call "
481+
"is allowed, needs approval, or is blocked."
482+
),
474483
]
475484
return "\n".join(lines)
476485

packages/agentveil-mcp-proxy/tests/test_mcp_proxy_cli.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,3 +3109,106 @@ def capture_center_start(**kwargs):
31093109
assert rc == 0
31103110
assert captured["proxy_command"] == sys.executable
31113111
_assert_demo_managed_approval_center_stopped(tmp_path / "demo-source-route" / "avp-home")
3112+
3113+
3114+
_ROLE_DOCTOR_FORBIDDEN_WORDING = (
3115+
"AgentVeil learned",
3116+
"inferred role",
3117+
)
3118+
3119+
3120+
def _assert_role_doctor_reviewer_output(output: str) -> None:
3121+
assert "Preset: reviewer" in output
3122+
assert "Allowed action families" in output
3123+
assert "Blocked action families" in output
3124+
assert "Action boundary:" in output
3125+
assert "Implementer" in output
3126+
assert "Build agent" in output
3127+
lowered = output.lower()
3128+
for forbidden in _ROLE_DOCTOR_FORBIDDEN_WORDING:
3129+
assert forbidden.lower() not in lowered
3130+
assert "adaptive recommendation" not in lowered
3131+
assert "before local policy" not in lowered
3132+
3133+
3134+
def test_role_doctor_reviewer_preset_shows_action_boundary(capsys):
3135+
assert main(["role", "doctor", "--preset", "reviewer"]) == 0
3136+
_assert_role_doctor_reviewer_output(capsys.readouterr().out)
3137+
3138+
3139+
def test_explain_role_reviewer_still_works(capsys):
3140+
assert main(["explain", "role", "--preset", "reviewer"]) == 0
3141+
_assert_role_doctor_reviewer_output(capsys.readouterr().out)
3142+
3143+
3144+
def test_role_doctor_and_explain_role_reviewer_match(capsys):
3145+
assert main(["role", "doctor", "--preset", "reviewer"]) == 0
3146+
role_output = capsys.readouterr().out
3147+
assert main(["explain", "role", "--preset", "reviewer"]) == 0
3148+
explain_output = capsys.readouterr().out
3149+
assert role_output == explain_output
3150+
3151+
3152+
def test_role_doctor_help_is_discoverable(capsys):
3153+
with pytest.raises(SystemExit) as exc:
3154+
main(["role", "doctor", "--help"])
3155+
assert exc.value.code == 0
3156+
text = capsys.readouterr().out
3157+
assert "--preset" in text
3158+
assert "reviewer" in text
3159+
3160+
3161+
def test_root_help_lists_role_doctor(capsys):
3162+
with pytest.raises(SystemExit) as exc:
3163+
main(["--help"])
3164+
assert exc.value.code == 0
3165+
text = capsys.readouterr().out
3166+
advanced = text.split("Advanced:", 1)[1].split("Typical first-run path:", 1)[0]
3167+
assert "role" in advanced
3168+
3169+
3170+
def test_doctor_human_output_includes_role_presets_hint(tmp_path):
3171+
home = tmp_path / "avp-home"
3172+
init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
3173+
out = io.StringIO()
3174+
assert doctor_proxy(home=home, passphrase=TEST_PASSPHRASE, out=out) == 0
3175+
text = out.getvalue()
3176+
assert proxy_cli._ROLE_PRESETS_DISCOVERABILITY_HINT in text
3177+
assert "inferred" not in text.lower()
3178+
assert "adaptive" not in text.lower()
3179+
3180+
3181+
def test_doctor_json_output_omits_role_presets_hint(tmp_path):
3182+
home = tmp_path / "avp-home"
3183+
init_proxy(home=home, agent_name="proxy", passphrase=TEST_PASSPHRASE)
3184+
out = io.StringIO()
3185+
assert doctor_proxy(
3186+
home=home,
3187+
passphrase=TEST_PASSPHRASE,
3188+
output_json=True,
3189+
out=out,
3190+
) == 0
3191+
payload = json.loads(out.getvalue())
3192+
serialized = json.dumps(payload)
3193+
assert proxy_cli._ROLE_PRESETS_DISCOVERABILITY_HINT not in serialized
3194+
assert "role doctor" not in serialized
3195+
3196+
3197+
def test_init_human_output_includes_role_presets_hint(tmp_path, capsys):
3198+
home = tmp_path / "avp-home"
3199+
assert main(["init", "--home", str(home), "--plaintext", "--force"]) == 0
3200+
text = capsys.readouterr().out
3201+
assert proxy_cli._ROLE_PRESETS_DISCOVERABILITY_HINT in text
3202+
assert "Next:" in text
3203+
3204+
3205+
def test_demo_human_output_omits_role_presets_hint(tmp_path, capsys):
3206+
work_dir = tmp_path / "demo-no-role-hint"
3207+
assert main([
3208+
"demo",
3209+
"--work-dir",
3210+
str(work_dir),
3211+
"--demo-auto-approve",
3212+
]) == 0
3213+
text = capsys.readouterr().out
3214+
assert proxy_cli._ROLE_PRESETS_DISCOVERABILITY_HINT not in text

0 commit comments

Comments
 (0)