1919_SECRET_ENV_NAME_PATTERNS = (
2020 r"(?i)(?:^|_)(?:api[_-]?key|token|secret|password|passwd|auth)(?:$|_)" ,
2121)
22+ _OAUTH_CALLBACK_URL_PATTERN = re .compile (r"https?://localhost[^\s]*[?&]code=" )
23+ _OAUTH_CODE_ONLY_PATTERN = re .compile (r"^\s*4/[^\s]+\s*$" )
2224
2325# Built-in skills that are handled specially (not loaded from SKILL.md files)
2426BUILTIN_SKILLS : dict [str , str ] = {}
7577This skill declares host capabilities. When capability auth is required or re-authorization is needed:
7678
77791. Start auth immediately with `ash-sb capability auth begin -c <capability>` (do not only say "need auth").
78- 2. Include the exact `auth_url` returned by the command.
79- 3. Include the exact `user_code` for device-code flows.
80- 4. Ask for one clear next step: paste callback URL or code (or confirm completion for device flow polling).
80+ 2. Include the exact `flow_id` returned by the command.
81+ 3. Include the exact `auth_url` returned by the command.
82+ 4. Include the exact `user_code` for device-code flows.
83+ 5. Ask for one clear next step: paste callback URL or code (or confirm completion for device flow polling).
84+
85+ If the user already pasted a callback URL or auth code, complete the existing flow first.
86+ Do not run another `auth begin` until completion fails as invalid/expired.
8187
8288Never replace auth instructions with generic hints or slash-command suggestions.
8389Preserve auth URLs/codes verbatim in your `complete` output.
@@ -460,6 +466,37 @@ async def _check_capability_availability(
460466 f"Skill '{ skill .name } ' has unavailable capabilities: { ', ' .join (missing )} "
461467 )
462468
469+ def _looks_like_oauth_callback_or_code (self , message : str ) -> bool :
470+ """Detect user-provided OAuth callback URLs or code-only replies."""
471+ return bool (
472+ _OAUTH_CALLBACK_URL_PATTERN .search (message )
473+ or _OAUTH_CODE_ONLY_PATTERN .match (message )
474+ )
475+
476+ def _build_capability_auth_recovery_context (
477+ self ,
478+ * ,
479+ skill : SkillDefinition ,
480+ message : str ,
481+ user_context : str ,
482+ ) -> str :
483+ """Inject deterministic auth-completion guidance for callback follow-ups."""
484+ if not skill .capabilities :
485+ return user_context
486+ if not self ._looks_like_oauth_callback_or_code (message ):
487+ return user_context
488+
489+ hint = (
490+ "OAuth callback/code detected in the latest user message.\n "
491+ "Do this before any new auth begin:\n "
492+ "1. Run `ash-sb capability auth list` (add `-c <capability>` / `--account <alias>` if known).\n "
493+ "2. Complete the newest matching pending flow with `ash-sb capability auth complete --flow-id <flow_id> --callback-url '<user_callback_url>'` or `--code '<user_code>'`.\n "
494+ "3. Only run `auth begin` if completion fails with invalid/expired flow."
495+ )
496+ if not user_context :
497+ return hint
498+ return f"{ user_context } \n \n { hint } "
499+
463500 async def execute (
464501 self ,
465502 input_data : dict [str , Any ],
@@ -475,6 +512,9 @@ async def execute(
475512 if not message :
476513 return ToolResult .error ("Missing required field: message" )
477514
515+ message = str (message )
516+ user_context = str (user_context )
517+
478518 if not self ._registry .has (skill_name ):
479519 self ._registry .reload_all (self ._config .workspace )
480520 if not self ._registry .has (skill_name ):
@@ -486,6 +526,11 @@ async def execute(
486526 )
487527
488528 skill = self ._registry .get (skill_name )
529+ user_context = self ._build_capability_auth_recovery_context (
530+ skill = skill ,
531+ message = message ,
532+ user_context = user_context ,
533+ )
489534 skill_config = self ._config .skills .get (skill_name )
490535
491536 if skill_config and not skill_config .enabled :
0 commit comments