1"""

2 AgentOS Skill Marketplace Platform (v1.8.1)

3

4 Full-stack developer marketplace:

5 - User registration & JWT authentication

6 - Skill upload with manifest validation

7 - Automated security scanning (dangerous imports, shell injection, obfuscation)

8 - Admin review queue (approve/reject with reason)

9 - Public skill browsing with search/filter

10 - Skill download & version management

11 - GitHub-style developer profiles

12

13 Tech: FastAPI + SQLite + JWT + bcrypt

14"""

15

16 from __future__ import annotations

17

18 import hashlib

19 import json

20 import os

21 import re

22 import secrets

23 import shutil

24 import sqlite3

25 import tempfile

26 import time

27 import zipfile

28 from datetime import datetime, timedelta

29 from pathlib import Path

30 from typing import Optional, List, Dict, Any

31

32 import jwt as pyjwt

33 try:

34 from fastapi import FastAPI, HTTPException, Query, UploadFile, File, Form, Depends, Request

35 from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, StreamingResponse

36 from fastapi.staticfiles import StaticFiles

37 from fastapi.middleware.cors import CORSMiddleware

38 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

39 except ImportError:

40 raise RuntimeError("FastAPI, pyjwt required. Run: pip install fastapi uvicorn pyjwt")

41

42 try:

43 import bcrypt

44 except ImportError:

45 bcrypt = None

46

47 STATIC_DIR = Path(__file__).parent / "static"

48 PLATFORM_DIR = Path.home() / ".agentos" / "marketplace"

49 PLATFORM_DIR.mkdir(parents=True, exist_ok=True)

50 DB_PATH = PLATFORM_DIR / "platform.db"

51 UPLOAD_DIR = PLATFORM_DIR / "uploads"

52 UPLOAD_DIR.mkdir(parents=True, exist_ok=True)

53

54 JWT_SECRET = os.environ.get("MARKETPLACE_SECRET", secrets.token_hex(32))

55 JWT_ALGORITHM = "HS256"

56 JWT_EXPIRY_HOURS = 72

57

58

59# ── Database ─────────────────────────────────

60

61 def get_db() -> sqlite3.Connection:

62 conn = sqlite3.connect(str(DB_PATH))

63 conn.row_factory = sqlite3.Row

64 conn.execute("PRAGMA journal_mode=WAL")

65 conn.execute("PRAGMA foreign_keys=ON")

66 return conn

67

68

69 def init_db():

70 db = get_db()

71 db.executescript("""

72 CREATE TABLE IF NOT EXISTS users (

73 id INTEGER PRIMARY KEY AUTOINCREMENT,

74 username TEXT UNIQUE NOT NULL,

75 email TEXT UNIQUE NOT NULL,

76 password_hash TEXT NOT NULL,

77 display_name TEXT,

78 avatar_url TEXT,

79 github_username TEXT,

80 role TEXT DEFAULT 'developer',

81 is_admin INTEGER DEFAULT 0,

82 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

83 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

84 );

85 CREATE TABLE IF NOT EXISTS skills (

86 id INTEGER PRIMARY KEY AUTOINCREMENT,

87 author_id INTEGER NOT NULL REFERENCES users(id),

88 name TEXT NOT NULL,

89 version TEXT NOT NULL DEFAULT '0.1.0',

90 description TEXT,

91 category TEXT DEFAULT 'uncategorized',

92 tags TEXT DEFAULT '[]',

93 format TEXT DEFAULT 'agentos',

94 entrypoint TEXT,

95 manifest_json TEXT,

96 file_path TEXT,

97 file_size INTEGER,

98 file_hash TEXT,

99 download_count INTEGER DEFAULT 0,

100 status TEXT DEFAULT 'pending',

101 security_score INTEGER,

102 security_report TEXT,

103 review_comment TEXT,

104 reviewed_by INTEGER REFERENCES users(id),

105 reviewed_at TIMESTAMP,

106 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

107 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

108 UNIQUE(name, author_id)

109 );

110 CREATE TABLE IF NOT EXISTS reviews (

111 id INTEGER PRIMARY KEY AUTOINCREMENT,

112 skill_id INTEGER NOT NULL REFERENCES skills(id),

113 user_id INTEGER NOT NULL REFERENCES users(id),

114 rating INTEGER CHECK(rating >= 1 AND rating <= 5),

115 comment TEXT,

116 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

117 );

118 CREATE TABLE IF NOT EXISTS api_tokens (

119 id INTEGER PRIMARY KEY AUTOINCREMENT,

120 user_id INTEGER NOT NULL REFERENCES users(id),

121 token_hash TEXT NOT NULL,

122 name TEXT,

123 last_used TIMESTAMP,

124 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

125 );

126 """)

127 # Ensure admin user exists

128 admin = db.execute("SELECT id FROM users WHERE username = ?", ("admin",)).fetchone()

129 if not admin:

130 _hash = _hash_password("admin123")

131 db.execute(

132 "INSERT INTO users (username, email, password_hash, role, is_admin) VALUES (?,?,?,?,?)",

133 ("admin", "admin@agentos.dev", _hash, "admin", 1),

134 )

135 db.commit()

136 db.close()

137

138

139 def _hash_password(password: str) -> str:

140 if bcrypt:

141 return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")

142 return hashlib.sha256(f"agentos:{password}".encode()).hexdigest()

143

144

145 def _verify_password(password: str, password_hash: str) -> bool:

146 if bcrypt and password_hash.startswith("$2"):

147 return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("utf-8"))

148 return _hash_password(password) == password_hash

149

150

151# ── Security Scanner ─────────────────────────

152

153 class SecurityScanner:

154 """Scans uploaded skill packages for security issues."""

155

156 DANGEROUS_IMPORTS = {

157 "os.system", "subprocess", "eval(", "exec(", "compile(",

158 "__import__", "importlib", "builtins", "ctypes",

159 "socket", "requests", "urllib", "http.client",

160 "shutil.rmtree", "shutil.copy", "pathlib.Path.unlink",

161 "pickle", "marshal", "dill",

162 }

163

164 DANGEROUS_SHELL = {" rm ", "rm -rf", "sudo ", "chmod 777", "chown ",

165 " | sh", " | bash", "$(", "`", "; rm", "wget ", "curl ",

166 "/dev/null", "> /etc/", "> ~/.ssh/"}

167

168 OBFUSCATION_SIGNALS = {"base64.b64decode", "base64.b64encode", "exec(base64",

169 "decode('utf-8')", "eval(compile", "globals()", "__builtins__",

170 "lambda.*exec", "lambda.*eval", "getattr.*__"}

171

172 @classmethod

173 def scan_zip(cls, zip_path: str) -> Dict[str, Any]:

174 """Scan a skill zip package and return security report."""

175 findings = []

176 score = 100

177 files_scanned = 0

178 total_size = 0

179

180 try:

181 with zipfile.ZipFile(zip_path, "r") as zf:

182 for info in zf.infolist():

183 if info.is_dir():

184 continue

185 total_size += info.file_size

186 name = info.filename.lower()

187

188 # Skip binary files

189 if any(name.endswith(ext) for ext in (".pyc", ".so", ".dll", ".exe", ".png", ".jpg", ".ico")):

190 continue

191

192 try:

193 content = zf.read(info.filename).decode("utf-8", errors="replace")

194 files_scanned += 1

195 except Exception:

196 continue

197

198 lines = content.split("\n")

199

200 # Check for dangerous imports

201 for imp in cls.DANGEROUS_IMPORTS:

202 if imp in content:

203 findings.append({"file": info.filename, "severity": "high",

204 "rule": f"dangerous_import:{imp}", "line": content.find(imp)})

205 score -= 20

206

207 # Check for shell injection

208 for pat in cls.DANGEROUS_SHELL:

209 if pat in content:

210 findings.append({"file": info.filename, "severity": "critical",

211 "rule": f"shell_injection:{pat.strip()}"})

212 score -= 30

213

214 # Check for obfuscation

215 for pat in cls.OBFUSCATION_SIGNALS:

216 if re.search(pat, content):

217 findings.append({"file": info.filename, "severity": "medium",

218 "rule": f"obfuscation:{pat}"})

219 score -= 15

220

221 # Check for hardcoded secrets

222 if re.search(r'(api_key|secret|password|token)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{20,}', content):

223 findings.append({"file": info.filename, "severity": "high",

224 "rule": "hardcoded_secret"})

225 score -= 25

226 except zipfile.BadZipFile:

227 return {"score": 0, "findings": [{"severity": "critical", "rule": "invalid_zip"}]}

228

229 return {

230 "score": max(0, score),

231 "findings": findings,

232 "files_scanned": files_scanned,

233 "total_size": total_size,

234 "risk_level": "low" if score >= 80 else "medium" if score >= 50 else "high" if score >= 20 else "critical",

235 }

236

237 @classmethod

238 def validate_manifest(cls, manifest: Dict) -> List[str]:

239 """Validate skill manifest structure. Returns list of errors."""

240 errors = []

241 required = ["name", "version", "description"]

242 for field in required:

243 if not manifest.get(field):

244 errors.append(f"Missing required field: {field}")

245

246 if "name" in manifest:

247 name = manifest["name"]

248 if not re.match(r'^[a-zA-Z][a-zA-Z0-9_\-]*$', name):

249 errors.append(f"Invalid skill name: {name}. Use alphanumeric, hyphens, underscores.")

250

251 if "version" in manifest:

252 version = manifest["version"]

253 if not re.match(r'^\d+\.\d+\.\d+$', version):

254 errors.append(f"Invalid version format: {version}. Use semver (e.g., 0.1.0).")

255

256 return errors

257

258

259# ── Auth Utilities ───────────────────────────

260

261 security_scheme = HTTPBearer(auto_error=False)

262

263

264 def create_token(user_id: int, username: str, is_admin: bool) -> str:

265 payload = {

266 "user_id": user_id,

267 "username": username,

268 "is_admin": is_admin,

269 "exp": datetime.utcnow() + timedelta(hours=JWT_EXPIRY_HOURS),

270 "iat": datetime.utcnow(),

271 }

272 return pyjwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

273

274

275 def decode_token(token: str) -> Optional[Dict]:

276 try:

277 return pyjwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])

278 except Exception:

279 return None

280

281

282 async def get_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_scheme)):

283 if not credentials:

284 raise HTTPException(status_code=401, detail="Authentication required")

285 payload = decode_token(credentials.credentials)

286 if not payload:

287 raise HTTPException(status_code=401, detail="Invalid or expired token")

288 db = get_db()

289 user = db.execute("SELECT * FROM users WHERE id = ?", (payload["user_id"],)).fetchone()

290 db.close()

291 if not user:

292 raise HTTPException(status_code=401, detail="User not found")

293 return dict(user)

294

295

296 async def get_admin_user(user: dict = Depends(get_current_user)):

297 if not user.get("is_admin"):

298 raise HTTPException(status_code=403, detail="Admin privileges required")

299 return user

300

301

302# ── FastAPI App ──────────────────────────────

303

304 def create_marketplace_app() -> FastAPI:

305 init_db()

306

307 app = FastAPI(

308 title="AgentOS Skill Marketplace",

309 version="1.8.1",

310 description="Open developer marketplace for AgentOS skills. Upload, review, and discover AI agent skills.",

311 )

312

313 app.add_middleware(

314 CORSMiddleware,

315 allow_origins=["*"],

316 allow_methods=["*"],

317 allow_headers=["*"],

318 )

319

320 if STATIC_DIR.exists():

321 app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")

322

323 # ── Web UI ──

324 @app.get("/", response_class=HTMLResponse)

325 async def web_ui():

326 html_path = STATIC_DIR / "platform.html"

327 if html_path.exists():

328 return HTMLResponse(html_path.read_text(encoding="utf-8"))

329 return HTMLResponse("<h1>Marketplace Platform</h1>", status_code=404)

330

331 # ── Auth Endpoints ──

332

333 @app.post("/api/auth/register")

334 async def register(username: str = Form(...), email: str = Form(...),

335 password: str = Form(...), display_name: str = Form("")):

336 if len(password) < 6:

337 raise HTTPException(400, "Password must be at least 6 characters")

338 if not re.match(r'^[a-zA-Z0-9_]{3,30}$', username):

339 raise HTTPException(400, "Username: 3-30 chars, alphanumeric/underscore")

340

341 db = get_db()

342 existing = db.execute("SELECT id FROM users WHERE username=? OR email=?",

343 (username, email)).fetchone()

344 if existing:

345 db.close()

346 raise HTTPException(409, "Username or email already exists")

347

348 pw_hash = _hash_password(password)

349 try:

350 db.execute("INSERT INTO users (username, email, password_hash, display_name) VALUES (?,?,?,?)",

351 (username, email, pw_hash, display_name or username))

352 db.commit()

353 user_id = db.lastrowid

354 finally:

355 db.close()

356

357 token = create_token(user_id, username, False)

358 return {"token": token, "user": {"id": user_id, "username": username, "email": email, "is_admin": False}}

359

360 @app.post("/api/auth/login")

361 async def login(username: str = Form(...), password: str = Form(...)):

362 db = get_db()

363 user = db.execute("SELECT * FROM users WHERE username = ? OR email = ?",

364 (username, username)).fetchone()

365 if not user or not _verify_password(password, user["password_hash"]):

366 db.close()

367 raise HTTPException(401, "Invalid credentials")

368 db.close()

369

370 token = create_token(user["id"], user["username"], bool(user["is_admin"]))

371 return {

372 "token": token,

373 "user": {

374 "id": user["id"], "username": user["username"], "email": user["email"],

375 "display_name": user["display_name"], "is_admin": bool(user["is_admin"]),

376 "github_username": user["github_username"],

377 }

378 }

379

380 @app.get("/api/auth/me")

381 async def me(user: dict = Depends(get_current_user)):

382 return {"user": {k: v for k, v in user.items() if k != "password_hash"}}

383

384 # ── Skill Upload ──

385

386 @app.post("/api/skills/upload")

387 async def upload_skill(

388 file: UploadFile = File(...),

389 name: str = Form(""),

390 version: str = Form("0.1.0"),

391 description: str = Form(""),

392 category: str = Form("uncategorized"),

393 tags: str = Form("[]"),

394 user: dict = Depends(get_current_user),

395 ):

396 """Upload a skill package (.zip containing skill files + manifest.json)."""

397 if not file.filename or not file.filename.endswith(".zip"):

398 raise HTTPException(400, "Only .zip files are accepted")

399

400 # Save uploaded file

401 ts = int(time.time())

402 safe_name = re.sub(r'[^a-zA-Z0-9_.-]', '_', file.filename)

403 file_id = f"{user['id']}_{ts}_{safe_name}"

404 file_path = UPLOAD_DIR / file_id

405 content = await file.read()

406 file_path.write_bytes(content)

407

408 # Validate zip

409 if not zipfile.is_zipfile(str(file_path)):

410 file_path.unlink()

411 raise HTTPException(400, "Invalid zip file")

412

413 # Extract and validate manifest

414 manifest = {}

415 with zipfile.ZipFile(str(file_path)) as zf:

416 if "skill.yaml" in zf.namelist():

417 manifest_text = zf.read("skill.yaml").decode("utf-8")

418 manifest = _parse_yaml_simple(manifest_text)

419 elif "skill.json" in zf.namelist():

420 manifest = json.loads(zf.read("skill.json"))

421 elif "manifest.json" in zf.namelist():

422 manifest = json.loads(zf.read("manifest.json"))

423

424 # Use form fields as fallback

425 skill_name = manifest.get("name") or name or file.filename.replace(".zip", "")

426 skill_version = manifest.get("version") or version

427 skill_desc = manifest.get("description") or description

428 skill_category = manifest.get("category") or category

429 skill_tags = manifest.get("tags", []) if isinstance(manifest.get("tags"), list) else []

430

431 # Use form tags if manifest has none

432 if not skill_tags:

433 try:

434 skill_tags = json.loads(tags) if isinstance(tags, str) else tags

435 except json.JSONDecodeError:

436 skill_tags = []

437

438 # Validate

439 manifest_errors = SecurityScanner.validate_manifest({

440 "name": skill_name, "version": skill_version, "description": skill_desc,

441 })

442 if manifest_errors:

443 file_path.unlink()

444 raise HTTPException(400, f"Manifest validation failed: {'; '.join(manifest_errors)}")

445

446 # Security scan

447 security = SecurityScanner.scan_zip(str(file_path))

448 auto_status = "published" if security["risk_level"] == "low" else "flagged"

449 if security["score"] <= 20:

450 auto_status = "rejected"

451 file_path.unlink()

452 raise HTTPException(400, f"Security scan failed (score: {security['score']}/100). "

453 f"Risk: {security['risk_level']}. Findings: {len(security['findings'])}")

454

455 # Compute hash

456 file_hash = hashlib.sha256(content).hexdigest()

457

458 db = get_db()

459 try:

460 db.execute(

461 """INSERT INTO skills (author_id, name, version, description, category, tags,

462 format, entrypoint, manifest_json, file_path, file_size, file_hash,

463 status, security_score, security_report)

464 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",

465 (user["id"], skill_name, skill_version, skill_desc, skill_category,

466 json.dumps(skill_tags), manifest.get("format", "agentos"),

467 manifest.get("entrypoint", ""), json.dumps(manifest, ensure_ascii=False),

468 str(file_path.absolute()), os.path.getsize(str(file_path)), file_hash,

469 auto_status, security["score"], json.dumps(security, ensure_ascii=False)),

470 )

471 db.commit()

472 skill_id = db.lastrowid

473 except sqlite3.IntegrityError:

474 file_path.unlink()

475 db.close()

476 raise HTTPException(409, "You already have a skill with this name")

477 db.close()

478

479 return {

480 "id": skill_id, "name": skill_name, "version": skill_version,

481 "status": auto_status, "security_score": security["score"],

482 "risk_level": security["risk_level"],

483 "findings_count": len(security["findings"]),

484 }

485

486 # ── Public Browse ──

487

488 @app.get("/api/skills")

489 async def list_skills(

490 q: str = Query(""),

491 category: str = Query(""),

492 status: str = Query("published"),

493 sort: str = Query("downloads"),

494 page: int = Query(1),

495 limit: int = Query(30),

496 ):

497 db = get_db()

498 where = ["s.status = ?"]

499 params: list = [status]

500

501 if q:

502 where.append("(s.name LIKE ? OR s.description LIKE ?)")

503 params.extend([f"%{q}%", f"%{q}%"])

504 if category:

505 where.append("s.category = ?")

506 params.append(category)

507

508 order = "s.download_count DESC" if sort == "downloads" else "s.created_at DESC"

509 offset = (page - 1) * limit

510

511 skills = db.execute(

512 f"""SELECT s.*, u.username as author_name, u.display_name as author_display

513 FROM skills s JOIN users u ON s.author_id = u.id

514 WHERE {' AND '.join(where)}

515 ORDER BY {order}

516 LIMIT ? OFFSET ?""",

517 params + [limit, offset],

518 ).fetchall()

519

520 total = db.execute(

521 f"SELECT COUNT(*) FROM skills s WHERE {' AND '.join(where)}", params

522 ).fetchone()[0]

523 db.close()

524

525 return {

526 "total": total, "page": page, "limit": limit,

527 "skills": [

528 {"id": s["id"], "name": s["name"], "version": s["version"],

529 "description": s["description"], "category": s["category"],

530 "tags": json.loads(s["tags"]), "format": s["format"],

531 "download_count": s["download_count"], "status": s["status"],

532 "security_score": s["security_score"],

533 "author": {"username": s["author_name"], "display_name": s["author_display"]},

534 "created_at": s["created_at"]}

535 for s in skills

536 ],

537 }

538

539 @app.get("/api/skills/{skill_id}")

540 async def get_skill_detail(skill_id: int):

541 db = get_db()

542 skill = db.execute(

543 """SELECT s.*, u.username as author_name, u.display_name as author_display

544 FROM skills s JOIN users u ON s.author_id = u.id

545 WHERE s.id = ? AND s.status = 'published'""",

546 (skill_id,),

547 ).fetchone()

548 if not skill:

549 db.close()

550 raise HTTPException(404, "Skill not found")

551

552 # Get review stats

553 review_stats = db.execute(

554 "SELECT COUNT(*) as count, AVG(rating) as avg_rating FROM reviews WHERE skill_id = ?",

555 (skill_id,),

556 ).fetchone()

557 db.close()

558

559 return {

560 "id": skill["id"], "name": skill["name"], "version": skill["version"],

561 "description": skill["description"], "category": skill["category"],

562 "tags": json.loads(skill["tags"]), "format": skill["format"],

563 "entrypoint": skill["entrypoint"], "manifest": json.loads(skill["manifest_json"] or "{}"),

564 "download_count": skill["download_count"], "status": skill["status"],

565 "security_score": skill["security_score"],

566 "author": {"username": skill["author_name"], "display_name": skill["author_display"]},

567 "created_at": skill["created_at"],

568 "reviews": {"count": review_stats["count"] or 0,

569 "avg_rating": round(review_stats["avg_rating"] or 0, 1)},

570 }

571

572 # ── Download ──

573

574 @app.get("/api/skills/{skill_id}/download")

575 async def download_skill(skill_id: int):

576 db = get_db()

577 skill = db.execute(

578 "SELECT * FROM skills WHERE id = ? AND status = 'published'", (skill_id,)

579 ).fetchone()

580 if not skill:

581 db.close()

582 raise HTTPException(404, "Skill not found")

583

584 db.execute("UPDATE skills SET download_count = download_count + 1 WHERE id = ?", (skill_id,))

585 db.commit()

586 db.close()

587

588 file_path = Path(skill["file_path"])

589 if not file_path.exists():

590 raise HTTPException(404, "Skill file not found on server")

591

592 return FileResponse(

593 path=str(file_path),

594 filename=f"{skill['name']}-{skill['version']}.zip",

595 media_type="application/zip",

596 )

597

598 # ── Admin: Review Queue ──

599

600 @app.get("/api/admin/review-queue")

601 async def review_queue(user: dict = Depends(get_admin_user)):

602 db = get_db()

603 skills = db.execute(

604 """SELECT s.*, u.username as author_name

605 FROM skills s JOIN users u ON s.author_id = u.id

606 WHERE s.status IN ('pending', 'flagged')

607 ORDER BY s.created_at DESC"""

608 ).fetchall()

609 db.close()

610 return {

611 "count": len(skills),

612 "skills": [

613 {"id": s["id"], "name": s["name"], "version": s["version"],

614 "description": s["description"], "category": s["category"],

615 "status": s["status"], "security_score": s["security_score"],

616 "security_report": json.loads(s["security_report"] or "{}"),

617 "author": s["author_name"], "created_at": s["created_at"]}

618 for s in skills

619 ],

620 }

621

622 @app.post("/api/admin/review/{skill_id}")

623 async def review_skill(

624 skill_id: int,

625 action: str = Form(...), # "approve" or "reject"

626 comment: str = Form(""),

627 user: dict = Depends(get_admin_user),

628 ):

629 if action not in ("approve", "reject"):

630 raise HTTPException(400, "Action must be 'approve' or 'reject'")

631

632 db = get_db()

633 skill = db.execute("SELECT * FROM skills WHERE id = ?", (skill_id,)).fetchone()

634 if not skill:

635 db.close()

636 raise HTTPException(404, "Skill not found")

637

638 new_status = "published" if action == "approve" else "rejected"

639 db.execute(

640 "UPDATE skills SET status=?, review_comment=?, reviewed_by=?, reviewed_at=CURRENT_TIMESTAMP WHERE id=?",

641 (new_status, comment, user["id"], skill_id),

642 )

643 db.commit()

644 db.close()

645

646 return {"id": skill_id, "status": new_status, "action": action}

647

648 # ── Categories ──

649

650 @app.get("/api/categories")

651 async def list_categories():

652 db = get_db()

653 cats = db.execute(

654 "SELECT category, COUNT(*) as count FROM skills WHERE status='published' GROUP BY category ORDER BY count DESC"

655 ).fetchall()

656 db.close()

657 return {"categories": [{"name": c["category"], "count": c["count"]} for c in cats]}

658

659 # ── Developer Profile ──

660

661 @app.get("/api/developers/{username}")

662 async def developer_profile(username: str):

663 db = get_db()

664 user = db.execute("SELECT id, username, display_name, avatar_url, github_username, created_at FROM users WHERE username = ?", (username,)).fetchone()

665 if not user:

666 db.close()

667 raise HTTPException(404, "Developer not found")

668

669 skills = db.execute(

670 "SELECT id, name, version, description, category, tags, download_count, status, created_at FROM skills WHERE author_id = ? AND status = 'published' ORDER BY download_count DESC",

671 (user["id"],),

672 ).fetchall()

673 db.close()

674

675 return {

676 "developer": dict(user),

677 "skills": [dict(s) for s in skills],

678 "total_skills": len(skills),

679 "total_downloads": sum(s["download_count"] for s in skills),

680 }

681

682 # ── My Skills ──

683

684 @app.get("/api/my/skills")

685 async def my_skills(user: dict = Depends(get_current_user)):

686 db = get_db()

687 skills = db.execute(

688 "SELECT * FROM skills WHERE author_id = ? ORDER BY created_at DESC",

689 (user["id"],),

690 ).fetchall()

691 db.close()

692 return {"skills": [dict(s) for s in skills]}

693

694 # ── Health ──

695

696 @app.get("/api/health")

697 async def health_check():

698 db = get_db()

699 skill_count = db.execute("SELECT COUNT(*) FROM skills WHERE status='published'").fetchone()[0]

700 user_count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]

701 db.close()

702 return {

703 "status": "healthy",

704 "version": "1.8.1",

705 "published_skills": skill_count,

706 "registered_developers": user_count,

707 }

708

709 return app

710

711

712 def _parse_yaml_simple(text: str) -> Dict[str, Any]:

713 """Simple YAML parser for skill manifests. Handles basic key: value + lists."""

714 result: Dict[str, Any] = {}

715 current_key = None

716 for line in text.split("\n"):

717 stripped = line.strip()

718 if not stripped or stripped.startswith("#"):

719 continue

720 if ":" in stripped and not stripped.startswith("- "):

721 key, _, val = stripped.partition(":")

722 key = key.strip()

723 val = val.strip().strip('"').strip("'")

724 if val:

725 result[key] = val

726 else:

727 result[key] = []

728 current_key = key

729 elif stripped.startswith("- ") and current_key:

730 item = stripped[2:].strip().strip('"').strip("'")

731 result[current_key].append(item)

732 return result

733

734

735 def start_marketplace_platform(host: str = "0.0.0.0", port: int = 8911) -> None:

736 """Start the marketplace platform server (blocking)."""

737 import uvicorn

738 app = create_marketplace_app()

739 print(f"\n AgentOS Skill Marketplace Platform v1.8.1")

740 print(f" Local: http://{host}:{port}")

741 print(f" Admin: admin / admin123")

742 print(f" Upload: POST /api/skills/upload | Browse: GET /api/skills")

743 print()

744 uvicorn.run(app, host=host, port=port, log_level="info")