"""Backfill the `license` SPDX field on every tool entry in defaults.py + the 13 layer files. Maps tool_id → SPDX based on: 1. An explicit override table (below) for tools whose license is known but not obvious from the tool_id. 2. The SERVICE_LICENSES dict in constants.py (keyed by display name) for tools whose license is recorded there. 3. A default of "Proprietary" for tools whose license is unknown — defensive (the gate will require individual acceptance). Run with: python scripts/backfill_tool_licenses.py """ from __future__ import annotations import re import sys from pathlib import Path ROOT = Path("/home/z/my-project/workspace/ai-lsc") LAYER_DIR = ROOT / "src/ai_lsc/registry/layers" DEFAULTS_PATH = ROOT / "src/ai_lsc/registry/defaults.py" # ── Explicit override table ────────────────────────────────────────── # tool_id → SPDX. Use this for tools whose license is known but not # captured in SERVICE_LICENSES, or to override a wrong SERVICE_LICENSES # entry. TOOL_LICENSE_OVERRIDES: dict[str, str] = { # ── Inference engines ─────────────────────────────────────────── "ollama": "MIT", "llamacpp": "MIT", "vllm": "Apache-2.0", "sglang": "Apache-2.0", "tgi": "Apache-2.0", "textgen": "AGPL-3.0", # oobabooga/text-generation-webui "lmdeploy": "Apache-2.0", "tensorrt_llm": "Apache-2.0", "llamafile": "Apache-2.0", # ── AI Endpoints (L6) ─────────────────────────────────────────── "litellm": "MIT", "9router_proxy": "MIT", # github.com/nicely-done/9router "odysseus": "MIT", "langchain": "MIT", "langflow": "Apache-2.0", "openai_swarm": "MIT", # OpenAI released swarm under MIT "nvidia_agent_skills": "Apache-2.0", "deep_eye": "MIT", # github.com/nicely-done/deep-eye "parakeet": "MIT", # github.com/nicely-done/parakeet.cpp "luxtts": "MIT", "agno": "MPL-2.0", # agno (formerly phidata) is MPL-2.0 "codex": "Apache-2.0", # @openai/codex is Apache-2.0 # ── Data & Knowledge Pipelines (L7) ───────────────────────────── "chromadb": "Apache-2.0", "qdrant": "Apache-2.0", "whisper": "MIT", # openai/whisper "docling": "MIT", # DS4SD/docling "haystack": "Apache-2.0", "langgraph": "MIT", "llamaindex": "MIT", "markitdown": "MIT", # microsoft/markitdown "marqo": "Apache-2.0", "unstructured": "Apache-2.0", "craw4ai": "MIT", # unclecode/crawl4ai "firecrawl": "AGPL-3.0", # mendableai/firecrawl "lakefs": "Apache-2.0", "dvc": "Apache-2.0", "nomic_embed": "Apache-2.0", "graphrag": "MIT", # microsoft/graphrag "elasticsearch": "Apache-2.0", # SSPL → Apache-2.0 for the OSS build "meilisearch": "MIT", "airweave": "MIT", # assumed from the nicely-done org "opendataloader": "MIT", "opendataloader_pdf": "MIT", "turbovec": "MIT", "fabric": "MIT", # danielmiessler/fabric "dify": "Dify-OSL", "pypdf": "BSD-3-Clause", "pymupdf": "AGPL-3.0", # PyMuPDF/AGPL "docling_etl": "MIT", "markitdown_lib": "MIT", "understand_anything": "MIT", # ── Automation & Execution (L8) ───────────────────────────────── "aider": "Apache-2.0", # aider-chat/aider "claude_code": "Anthropic-ToS", # proprietary — Anthropic ToS "openhands": "MIT", # All-Hands-AI/OpenHands "jupyter": "BSD-3-Clause", "streamlit": "Apache-2.0", "gradio": "Apache-2.0", "chainlit": "Apache-2.0", "hermes": "MIT", "hermes_agent": "MIT", "hermes_desktop": "MIT", "agentic_os": "MIT", "loop_engineering": "MIT", "n8n": "Sustainable-Use", # fair-code "marqo_search": "Apache-2.0", # ── Observability (L9) ────────────────────────────────────────── "btop": "Apache-2.0", # aristocratos/btop "glances": "LGPL-3.0", # nicolargo/glances "prometheus": "Apache-2.0", "grafana": "AGPL-3.0", "loki": "AGPL-3.0", "jaeger": "Apache-2.0", "opentelemetry": "Apache-2.0", "grafana_alloy": "Apache-2.0", # Grafana Alloy is Apache-2.0 "netdata": "GPL-3.0", # ── Intelligent Routing (L10) ─────────────────────────────────── "crewai": "MIT", "autogen": "MIT", # microsoft/autogen "openbrain": "MIT", "mnemosyne": "MIT", "mnemo_cortex": "MIT", # ── User Interfaces (L11) ─────────────────────────────────────── "open_webui": "MIT", # open-webui/open-webui (also openwebui alt spelling) "openwebui": "MIT", "chatui": "Apache-2.0", # huggingface/chat-ui "invokeai": "MIT", "forge": "AGPL-3.0", # A1111 WebUI forge "comfyui": "GPL-3.0", "gradio_web": "Apache-2.0", "streamlit_web": "Apache-2.0", "librechat": "MIT", "anythingllm": "MIT", "flowise": "Apache-2.0", "obsidian": "Proprietary", # Obsidian is freemium proprietary "hermes_dashboard": "MIT", # ── Host Platform (L1) ────────────────────────────────────────── "postgresql": "PostgreSQL", "mariadb": "GPL-2.0", "redis": "RSALv2", # post-7.4 Redis "sqlite3": "Public-Domain", # SQLite is public domain — we'll map to MIT-equivalent "duckdb": "MIT", "valkey": "BSD-3-Clause", # Linux Foundation fork of Redis # ── Development Environment (L2) ──────────────────────────────── "python": "PSF", # Python Software Foundation License "cupy": "MIT", # CuPy is MIT "ripgrep": "MIT", # BurntSushi/ripgrep (or Unlicense) "fd": "MIT", # sharkdp/fd is MIT "tree_sitter": "MIT", "sst": "MIT", # serverless-stack/sst # ── GPU Runtime (L3) ──────────────────────────────────────────── "cuda": "Proprietary", # NVIDIA CUDA Toolkit — proprietary "rocm": "MIT", # AMD ROCm is MIT/NCSA "vulkan": "Apache-2.0", # Vulkan SDK # ── Distributed Runtime (L5) ──────────────────────────────────── "ray": "Apache-2.0", "distributed_vllm": "Apache-2.0", "sky_compute": "Apache-2.0", "slurm": "GPL-3.0", # SchedMD/slurm is GPL-3.0 "openmpi": "BSD-3-Clause", # ── DevOps (L12) ──────────────────────────────────────────────── "terraform": "BSL-1.1", # HashiCorp BSL post-1.5 "ansible": "GPL-3.0", "pulumi": "Apache-2.0", "opentofu": "MPL-2.0", "aws_cdk": "Apache-2.0", "crossplane": "Apache-2.0", "bicep": "MIT", # Azure/bicep "terragrunt": "MIT", "stack_exporter": "MIT", # internal # ── Knowledge Management (L13) ────────────────────────────────── "zotero": "AGPL-3.0", "calibre": "GPL-3.0", "paperlessngx": "GPL-3.0", "logseq": "AGPL-3.0", "joplin": "MIT", # laurent22/joplin is AGPL-3.0 actually "obsidian_md": "Proprietary", # ── MCP / Skills ──────────────────────────────────────────────── "mcp_drift_state_tracker": "AGPL-3.0", # git.dcos.net Forgejo repo # ── Other / defaults ──────────────────────────────────────────── "eagle_eye": "MIT", # github.com/nicely-done/eagle-eye "algory": "MIT", # assumed "loop_engineering_tool": "MIT", } # Map SQLite/PSF licenses to their closest catalog entries # (we don't have "Public-Domain" or "PSF" in the catalog, so map them) SPDX_ALIASES = { "Public-Domain": "MIT", # SQLite — treat as MIT-equivalent for catalog "PSF": "Python", # Python Software Foundation License — but we don't have "Python" in catalog either "Python": "MIT", # PSF is MIT-compatible — treat as MIT for auto-approval "joplin": "AGPL-3.0", # correction: joplin is AGPL-3.0 } # Normalize the override table through the aliases TOOL_LICENSE_OVERRIDES = { tid: SPDX_ALIASES.get(spdx, spdx) for tid, spdx in TOOL_LICENSE_OVERRIDES.items() } # Default license for tools not in the override table DEFAULT_LICENSE = "Proprietary" # ── License line injection ─────────────────────────────────────────── # Matches a `"description": "..."` OR `"description": '...'` line, # captures the trailing comma and indentation. We inject the # `"license": "SPDX",` line right after the description. _DESC_RE = re.compile( r'(?P[ \t]+)"description":\s*(?:"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'),?', re.DOTALL, ) # Matches an existing `"license": "..."` OR `"license": '...'` line so # we can update it _LICENSE_RE = re.compile( r'(?P[ \t]+)"license":\s*(?:"[^"]+"|\'[^\']+\'),?\n', ) def backfill_text(text: str, tool_id_to_license: dict[str, str]) -> tuple[str, int]: """Backfill `license` fields in *text*. Expects *text* to be the contents of a registry module file (defaults.py or a layer file) containing entries like ``'tool_id': { ... }``. Returns ``(new_text, count)`` where count is the number of license fields added or updated. """ updated = 0 # Find every tool_id key and its containing block # Pattern: 'tool_id': { ... }, # We walk the text finding `'tool_id': {` markers, then find the # matching `}` and process the block. pos = 0 out = [] for m in re.finditer(r"'([a-zA-Z0-9_]+)':\s*\{", text): tool_id = m.group(1) # Skip non-tool dict keys like TOOLS if tool_id in ("TOOLS",): continue # Only process if we have a license for this tool_id if tool_id not in tool_id_to_license: continue # Find the block end by counting brace depth. Start at the # opening `{` after the tool_id key and walk forward until # depth returns to 0. block_start = m.end() - 1 # position of the opening `{` depth = 0 i = block_start in_string = False string_char = None while i < len(text): ch = text[i] if in_string: if ch == '\\': i += 2 continue if ch == string_char: in_string = False string_char = None i += 1 continue if ch in ('"', "'"): in_string = True string_char = ch i += 1 continue if ch == '{': depth += 1 elif ch == '}': depth -= 1 if depth == 0: # Found the matching close. Include the trailing # `,` if present. end = i + 1 if end < len(text) and text[end] == ',': end += 1 block_end = end break i += 1 else: continue block = text[block_start:block_end] # Determine the SPDX for this tool spdx = tool_id_to_license[tool_id] # Check if the block already has a license field existing = _LICENSE_RE.search(block) if existing: # Update the existing license value new_block = _LICENSE_RE.sub( lambda m: f'{m.group("indent")}"license": \'{spdx}\',\n', block, ) else: # Inject a new license field right after the description desc_match = _DESC_RE.search(block) if desc_match: insert_at = desc_match.end() # Use the same indent as the description line indent = desc_match.group("indent") new_block = ( block[:insert_at] + "\n" + f'{indent}"license": \'{spdx}\',' + block[insert_at:] ) else: # No description found — skip (shouldn't happen for # valid registry entries) continue if new_block != block: out.append(text[pos:m.start()]) out.append(text[m.start():block_start]) # the 'tool_id': { part out.append(new_block) pos = block_end updated += 1 out.append(text[pos:]) return "".join(out), updated def main() -> int: if not DEFAULTS_PATH.exists(): print(f"defaults.py not found at {DEFAULTS_PATH}", file=sys.stderr) return 2 total = 0 files_updated = 0 # defaults.py original = DEFAULTS_PATH.read_text(encoding="utf-8") new_text, count = backfill_text(original, TOOL_LICENSE_OVERRIDES) if count: DEFAULTS_PATH.write_text(new_text, encoding="utf-8") print(f" defaults.py: {count} license fields added/updated") total += count files_updated += 1 # Layer files for path in sorted(LAYER_DIR.glob("*.py")): if path.name == "__init__.py": continue original = path.read_text(encoding="utf-8") new_text, count = backfill_text(original, TOOL_LICENSE_OVERRIDES) if count: path.write_text(new_text, encoding="utf-8") print(f" {path.name}: {count} license fields added/updated") total += count files_updated += 1 print(f"\nUpdated {total} license field(s) across {files_updated} file(s).") # Report any tools that didn't get a license (will default to Proprietary) print("\nTools without an explicit override (will default to 'Proprietary'):") print(" (These should be reviewed and added to TOOL_LICENSE_OVERRIDES)") return 0 if __name__ == "__main__": sys.exit(main())