376 lines
14 KiB
Python
Executable File
376 lines
14 KiB
Python
Executable File
"""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<indent>[ \t]+)"description":\s*(?:"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'),?',
|
|
re.DOTALL,
|
|
)
|
|
|
|
# Matches an existing `"license": "..."` OR `"license": '...'` line so
|
|
# we can update it
|
|
_LICENSE_RE = re.compile(
|
|
r'(?P<indent>[ \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())
|