@@ -413,10 +413,21 @@ def _check_remote(where: str, entry: dict, report: Report) -> None:
413413 f"url { url !r} must not contain placeholders; clients MUST NOT perform "
414414 f"placeholder or environment-variable expansion in url (§7.2.1)." ,
415415 )
416- if not url .startswith (("http://" , "https://" )):
417- report .fail (where , f"url { url !r} must be an absolute HTTP or HTTPS URL (§7.2.1)" )
418- elif "#" in url :
419- report .fail (where , f"url { url !r} must not contain a fragment (§7.2.1)" )
416+ # §7.2.1: an absolute HTTP or HTTPS URL. Parse, don't prefix-match — a bare
417+ # `https://` or a URL with an empty host or embedded whitespace must not pass.
418+ from urllib .parse import urlsplit
419+
420+ if any (ch .isspace () for ch in url ):
421+ report .fail (where , f"url { url !r} must not contain whitespace (§7.2.1)" )
422+ else :
423+ parts = urlsplit (url )
424+ if parts .scheme not in ("http" , "https" ) or not parts .netloc :
425+ report .fail (
426+ where ,
427+ f"url { url !r} must be an absolute HTTP or HTTPS URL with a host (§7.2.1)" ,
428+ )
429+ elif parts .fragment :
430+ report .fail (where , f"url { url !r} must not contain a fragment (§7.2.1)" )
420431
421432 headers = entry .get ("headers" )
422433 if headers is not None :
@@ -488,16 +499,23 @@ def check_skills(root: Path, report: Report) -> tuple[int, list[str]]:
488499
489500def check_skill_md (where : str , skill_dir : Path , path : Path , report : Report ) -> None :
490501 text = path .read_text (encoding = "utf-8" , errors = "replace" )
491- if not text .startswith ("---" ):
502+ # The opening delimiter must be a line that is exactly `---`; a prefix or
503+ # substring check would accept `---not-a-fence`.
504+ lines = text .splitlines ()
505+ if not lines or lines [0 ].strip () != "---" :
492506 report .fail (where , "missing YAML frontmatter; required by the Agent Skills spec (§7.1)" )
493507 return
494- end = text .find ("\n ---" , 3 )
508+ end = - 1
509+ for index in range (1 , len (lines )):
510+ if lines [index ].strip () == "---" :
511+ end = index
512+ break
495513 if end == - 1 :
496514 report .fail (where , "unterminated YAML frontmatter" )
497515 return
498516
499517 fields : dict [str , str ] = {}
500- for line in text [ 3 :end ]. splitlines () :
518+ for line in lines [ 1 :end ]:
501519 match = re .match (r"^([A-Za-z_-]+):\s*(.*)$" , line )
502520 if match :
503521 fields [match .group (1 )] = match .group (2 ).strip ().strip ('"' ).strip ("'" )
@@ -522,18 +540,24 @@ def check_skill_md(where: str, skill_dir: Path, path: Path, report: Report) -> N
522540
523541
524542def _content_files (directory : Path ) -> dict [str , Path ]:
525- """Map relative path -> file for every real content file under `directory`."""
543+ """Map relative path -> file for every real content file under `directory`.
544+
545+ Dot-prefixed paths are skipped along with build caches — mirrors and scans
546+ must never treat `.DS_Store`, `.gitignore` or similar as content."""
526547 found : dict [str , Path ] = {}
527548 if not directory .is_dir ():
528549 return found
529550 for path in directory .rglob ("*" ):
530551 if not path .is_file ():
531552 continue
553+ relative = path .relative_to (directory )
554+ if any (part .startswith ("." ) for part in relative .parts ):
555+ continue
532556 if any (part in _IGNORED_PARTS for part in path .parts ):
533557 continue
534558 if path .suffix in _IGNORED_SUFFIXES :
535559 continue
536- found [str (path . relative_to ( directory ) )] = path
560+ found [str (relative )] = path
537561 return found
538562
539563
@@ -574,20 +598,20 @@ def check_client_extensions(root: Path, report: Report) -> None:
574598 report .fail (
575599 f"{ namespace } /{ source } /{ relative } " ,
576600 f"is missing from the client extension mirror of `{ source } /`; "
577- f"re-run scripts/add_client_extensions.py (§8.2)." ,
601+ f"refresh the ` { namespace } / { source } /` mirror from the root copy (§8.2)." ,
578602 )
579603 for relative in sorted (set (mirrored ) - set (original )):
580604 report .fail (
581605 f"{ namespace } /{ source } /{ relative } " ,
582606 f"has no counterpart in the root `{ source } /`; remove the stale "
583- f"mirror (§8.2)." ,
607+ f"mirror file (§8.2)." ,
584608 )
585609 for relative in sorted (set (mirrored ) & set (original )):
586610 if mirrored [relative ].read_bytes () != original [relative ].read_bytes ():
587611 report .fail (
588612 f"{ namespace } /{ source } /{ relative } " ,
589- f"has drifted from the root `{ source } /` copy; re-run "
590- f"scripts/add_client_extensions.py (§8.2)." ,
613+ f"has drifted from the root `{ source } /` copy; refresh the "
614+ f"mirror from the root copy (§8.2)." ,
591615 )
592616
593617 if mirrored :
@@ -631,7 +655,8 @@ def base(value):
631655 interface = other .get ("interface" )
632656 if isinstance (interface , dict ):
633657 extensions = plugin .get ("extensions" )
634- carried = extensions .get ("com.openai" , {}).get ("interface" ) if isinstance (extensions , dict ) else None
658+ openai = extensions .get ("com.openai" ) if isinstance (extensions , dict ) else None
659+ carried = openai .get ("interface" ) if isinstance (openai , dict ) else None
635660 if carried is None :
636661 report .fail (
637662 where ,
0 commit comments