2026-10-12 20:48:46 +00:00
import json , re , sys
def parse_fm ( text ) :
try :
import yaml # pip install pyyaml (recommended: matches real YAML parsing)
return yaml . safe_load ( text ) or { }
except ImportError :
out = { }
for line in text . split ( " \n " ) :
if " : " in line and not line . startswith ( " " ) :
k , v = line . split ( " : " , 1 ) ; v = v . strip ( )
if len ( v ) > 1 and v [ 0 ] == v [ - 1 ] and v [ 0 ] in " \" ' " : v = v [ 1 : - 1 ]
out [ k . strip ( ) ] = v
return out
def main ( path ) :
errs = [ ]
d = json . load ( open ( path ) )
if d . get ( " format " ) != " openmaus.package " : errs . append ( " format must be ' openmaus.package ' " )
if d . get ( " version " ) != 2 : errs . append ( " version must be 2 " )
p = d . get ( " package " , { } )
for k in [ " id " , " release " , " name " , " tagline " , " summary " , " category " , " author " , " license " , " outcomes " , " requirements " , " team " , " agents " ] :
if k not in p : errs . append ( f " package. { k } missing " )
2026-10-12 20:57:22 +00:00
# The supplied template/export examples establish empty requirement arrays.
# Nonempty registry requirements need the actual app schema, not free text.
requirements = p . get ( " requirements " , { } )
if not isinstance ( requirements , dict ) :
errs . append ( " package.requirements must be an object " )
else :
for field in ( " apps " , " capabilities " ) :
if requirements . get ( field ) != [ ] :
errs . append ( f " package.requirements. { field } : this portable generator supports [] only; put manual setup in skills until registry requirements are verified " )
2026-10-12 20:48:46 +00:00
kebab = re . compile ( r " ^[a-z0-9]+(-[a-z0-9]+)*$ " )
if not kebab . match ( p . get ( " id " , " " ) ) : errs . append ( " package.id must be kebab-case " )
if not re . match ( r " ^ \ d+ \ . \ d+ \ . \ d+$ " , p . get ( " release " , " " ) ) : errs . append ( " release must be semver " )
entries = { e . get ( " name " ) : e for e in p . get ( " skills " , { } ) . get ( " entries " , [ ] ) }
for name , e in entries . items ( ) :
m = re . match ( r " ^--- \ n(.*?) \ n--- " , e . get ( " instructions " , " " ) , re . S )
if not m : errs . append ( f " skill { name } : instructions need --- frontmatter --- " ) ; continue
fm = parse_fm ( m . group ( 1 ) )
if fm . get ( " name " ) != e . get ( " name " ) : errs . append ( f " skill { name } : frontmatter name differs " )
if fm . get ( " description " ) != e . get ( " description " ) : errs . append ( f " skill { name } : frontmatter description differs from entry description " )
keys = set ( )
for a in p . get ( " agents " , [ ] ) :
keys . add ( a . get ( " key " ) )
for k in [ " key " , " name " ] :
if k not in a : errs . append ( f " agent missing { k } " )
for s in a . get ( " skills " , [ ] ) :
if s not in entries : errs . append ( f " agent { a . get ( ' key ' ) } : unknown skill { s } " )
for r in p . get ( " routines " , [ ] ) :
if r . get ( " agent " ) not in keys : errs . append ( f " routine { r . get ( ' key ' ) } : unknown agent { r . get ( ' agent ' ) } " )
if r . get ( " enabledAfterInstall " ) is not False : errs . append ( f " routine { r . get ( ' key ' ) } : enabledAfterInstall must be false " )
if " schedule " not in r or " expression " not in r [ " schedule " ] : errs . append ( f " routine { r . get ( ' key ' ) } : schedule.expression missing " )
blob = json . dumps ( d ) . lower ( )
for pat in [ r " password \ s*[:=] " , r " api[_-]?key \ s*[:=] " , r " secret \ s*[:=] " , r " bearer \ s+[a-z0-9] " , r " sk-[a-z0-9] { 10,} " ] :
if re . search ( pat , blob ) : errs . append ( f " possible secret matches / { pat } / - remove it " )
print ( " OK " if not errs else " \n " . join ( " ERROR: " + e for e in errs ) )
sys . exit ( 1 if errs else 0 )
main ( sys . argv [ 1 ] )