Skip to content

Commit 59191c7

Browse files
authored
feat(guard): add dashboard shortcut (#179)
* feat(guard): add dashboard shortcut Signed-off-by: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> * fix(guard): report dashboard open state Signed-off-by: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> * fix(guard): refine dashboard launch flow Signed-off-by: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> * fix(guard): tighten dashboard open flow Signed-off-by: Michael Kantor <6068672+kantorcodes@users.noreply.github.com> --------- Signed-off-by: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
1 parent 136913f commit 59191c7

5 files changed

Lines changed: 230 additions & 6 deletions

File tree

src/codex_plugin_scanner/guard/cli/commands.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"Everyday protection:\n"
113113
" start First-run setup and the Guard operating loop\n"
114114
" status Current local protection state and next actions\n"
115+
" dashboard Open the local Guard dashboard in your browser\n"
115116
" run Enforce Guard before a harness launch\n"
116117
" approvals Resolve the current request queue\n"
117118
" receipts Review recent local decisions\n"
@@ -204,7 +205,7 @@ def _configure_guard_parser(guard_parser: argparse.ArgumentParser) -> None:
204205
required=True,
205206
parser_class=FriendlyArgumentParser,
206207
metavar=(
207-
"{start,status,bootstrap,detect,install,update,uninstall,run,protect,preflight,scan,diff,receipts,inventory,abom,"
208+
"{start,status,dashboard,bootstrap,detect,install,update,uninstall,run,protect,preflight,scan,diff,receipts,inventory,abom,"
208209
"approvals,explain,allow,deny,policies,exceptions,advisories,events,doctor,connect,login,sync,device,bridge}"
209210
),
210211
)
@@ -217,6 +218,17 @@ def _configure_guard_parser(guard_parser: argparse.ArgumentParser) -> None:
217218
_add_guard_common_args(status_parser)
218219
status_parser.add_argument("--json", action="store_true")
219220

221+
dashboard_parser = guard_subparsers.add_parser(
222+
"dashboard",
223+
help="Open the local Guard dashboard in your browser",
224+
)
225+
_add_guard_common_args(dashboard_parser)
226+
dashboard_parser.add_argument("--json", action="store_true")
227+
228+
admin_parser = guard_subparsers.add_parser("admin", help=argparse.SUPPRESS)
229+
_add_guard_common_args(admin_parser)
230+
admin_parser.add_argument("--json", action="store_true")
231+
220232
bootstrap_parser = guard_subparsers.add_parser(
221233
"bootstrap",
222234
help="Detect a harness, start the approval center, and install Guard for the best local target",
@@ -513,6 +525,7 @@ def _configure_guard_parser(guard_parser: argparse.ArgumentParser) -> None:
513525
hermes_mcp_proxy_parser.add_argument("--server", required=True)
514526
hermes_mcp_proxy_parser.add_argument("--stdio", action="store_true")
515527
hidden_commands = {
528+
"admin",
516529
"hook",
517530
"daemon",
518531
"codex-mcp-proxy",
@@ -660,6 +673,43 @@ def run_guard_command(
660673
_emit("status", payload, getattr(args, "json", False))
661674
return 0
662675

676+
if args.guard_command in {"dashboard", "admin"}:
677+
try:
678+
approval_center_url = ensure_guard_daemon(guard_home)
679+
except RuntimeError as error:
680+
if getattr(args, "json", False):
681+
_emit(
682+
"dashboard",
683+
{
684+
"generated_at": _now(),
685+
"opened": False,
686+
"error": str(error),
687+
},
688+
True,
689+
)
690+
else:
691+
print(str(error), file=sys.stderr)
692+
return 1
693+
open_result = _open_approval_center(
694+
approval_center_url,
695+
store=store,
696+
config=config,
697+
open_key="dashboard",
698+
force_open=True,
699+
)
700+
_emit(
701+
"dashboard",
702+
{
703+
"generated_at": _now(),
704+
"approval_center_url": approval_center_url,
705+
"browser_url": open_result.get("browser_url"),
706+
"opened": bool(open_result.get("opened")),
707+
"reason": str(open_result.get("reason") or "unknown"),
708+
},
709+
getattr(args, "json", False),
710+
)
711+
return 0
712+
663713
if args.guard_command == "bootstrap":
664714
try:
665715
payload = build_guard_bootstrap_payload(
@@ -3278,17 +3328,28 @@ def resolve(detection, payload):
32783328
return resolve
32793329

32803330

3281-
def _open_approval_center(approval_center_url: str, *, store: GuardStore, config, open_key: str | None = None) -> None:
3331+
def _open_approval_center(
3332+
approval_center_url: str,
3333+
*,
3334+
store: GuardStore,
3335+
config: GuardConfig,
3336+
open_key: str | None = None,
3337+
force_open: bool = False,
3338+
) -> dict[str, object]:
32823339
surface_runtime = GuardSurfaceRuntime(store)
32833340
auth_token = load_guard_daemon_auth_token(store.guard_home)
3284-
surface_runtime.ensure_surface(
3341+
browser_url = _approval_center_browser_url(approval_center_url, auth_token)
3342+
open_result = surface_runtime.ensure_surface(
32853343
surface="approval-center",
32863344
approval_center_url=approval_center_url,
3287-
browser_url=_approval_center_browser_url(approval_center_url, auth_token),
3345+
browser_url=browser_url,
32883346
approval_surface_policy=config.approval_surface_policy,
32893347
open_key=open_key or approval_center_url,
3348+
force_open=force_open,
32903349
opener=webbrowser.open,
32913350
)
3351+
open_result["browser_url"] = _public_approval_center_url(browser_url) or approval_center_url
3352+
return open_result
32923353

32933354

32943355
def _approval_center_browser_url(approval_center_url: str, auth_token: str | None) -> str | None:
@@ -3304,6 +3365,18 @@ def _approval_center_browser_url(approval_center_url: str, auth_token: str | Non
33043365
return urllib.parse.urlunparse(parsed._replace(fragment=urllib.parse.urlencode(fragment_pairs)))
33053366

33063367

3368+
def _public_approval_center_url(browser_url: str | None) -> str | None:
3369+
if browser_url is None:
3370+
return None
3371+
parsed = urllib.parse.urlparse(browser_url)
3372+
fragment_pairs = [
3373+
(key, value)
3374+
for key, value in urllib.parse.parse_qsl(parsed.fragment, keep_blank_values=True)
3375+
if key != "guard-token"
3376+
]
3377+
return urllib.parse.urlunparse(parsed._replace(fragment=urllib.parse.urlencode(fragment_pairs)))
3378+
3379+
33073380
def _approval_surface_policy_for_flow(config_policy: str, approval_flow: dict[str, object]) -> str:
33083381
if approval_flow.get("tier") != "approval-center":
33093382
return "notify-only"

src/codex_plugin_scanner/guard/cli/render.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,14 @@ def _render_connect(console: Console, payload: dict[str, object]) -> None:
654654
console.print(_build_steps_panel(_coerce_dict_list(payload.get("next_steps"))))
655655

656656

657+
def _render_dashboard(console: Console, payload: dict[str, object]) -> None:
658+
body = Table.grid(padding=(0, 1))
659+
body.add_row("Dashboard", str(payload.get("approval_center_url") or "unknown"))
660+
if payload.get("opened") is not None:
661+
body.add_row("Browser opened", _bool_label(bool(payload.get("opened"))))
662+
console.print(Panel(body, title="HOL Guard dashboard", border_style="cyan"))
663+
664+
657665
def _render_sync(console: Console, payload: dict[str, object]) -> None:
658666
body = Table.grid(padding=(0, 1))
659667
body.add_row("Synced at", str(payload.get("synced_at") or "unknown"))
@@ -1615,6 +1623,7 @@ def _clean_terminal_output(value: str) -> str:
16151623
"approvals": _render_approvals,
16161624
"start": _render_start,
16171625
"status": _render_status,
1626+
"dashboard": _render_dashboard,
16181627
"connect": _render_connect,
16191628
"bootstrap": _render_bootstrap,
16201629
"detect": _render_detect,

src/codex_plugin_scanner/guard/runtime/surface_server.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,15 @@ def ensure_surface(
332332
approval_surface_policy: str,
333333
open_key: str,
334334
opener: Callable[[str], object],
335+
force_open: bool = False,
335336
) -> dict[str, object]:
336-
if approval_surface_policy in {"notify-only", "never-auto-open"}:
337+
if approval_surface_policy in {"notify-only", "never-auto-open"} and not force_open:
337338
return {"surface": surface, "opened": False, "reason": "policy-disabled", "open_key": open_key}
338-
if approval_surface_policy == "auto-open-once" and self.has_surface_opened(surface, open_key):
339+
if (
340+
approval_surface_policy == "auto-open-once"
341+
and not force_open
342+
and self.has_surface_opened(surface, open_key)
343+
):
339344
return {"surface": surface, "opened": False, "reason": "already-opened", "open_key": open_key}
340345
if self.has_live_surface(surface):
341346
return {"surface": surface, "opened": False, "reason": "live-client", "open_key": open_key}

tests/test_guard_cli.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6048,6 +6048,100 @@ def complete_pairing() -> None:
60486048
assert connect_output["reason"] == "Guard sync requires a Pro or Team plan."
60496049
assert connect_output["sync_message"] == "Guard sync requires a Pro or Team plan."
60506050

6051+
def test_guard_dashboard_opens_local_approval_center(self, tmp_path, capsys, monkeypatch):
6052+
home_dir = tmp_path / "home"
6053+
opened_urls: list[str] = []
6054+
open_keys: list[str | None] = []
6055+
force_open_flags: list[bool] = []
6056+
6057+
monkeypatch.setattr(
6058+
guard_commands_module,
6059+
"ensure_guard_daemon",
6060+
lambda guard_home: "http://127.0.0.1:5474",
6061+
)
6062+
monkeypatch.setattr(
6063+
guard_commands_module,
6064+
"_open_approval_center",
6065+
lambda approval_center_url, *, store, config, open_key=None, force_open=False: (
6066+
opened_urls.append(approval_center_url),
6067+
open_keys.append(open_key),
6068+
force_open_flags.append(force_open),
6069+
{"opened": True, "reason": "opened", "browser_url": approval_center_url},
6070+
)[-1],
6071+
)
6072+
6073+
rc = main(["guard", "dashboard", "--home", str(home_dir), "--json"])
6074+
output = json.loads(capsys.readouterr().out)
6075+
6076+
assert rc == 0
6077+
assert opened_urls == ["http://127.0.0.1:5474"]
6078+
assert open_keys == ["dashboard"]
6079+
assert force_open_flags == [True]
6080+
assert output["approval_center_url"] == "http://127.0.0.1:5474"
6081+
assert output["browser_url"] == "http://127.0.0.1:5474"
6082+
assert output["opened"] is True
6083+
assert output["reason"] == "opened"
6084+
6085+
def test_guard_admin_alias_opens_local_approval_center(self, tmp_path, capsys, monkeypatch):
6086+
home_dir = tmp_path / "home"
6087+
opened_urls: list[str] = []
6088+
open_keys: list[str | None] = []
6089+
force_open_flags: list[bool] = []
6090+
6091+
monkeypatch.setattr(
6092+
guard_commands_module,
6093+
"ensure_guard_daemon",
6094+
lambda guard_home: "http://127.0.0.1:5474",
6095+
)
6096+
monkeypatch.setattr(
6097+
guard_commands_module,
6098+
"_open_approval_center",
6099+
lambda approval_center_url, *, store, config, open_key=None, force_open=False: (
6100+
opened_urls.append(approval_center_url),
6101+
open_keys.append(open_key),
6102+
force_open_flags.append(force_open),
6103+
{"opened": False, "reason": "policy-disabled", "browser_url": approval_center_url},
6104+
)[-1],
6105+
)
6106+
6107+
rc = main(["guard", "admin", "--home", str(home_dir), "--json"])
6108+
output = json.loads(capsys.readouterr().out)
6109+
6110+
assert rc == 0
6111+
assert opened_urls == ["http://127.0.0.1:5474"]
6112+
assert open_keys == ["dashboard"]
6113+
assert force_open_flags == [True]
6114+
assert output["approval_center_url"] == "http://127.0.0.1:5474"
6115+
assert output["browser_url"] == "http://127.0.0.1:5474"
6116+
assert output["opened"] is False
6117+
assert output["reason"] == "policy-disabled"
6118+
6119+
def test_guard_dashboard_returns_error_when_daemon_start_fails(self, tmp_path, capsys, monkeypatch):
6120+
home_dir = tmp_path / "home"
6121+
6122+
monkeypatch.setattr(
6123+
guard_commands_module,
6124+
"ensure_guard_daemon",
6125+
lambda guard_home: (_ for _ in ()).throw(RuntimeError("dashboard_unavailable")),
6126+
)
6127+
6128+
rc = main(["guard", "dashboard", "--home", str(home_dir), "--json"])
6129+
output = json.loads(capsys.readouterr().out)
6130+
6131+
assert rc == 1
6132+
assert output["opened"] is False
6133+
assert output["error"] == "dashboard_unavailable"
6134+
6135+
def test_public_approval_center_url_strips_guard_token(self):
6136+
browser_url = guard_commands_module._approval_center_browser_url(
6137+
"http://127.0.0.1:5474#section=inbox",
6138+
"secret-token",
6139+
)
6140+
6141+
assert browser_url is not None
6142+
assert "guard-token=secret-token" in browser_url
6143+
assert "guard-token=" not in guard_commands_module._public_approval_center_url(browser_url)
6144+
60516145
def test_guard_connect_pending_output_uses_product_copy_for_sign_in_gap(self, capsys):
60526146
emit_guard_payload(
60536147
"connect",

tests/test_guard_surface_server.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,3 +1620,46 @@ def test_copilot_adapter_implements_surface_runtime_contract(self, tmp_path) ->
16201620
assert operation["operation_type"] == "run"
16211621
assert approval["request_ids"] == ["req-1", "req-2"]
16221622
assert resumed["status"] == "completed"
1623+
1624+
def test_guard_surface_runtime_force_open_bypasses_auto_open_once(self, tmp_path) -> None:
1625+
store = GuardStore(tmp_path / "guard-home")
1626+
runtime = GuardSurfaceRuntime(store)
1627+
opened_urls: list[str] = []
1628+
1629+
first_result = runtime.ensure_surface(
1630+
surface="approval-center",
1631+
approval_center_url="http://127.0.0.1:5474",
1632+
approval_surface_policy="auto-open-once",
1633+
open_key="dashboard",
1634+
opener=lambda url: opened_urls.append(url) or True,
1635+
)
1636+
second_result = runtime.ensure_surface(
1637+
surface="approval-center",
1638+
approval_center_url="http://127.0.0.1:5474",
1639+
approval_surface_policy="auto-open-once",
1640+
open_key="dashboard",
1641+
force_open=True,
1642+
opener=lambda url: opened_urls.append(url) or True,
1643+
)
1644+
1645+
assert first_result["opened"] is True
1646+
assert second_result["opened"] is True
1647+
assert opened_urls == ["http://127.0.0.1:5474", "http://127.0.0.1:5474"]
1648+
1649+
def test_guard_surface_runtime_force_open_overrides_disabled_policy(self, tmp_path) -> None:
1650+
store = GuardStore(tmp_path / "guard-home")
1651+
runtime = GuardSurfaceRuntime(store)
1652+
opened_urls: list[str] = []
1653+
1654+
result = runtime.ensure_surface(
1655+
surface="approval-center",
1656+
approval_center_url="http://127.0.0.1:5474",
1657+
approval_surface_policy="never-auto-open",
1658+
open_key="dashboard",
1659+
force_open=True,
1660+
opener=lambda url: opened_urls.append(url) or True,
1661+
)
1662+
1663+
assert result["opened"] is True
1664+
assert result["reason"] == "opened"
1665+
assert opened_urls == ["http://127.0.0.1:5474"]

0 commit comments

Comments
 (0)