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
32943355def _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+
33073380def _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"
0 commit comments