leangate 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- leangate-0.1.0/.claude-plugin/marketplace.json +13 -0
- leangate-0.1.0/.dockerignore +17 -0
- leangate-0.1.0/.github/scripts/action_report.py +77 -0
- leangate-0.1.0/.github/workflows/ci.yml +49 -0
- leangate-0.1.0/.github/workflows/publish.yml +43 -0
- leangate-0.1.0/.gitignore +19 -0
- leangate-0.1.0/.pre-commit-hooks.yaml +12 -0
- leangate-0.1.0/DEPLOY.md +195 -0
- leangate-0.1.0/Dockerfile +19 -0
- leangate-0.1.0/LICENSE +21 -0
- leangate-0.1.0/PKG-INFO +556 -0
- leangate-0.1.0/README.md +505 -0
- leangate-0.1.0/action.yml +137 -0
- leangate-0.1.0/bench/RESULTS.md +79 -0
- leangate-0.1.0/bench/RESULTS_claude.md +84 -0
- leangate-0.1.0/bench/RESULTS_fresh.md +109 -0
- leangate-0.1.0/bench/RESULTS_large.md +107 -0
- leangate-0.1.0/bench/RESULTS_new.md +63 -0
- leangate-0.1.0/bench/agent_bench.py +252 -0
- leangate-0.1.0/bench/claude_bench.py +304 -0
- leangate-0.1.0/bench/claude_bench_reps.py +415 -0
- leangate-0.1.0/bench/tasks_claude.json +270 -0
- leangate-0.1.0/bench/tasks_famous.json +26 -0
- leangate-0.1.0/bench/tasks_fresh.json +207 -0
- leangate-0.1.0/bench/tasks_fresh_reuse.json +57 -0
- leangate-0.1.0/bench/tasks_large.json +252 -0
- leangate-0.1.0/bench/tasks_new.json +250 -0
- leangate-0.1.0/bench/tasks_zulip.json +26 -0
- leangate-0.1.0/demo/agent_change/admin_export.py +50 -0
- leangate-0.1.0/demo/agent_change/billing.py +51 -0
- leangate-0.1.0/demo/agent_change/billing_lean.py +26 -0
- leangate-0.1.0/demo/agent_change/billing_sandbox_tests.py +36 -0
- leangate-0.1.0/demo/layout.ps1 +67 -0
- leangate-0.1.0/demo/run_demo.py +195 -0
- leangate-0.1.0/demo/sample_service/app/__init__.py +0 -0
- leangate-0.1.0/demo/sample_service/app/api.py +10 -0
- leangate-0.1.0/demo/sample_service/app/utils/__init__.py +0 -0
- leangate-0.1.0/demo/sample_service/app/utils/retry.py +21 -0
- leangate-0.1.0/demo/sample_service/pyproject.toml +5 -0
- leangate-0.1.0/editors/vscode/.gitignore +3 -0
- leangate-0.1.0/editors/vscode/.vscodeignore +11 -0
- leangate-0.1.0/editors/vscode/LICENSE +21 -0
- leangate-0.1.0/editors/vscode/README.md +78 -0
- leangate-0.1.0/editors/vscode/package-lock.json +4302 -0
- leangate-0.1.0/editors/vscode/package.json +105 -0
- leangate-0.1.0/editors/vscode/src/extension.ts +350 -0
- leangate-0.1.0/editors/vscode/test/runTest.js +47 -0
- leangate-0.1.0/editors/vscode/test/suite.js +67 -0
- leangate-0.1.0/editors/vscode/tsconfig.json +13 -0
- leangate-0.1.0/examples/AGENTS.md +11 -0
- leangate-0.1.0/examples/claude-settings.json +49 -0
- leangate-0.1.0/examples/codex-config.toml +4 -0
- leangate-0.1.0/examples/mcp.json +9 -0
- leangate-0.1.0/plugins/leangate/.claude-plugin/plugin.json +10 -0
- leangate-0.1.0/plugins/leangate/.mcp.json +9 -0
- leangate-0.1.0/plugins/leangate/README.md +13 -0
- leangate-0.1.0/plugins/leangate/hooks/hooks.json +17 -0
- leangate-0.1.0/pyproject.toml +63 -0
- leangate-0.1.0/scripts/list_models.py +13 -0
- leangate-0.1.0/src/leangate/__init__.py +17 -0
- leangate-0.1.0/src/leangate/__main__.py +192 -0
- leangate-0.1.0/src/leangate/analysis/__init__.py +4 -0
- leangate-0.1.0/src/leangate/analysis/bloat.py +507 -0
- leangate-0.1.0/src/leangate/analysis/deps.py +43 -0
- leangate-0.1.0/src/leangate/analysis/errors.py +389 -0
- leangate-0.1.0/src/leangate/analysis/helpers.py +54 -0
- leangate-0.1.0/src/leangate/analysis/imports.py +168 -0
- leangate-0.1.0/src/leangate/analysis/missing.py +184 -0
- leangate-0.1.0/src/leangate/analysis/questions.py +168 -0
- leangate-0.1.0/src/leangate/analysis/redundancy.py +64 -0
- leangate-0.1.0/src/leangate/analysis/ruff_rules.py +65 -0
- leangate-0.1.0/src/leangate/analysis/scopes.py +31 -0
- leangate-0.1.0/src/leangate/analysis/security.py +440 -0
- leangate-0.1.0/src/leangate/backend.py +67 -0
- leangate-0.1.0/src/leangate/cache.py +68 -0
- leangate-0.1.0/src/leangate/cloud/__init__.py +8 -0
- leangate-0.1.0/src/leangate/cloud/client.py +184 -0
- leangate-0.1.0/src/leangate/cloud/sanitize.py +65 -0
- leangate-0.1.0/src/leangate/config.py +149 -0
- leangate-0.1.0/src/leangate/diff.py +163 -0
- leangate-0.1.0/src/leangate/engine.py +261 -0
- leangate-0.1.0/src/leangate/grounding/__init__.py +4 -0
- leangate-0.1.0/src/leangate/grounding/ground.py +70 -0
- leangate-0.1.0/src/leangate/grounding/registry.py +65 -0
- leangate-0.1.0/src/leangate/grounding/tavily_search.py +61 -0
- leangate-0.1.0/src/leangate/grounding/versions.py +75 -0
- leangate-0.1.0/src/leangate/hooks/__init__.py +0 -0
- leangate-0.1.0/src/leangate/hooks/edit_hook.py +173 -0
- leangate-0.1.0/src/leangate/hooks/install.py +76 -0
- leangate-0.1.0/src/leangate/hooks/prompt_hook.py +66 -0
- leangate-0.1.0/src/leangate/hooks/session_hook.py +47 -0
- leangate-0.1.0/src/leangate/hooks/stop_hook.py +44 -0
- leangate-0.1.0/src/leangate/index/__init__.py +3 -0
- leangate-0.1.0/src/leangate/index/conventions.py +193 -0
- leangate-0.1.0/src/leangate/index/house.py +263 -0
- leangate-0.1.0/src/leangate/index/repo_index.py +580 -0
- leangate-0.1.0/src/leangate/judge/__init__.py +3 -0
- leangate-0.1.0/src/leangate/judge/jev.py +147 -0
- leangate-0.1.0/src/leangate/learning.py +220 -0
- leangate-0.1.0/src/leangate/live/__init__.py +180 -0
- leangate-0.1.0/src/leangate/live/server.py +238 -0
- leangate-0.1.0/src/leangate/live/static/live.html +114 -0
- leangate-0.1.0/src/leangate/live/static/live.js +385 -0
- leangate-0.1.0/src/leangate/llm/__init__.py +3 -0
- leangate-0.1.0/src/leangate/llm/nemotron.py +98 -0
- leangate-0.1.0/src/leangate/lsp/__init__.py +35 -0
- leangate-0.1.0/src/leangate/lsp/convert.py +280 -0
- leangate-0.1.0/src/leangate/lsp/server.py +243 -0
- leangate-0.1.0/src/leangate/mcp_server.py +152 -0
- leangate-0.1.0/src/leangate/memory/__init__.py +3 -0
- leangate-0.1.0/src/leangate/memory/store.py +134 -0
- leangate-0.1.0/src/leangate/models.py +199 -0
- leangate-0.1.0/src/leangate/pipeline.py +843 -0
- leangate-0.1.0/src/leangate/pricing.py +33 -0
- leangate-0.1.0/src/leangate/router.py +121 -0
- leangate-0.1.0/src/leangate/sandbox/__init__.py +3 -0
- leangate-0.1.0/src/leangate/sandbox/runner.py +240 -0
- leangate-0.1.0/src/leangate/service/__init__.py +0 -0
- leangate-0.1.0/src/leangate/service/app.py +246 -0
- leangate-0.1.0/src/leangate/service/limits.py +178 -0
- leangate-0.1.0/src/leangate/service/playground.py +97 -0
- leangate-0.1.0/src/leangate/service/static/app.css +503 -0
- leangate-0.1.0/src/leangate/service/static/app.js +481 -0
- leangate-0.1.0/src/leangate/service/static/example.js +16 -0
- leangate-0.1.0/src/leangate/service/static/favicon.svg +1 -0
- leangate-0.1.0/src/leangate/service/static/index.html +205 -0
- leangate-0.1.0/src/leangate/service/static/mock.js +262 -0
- leangate-0.1.0/src/leangate/service/static/render.js +334 -0
- leangate-0.1.0/tests/conftest.py +97 -0
- leangate-0.1.0/tests/test_checks.py +1047 -0
- leangate-0.1.0/tests/test_cli.py +187 -0
- leangate-0.1.0/tests/test_cloud.py +114 -0
- leangate-0.1.0/tests/test_core_polish.py +56 -0
- leangate-0.1.0/tests/test_cost_and_timing.py +98 -0
- leangate-0.1.0/tests/test_demo.py +30 -0
- leangate-0.1.0/tests/test_detectors.py +523 -0
- leangate-0.1.0/tests/test_edit_hook.py +98 -0
- leangate-0.1.0/tests/test_engine.py +65 -0
- leangate-0.1.0/tests/test_frontend.py +194 -0
- leangate-0.1.0/tests/test_house_rules.py +201 -0
- leangate-0.1.0/tests/test_index.py +280 -0
- leangate-0.1.0/tests/test_judge.py +122 -0
- leangate-0.1.0/tests/test_languages.py +28 -0
- leangate-0.1.0/tests/test_learning.py +287 -0
- leangate-0.1.0/tests/test_live.py +275 -0
- leangate-0.1.0/tests/test_lsp.py +384 -0
- leangate-0.1.0/tests/test_mcp_tools.py +19 -0
- leangate-0.1.0/tests/test_mission_control.py +77 -0
- leangate-0.1.0/tests/test_pipeline.py +277 -0
- leangate-0.1.0/tests/test_reuse_gaps.py +88 -0
- leangate-0.1.0/tests/test_scoping.py +95 -0
- leangate-0.1.0/tests/test_service.py +313 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "leangate",
|
|
3
|
+
"owner": { "name": "lambdabypi", "url": "https://leangates.com" },
|
|
4
|
+
"description": "LeanGate for Claude Code: MCP tools and hooks that keep agent-written code from reimplementing your helpers, skipping error handling, or shipping security holes.",
|
|
5
|
+
"plugins": [
|
|
6
|
+
{
|
|
7
|
+
"name": "leangate",
|
|
8
|
+
"source": "./plugins/leangate",
|
|
9
|
+
"description": "MCP server plus SessionStart, UserPromptSubmit, per-edit and Stop hooks. Needs uv (uvx) on PATH.",
|
|
10
|
+
"category": "code-quality"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Turn a `leangate verify --json` report into a job summary and PR annotations.
|
|
2
|
+
|
|
3
|
+
Used by the composite Action (action.yml). Stdlib only: it runs on the runner's Python.
|
|
4
|
+
|
|
5
|
+
python action_report.py report.json [--summary $GITHUB_STEP_SUMMARY] [--annotate]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _cell(text: str | None) -> str:
|
|
17
|
+
"""Make text safe inside one markdown table cell."""
|
|
18
|
+
return (text or "").replace("|", "\\|").replace("\r", " ").replace("\n", " ").strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _escape(text: str, prop: bool = False) -> str:
|
|
22
|
+
"""Escape a workflow-command value (https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts)."""
|
|
23
|
+
text = text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
|
|
24
|
+
return text.replace(":", "%3A").replace(",", "%2C") if prop else text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def summary(report: dict) -> str:
|
|
28
|
+
findings = report.get("findings", [])
|
|
29
|
+
blocked = report.get("gate") == "block"
|
|
30
|
+
head = "### LeanGate: :no_entry: blocked" if blocked else "### LeanGate: :white_check_mark: passed"
|
|
31
|
+
lines = [head, ""]
|
|
32
|
+
if not findings:
|
|
33
|
+
lines.append("No bloat found in this change.")
|
|
34
|
+
else:
|
|
35
|
+
lines += [f"{len(findings)} finding(s).", "",
|
|
36
|
+
"| Kind | Location | Severity | Decision | Finding | Suggestion |",
|
|
37
|
+
"|---|---|---|---|---|---|"]
|
|
38
|
+
for f in findings:
|
|
39
|
+
d = f.get("decision") or {}
|
|
40
|
+
decision = f"{d.get('choice', '')} ({d.get('action', '')}, {d.get('source', '')})" if d else ""
|
|
41
|
+
lines.append(f"| `{f['kind']}` | `{f['path']}:{f['line']}` | {f.get('severity', 0):.2f} | {_cell(decision)} "
|
|
42
|
+
f"| {_cell(f.get('message'))} | {_cell(f.get('suggestion'))} |")
|
|
43
|
+
if report.get("suppressed"):
|
|
44
|
+
lines += ["", f"{report['suppressed']} previously rejected finding(s) suppressed."]
|
|
45
|
+
return "\n".join(lines) + "\n"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def annotations(report: dict, block_severity: float) -> list[str]:
|
|
49
|
+
"""Findings that caused a block are errors; the rest are warnings."""
|
|
50
|
+
blocked = report.get("gate") == "block"
|
|
51
|
+
out = []
|
|
52
|
+
for f in report.get("findings", []):
|
|
53
|
+
level = "error" if blocked and f.get("severity", 0) >= block_severity else "warning"
|
|
54
|
+
message = f.get("message", "") + (f" -> {f['suggestion']}" if f.get("suggestion") else "")
|
|
55
|
+
out.append(f"::{level} file={_escape(f['path'], True)},line={f['line']},"
|
|
56
|
+
f"title={_escape('LeanGate ' + f['kind'], True)}::{_escape(message)}")
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv: list[str] | None = None) -> None:
|
|
61
|
+
parser = argparse.ArgumentParser()
|
|
62
|
+
parser.add_argument("report")
|
|
63
|
+
parser.add_argument("--summary", help="file to append the markdown summary to")
|
|
64
|
+
parser.add_argument("--annotate", action="store_true", help="print ::error/::warning workflow commands")
|
|
65
|
+
parser.add_argument("--block-severity", type=float,
|
|
66
|
+
default=float(os.getenv("LEANGATE_BLOCK_SEVERITY") or 0.6))
|
|
67
|
+
args = parser.parse_args(argv)
|
|
68
|
+
report = json.loads(Path(args.report).read_text(encoding="utf-8"))
|
|
69
|
+
if args.summary:
|
|
70
|
+
with open(args.summary, "a", encoding="utf-8") as fh:
|
|
71
|
+
fh.write(summary(report))
|
|
72
|
+
if args.annotate and report.get("findings"):
|
|
73
|
+
print("\n".join(annotations(report, args.block_severity)))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: pytest (${{ matrix.os }}, py${{ matrix.python-version }})
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
os: [ubuntu-latest, windows-latest]
|
|
24
|
+
python-version: ["3.11", "3.12"]
|
|
25
|
+
env:
|
|
26
|
+
# Offline: no provider keys in CI, every provider uses its fallback.
|
|
27
|
+
LEANGATE_MOCK: "1"
|
|
28
|
+
PYTHONUTF8: "1"
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: ${{ matrix.python-version }}
|
|
35
|
+
cache: pip
|
|
36
|
+
cache-dependency-path: pyproject.toml
|
|
37
|
+
|
|
38
|
+
- name: Install
|
|
39
|
+
run: python -m pip install -e ".[service,editor,dev]" # editor: pygls, which tests/test_lsp.py needs
|
|
40
|
+
|
|
41
|
+
# The test fixtures build throwaway git repos and commit into them.
|
|
42
|
+
- name: Configure git
|
|
43
|
+
run: |
|
|
44
|
+
git config --global user.email "ci@leangate.invalid"
|
|
45
|
+
git config --global user.name "LeanGate CI"
|
|
46
|
+
git config --global init.defaultBranch main
|
|
47
|
+
|
|
48
|
+
- name: Test
|
|
49
|
+
run: python -m pytest -q
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Runs when a version tag is pushed (git tag v0.1.0 && git push origin v0.1.0). Uses PyPI trusted publishing:
|
|
4
|
+
# no token is stored; PyPI trusts this workflow in this repo, set up once as a "pending publisher" on pypi.org.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- name: Tag matches the package version
|
|
21
|
+
run: |
|
|
22
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
23
|
+
test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != pyproject version $version"; exit 1; }
|
|
24
|
+
- run: python -m pip install build twine
|
|
25
|
+
- run: python -m build
|
|
26
|
+
- run: python -m twine check --strict dist/*
|
|
27
|
+
- uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
publish:
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: pypi
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.env
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Local-only project notes
|
|
10
|
+
docs/
|
|
11
|
+
|
|
12
|
+
# `leangate --json verify` output: findings quote code from whatever repo was checked
|
|
13
|
+
report*.json
|
|
14
|
+
|
|
15
|
+
# VS Code extension build output
|
|
16
|
+
editors/vscode/node_modules/
|
|
17
|
+
editors/vscode/out/
|
|
18
|
+
*.vsix
|
|
19
|
+
leangate.env
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Backstop for agents that skip the MCP tools: check the staged change before it is committed.
|
|
2
|
+
# Fast path (static analysis + Jev, no Nemotron or web grounding). Exit 1 blocks the commit.
|
|
3
|
+
- id: leangate-verify
|
|
4
|
+
name: LeanGate verify (fast)
|
|
5
|
+
description: Block commits that add duplicates, dead code, missing error handling, security holes, or unneeded or unknown dependencies.
|
|
6
|
+
entry: leangate verify --fast --staged
|
|
7
|
+
language: python
|
|
8
|
+
pass_filenames: false
|
|
9
|
+
files: '(\.py|(^|/)pyproject\.toml|(^|/)requirements[^/]*\.txt)$'
|
|
10
|
+
stages: [pre-commit]
|
|
11
|
+
require_serial: true
|
|
12
|
+
minimum_pre_commit_version: "3.2.0"
|
leangate-0.1.0/DEPLOY.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Deploying the LeanGate service on Nebius
|
|
2
|
+
|
|
3
|
+
The service (`leangate-service`, [Dockerfile](Dockerfile)) is one container. It serves the web UI at `/`, the
|
|
4
|
+
playground API at `/api/*`, the engine routes the local MCP server calls through `LEANGATE_REMOTE_URL`, and
|
|
5
|
+
the sandbox that runs untrusted test code. It needs no GPU, because Nemotron runs on Token Factory, not in this container.
|
|
6
|
+
|
|
7
|
+
Two ways to host it:
|
|
8
|
+
- **A. Serverless AI endpoint (recommended):** Nebius runs the container and gives you a managed `https://` URL.
|
|
9
|
+
- **B. Compute VM:** you run Docker on an Ubuntu VM yourself. Use it if the endpoint route is blocked.
|
|
10
|
+
|
|
11
|
+
**Status:** nothing here has been run yet. The commands below are copied from Nebius's official docs (links per
|
|
12
|
+
step, read 2026-10-05). Steps marked **UNVERIFIED** are ones those docs don't cover; confirm them on first deploy.
|
|
13
|
+
No command was guessed. The Docker image has not been built yet either (Docker Desktop wasn't running).
|
|
14
|
+
|
|
15
|
+
## 0. Build and test the image locally
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
docker build -t leangate-service .
|
|
19
|
+
docker run --rm -p 8000:8000 -e LEANGATE_MOCK=1 leangate-service
|
|
20
|
+
curl http://127.0.0.1:8000/health # {"status":"ok", ... "mock": true}
|
|
21
|
+
# then open http://127.0.0.1:8000/ and run the bundled example
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration (environment variables)
|
|
25
|
+
|
|
26
|
+
| Variable | Purpose |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `NEBIUS_API_KEY` | Token Factory key for Nemotron. Unset = fallback mode |
|
|
29
|
+
| `OPENROUTER_API_KEY` (or `TYPESAFE_API_KEY`), `TAVILY_API_KEY`, `MEM0_API_KEY` | Jev (via OpenRouter, or TypeSafe directly), Tavily, and Mem0. Each one is optional |
|
|
30
|
+
| `NEMOTRON_SUPER_MODEL` etc. | Model IDs; check them with `python scripts/list_models.py` |
|
|
31
|
+
| `LEANGATE_SERVICE_TOKEN` | Bearer token the **engine routes** require (`/ground`, `/minimize`, `/prove_equivalent`, `/check_dependency`). Set the same value locally so the MCP server's `RemoteBackend` can call them. Unset = those routes answer 503 (they fail closed) |
|
|
32
|
+
| `LEANGATE_PLAYGROUND_TOKEN_REQUIRED` | `1` puts `/api/*` behind the same token. Default off: judges open the demo URL with no token |
|
|
33
|
+
| `LEANGATE_RATE_PER_MINUTE` | Per-client-IP limit on `/api/*` (default 30; `0` = off). IPv6 clients are counted per /64 |
|
|
34
|
+
| `LEANGATE_MINIMIZE_PER_MINUTE` | Extra, lower per-IP limit on `/api/minimize`, the playground route that runs code (default 6; `0` = off) |
|
|
35
|
+
| `LEANGATE_PLAYGROUND_SANDBOX` | `0` = `/api/minimize` never runs submitted code: static edits are still offered, model rewrites aren't accepted. Default `1` |
|
|
36
|
+
| `LEANGATE_TRUST_PROXY` | `1` = read the client IP from the rightmost `X-Forwarded-For` entry (the one your proxy appended). See the note under step A4 |
|
|
37
|
+
| `LEANGATE_MAX_BODY_BYTES` / `LEANGATE_MAX_FILES` / `LEANGATE_MAX_FILE_BYTES` | Request caps (1 MB / 40 / 200 KB) |
|
|
38
|
+
| `LEANGATE_SANDBOX_TIMEOUT`, `LEANGATE_SANDBOX_MEMORY_MB`, `LEANGATE_SANDBOX_CONCURRENCY` | Sandbox wall/CPU seconds (60), address-space cap (1024 MB), concurrent runs (2) |
|
|
39
|
+
| `LEANGATE_SANDBOX_QUEUE_SECONDS` | How long a run waits for a free sandbox slot before the request gets a 503 with `Retry-After` (10) |
|
|
40
|
+
|
|
41
|
+
## A. Serverless AI endpoint
|
|
42
|
+
|
|
43
|
+
Source: [Serverless endpoints quickstart](https://docs.nebius.com/serverless/quickstart/endpoints),
|
|
44
|
+
[`nebius ai endpoint create` reference](https://docs.nebius.com/cli/reference/ai/endpoint/create),
|
|
45
|
+
[Container Registry quickstart](https://docs.nebius.com/container-registry/quickstart).
|
|
46
|
+
|
|
47
|
+
### A1. Install and configure the CLI
|
|
48
|
+
From [CLI install](https://docs.nebius.com/cli/install) and [CLI quickstart](https://docs.nebius.com/cli/quickstart)
|
|
49
|
+
(Linux/macOS shell; on Windows, run these in WSL. **UNVERIFIED:** whether the CLI has a native Windows installer):
|
|
50
|
+
```bash
|
|
51
|
+
curl -sSL https://artifacts.nebius.cloud/cli/install.sh | bash
|
|
52
|
+
exec -l $SHELL
|
|
53
|
+
nebius version
|
|
54
|
+
nebius profile create # keep the defaults: api.nebius.cloud, federation auth, auth.nebius.com
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### A2. Push the image to Container Registry
|
|
58
|
+
```bash
|
|
59
|
+
nebius registry create --name leangate --format json | jq -r ".metadata.id" | cut -d- -f 2
|
|
60
|
+
# save the printed value as REGISTRY_PATH; REGION_ID is your project's region, e.g. eu-north1
|
|
61
|
+
nebius registry configure-helper # run docker as your user, not root (the helper is per user)
|
|
62
|
+
docker tag leangate-service cr.$REGION_ID.nebius.cloud/$REGISTRY_PATH/leangate-service:v1
|
|
63
|
+
docker push cr.$REGION_ID.nebius.cloud/$REGISTRY_PATH/leangate-service:v1
|
|
64
|
+
```
|
|
65
|
+
**UNVERIFIED:** whether an endpoint can pull from a private Nebius registry in the same project without
|
|
66
|
+
`--registry-username/--registry-password` or `--registry-secret` (a MysteryBox secret with `REGISTRY_USERNAME`
|
|
67
|
+
and `REGISTRY_PASSWORD` keys). If the endpoint fails to pull, add one of those.
|
|
68
|
+
|
|
69
|
+
### A3. Keep the provider keys in MysteryBox, not on the command line
|
|
70
|
+
`--env-secret KEY=SECRET_SELECTOR` reads an env var from MysteryBox (selector = secret name, ID, version ID, or
|
|
71
|
+
`SECRET_ID@VERSION_ID`). **UNVERIFIED:** the exact command to create a MysteryBox secret and which payload key
|
|
72
|
+
gets used for each env var; see the MysteryBox docs. If you're short on time, plain `--env KEY=VALUE` works,
|
|
73
|
+
but the value ends up in your shell history and in the endpoint spec.
|
|
74
|
+
|
|
75
|
+
### A4. Create the endpoint
|
|
76
|
+
**Use endpoint auth `none`.** With `--auth token`, Nebius rejects every request that lacks the token before it
|
|
77
|
+
reaches the container, so judges could not open the UI. The engine routes are still protected by
|
|
78
|
+
`LEANGATE_SERVICE_TOKEN` inside the app.
|
|
79
|
+
```bash
|
|
80
|
+
export LEANGATE_SERVICE_TOKEN=$(openssl rand -hex 32) # also put it in your local .env
|
|
81
|
+
nebius ai endpoint create \
|
|
82
|
+
--name leangate \
|
|
83
|
+
--image cr.$REGION_ID.nebius.cloud/$REGISTRY_PATH/leangate-service:v1 \
|
|
84
|
+
--platform cpu-d3 \
|
|
85
|
+
--preset 4vcpu-16gb \
|
|
86
|
+
--container-port 8000 \
|
|
87
|
+
--auth none \
|
|
88
|
+
--env LEANGATE_SERVICE_TOKEN=$LEANGATE_SERVICE_TOKEN \
|
|
89
|
+
--env-secret NEBIUS_API_KEY=<secret> \
|
|
90
|
+
--env-secret TAVILY_API_KEY=<secret> \
|
|
91
|
+
--env-secret OPENROUTER_API_KEY=<secret>
|
|
92
|
+
```
|
|
93
|
+
- `--platform cpu-d3 --preset 4vcpu-16gb` come from the nginx quickstart. Without `--platform` the default is
|
|
94
|
+
a **GPU** platform (`gpu-h100-sxm` in eu-north1), which this service doesn't need. Always pass it.
|
|
95
|
+
- `--disk-size` defaults to 250Gi. The image is small, so a smaller value is fine. **UNVERIFIED:** the minimum.
|
|
96
|
+
- `--public` only assigns a public IP. The managed `https://` URL is provided either way.
|
|
97
|
+
- **UNVERIFIED:** whether the endpoint proxy sets `X-Forwarded-For`. Until you confirm it (log `request.client`
|
|
98
|
+
from one real request), the rate limit may see every visitor as the proxy's IP and throttle them all together.
|
|
99
|
+
If that happens, set `LEANGATE_TRUST_PROXY=1` once you know the proxy sets the header, or raise `LEANGATE_RATE_PER_MINUTE`.
|
|
100
|
+
|
|
101
|
+
### A5. Get the URL and smoke-test
|
|
102
|
+
```bash
|
|
103
|
+
export ENDPOINT_ID=$(nebius ai endpoint get-by-name --name leangate --format jsonpath='{.metadata.id}')
|
|
104
|
+
export ENDPOINT_URL=$(nebius ai endpoint get "$ENDPOINT_ID" --format json \
|
|
105
|
+
| jq -r '.status.public_endpoints[] | select(startswith("https://"))' | head -1)
|
|
106
|
+
curl "$ENDPOINT_URL/health" # providers should show live: true
|
|
107
|
+
curl -X POST "$ENDPOINT_URL/check_dependency" -H 'content-type: application/json' -d '{"name":"httpx"}' # 401
|
|
108
|
+
curl -X POST "$ENDPOINT_URL/check_dependency" -H "Authorization: Bearer $LEANGATE_SERVICE_TOKEN" \
|
|
109
|
+
-H 'content-type: application/json' -d '{"name":"httpx"}' # 200
|
|
110
|
+
nebius ai endpoint logs $ENDPOINT_ID
|
|
111
|
+
```
|
|
112
|
+
Then open `$ENDPOINT_URL/` in a browser, which is the demo URL. Locally, set `LEANGATE_REMOTE_URL=$ENDPOINT_URL`
|
|
113
|
+
and `LEANGATE_SERVICE_TOKEN` in `.env` so the MCP server uses the remote backend.
|
|
114
|
+
|
|
115
|
+
### A6. After judging: lock down or tear down
|
|
116
|
+
`/api/minimize` is open by default and runs submitted code. The bounds are 2 concurrent runs, a 10 s queue, the
|
|
117
|
+
two rate limits and the rlimits. If you'd rather not run strangers' code at all, deploy with
|
|
118
|
+
`--env LEANGATE_PLAYGROUND_SANDBOX=0` (static rewrites only). Once judging is over, either redeploy with
|
|
119
|
+
`--env LEANGATE_PLAYGROUND_TOKEN_REQUIRED=1` or delete the endpoint:
|
|
120
|
+
```bash
|
|
121
|
+
nebius ai endpoint delete $ENDPOINT_ID # billing stops once the endpoint is deleted
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## B. Compute VM (fallback)
|
|
125
|
+
|
|
126
|
+
Source: [Create a VM](https://docs.nebius.com/compute/virtual-machines/manage),
|
|
127
|
+
[VM networking](https://docs.nebius.com/compute/virtual-machines/network).
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
nebius vpc subnet list # note the subnet ID
|
|
131
|
+
nebius compute instance create \
|
|
132
|
+
--name leangate-vm \
|
|
133
|
+
--resources-platform cpu-d3 \
|
|
134
|
+
--resources-preset cpu-d3-c2-m8 \
|
|
135
|
+
--boot-disk-managed-disk-name leangate-boot \
|
|
136
|
+
--boot-disk-managed-disk-type network_ssd \
|
|
137
|
+
--boot-disk-managed-disk-size-gibibytes 20 \
|
|
138
|
+
--boot-disk-managed-disk-source-image-family-image-family ubuntu24.04-driverless \
|
|
139
|
+
--boot-disk-attach-mode READ_WRITE \
|
|
140
|
+
--network-interfaces '[{"name": "eth0", "ip_address": {}, "public_ip_address": {}, "subnet_id": "<subnet_ID>"}]' \
|
|
141
|
+
--cloud-init-user-data "#cloud-config
|
|
142
|
+
users:
|
|
143
|
+
- name: ubuntu
|
|
144
|
+
sudo: ALL=(ALL) NOPASSWD:ALL
|
|
145
|
+
shell: /bin/bash
|
|
146
|
+
ssh_authorized_keys:
|
|
147
|
+
- $(cat ~/.ssh/id_ed25519.pub)"
|
|
148
|
+
nebius compute instance get leangate-vm --format json | grep public_ip_address
|
|
149
|
+
```
|
|
150
|
+
**UNVERIFIED**, because the docs above don't cover these steps:
|
|
151
|
+
- installing Docker on the VM (use Docker's own Ubuntu instructions),
|
|
152
|
+
- pulling from the Nebius registry on the VM (`nebius registry configure-helper` needs the CLI on the VM),
|
|
153
|
+
- opening port 8000 (or 80/443) in the VPC security rules,
|
|
154
|
+
- TLS. The VM serves plain HTTP unless you put a reverse proxy with a certificate in front of it.
|
|
155
|
+
|
|
156
|
+
Then run it on the VM:
|
|
157
|
+
```bash
|
|
158
|
+
docker run -d --restart unless-stopped -p 80:8000 --env-file leangate.env \
|
|
159
|
+
cr.$REGION_ID.nebius.cloud/$REGISTRY_PATH/leangate-service:v1
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## What the sandbox does and doesn't protect
|
|
163
|
+
|
|
164
|
+
Untrusted test code from `/api/minimize` and `/prove_equivalent` runs as a pytest subprocess in a temp dir, as the
|
|
165
|
+
image's non-root `app` user, with:
|
|
166
|
+
- **Linux rlimits** (checked under WSL Ubuntu): CPU seconds = timeout (a busy loop is killed at 3 s with a 3 s
|
|
167
|
+
timeout), address space 1 GB (a 2 GB allocation raises `MemoryError`), file size 16 MB (`EFBIG`), 256 open
|
|
168
|
+
files, no core dumps, and at most 64 more processes/threads than the `app` user already runs (`RLIMIT_NPROC`;
|
|
169
|
+
checked under WSL with a large memory cap: a 500-thread bomb stops at 35, and at 500 without the limit).
|
|
170
|
+
- **Wall timeout** that kills the whole process group, including children the test spawned (checked: no orphans left).
|
|
171
|
+
- **Network:** a pytest plugin refuses socket connects and DNS, and proxy env vars point at a dead port. On Linux
|
|
172
|
+
it also tries an unprivileged user+network namespace. Under WSL that worked: even a raw `_socket` connect that
|
|
173
|
+
gets past the Python-level guard failed with `Network is unreachable`. **Docker's default seccomp profile usually
|
|
174
|
+
blocks `unshare`, so in the container expect only the Python-level block, which ctypes or a subprocess can bypass.**
|
|
175
|
+
- **Environment:** only PATH and the temp vars are passed; API keys never reach test code. Output is capped at 4 KB.
|
|
176
|
+
- **The server's own keys:** test code runs as the same `app` user as the server, so it could read
|
|
177
|
+
`/proc/<server pid>/environ`, which holds the provider keys. At startup the service sets `PR_SET_DUMPABLE=0`
|
|
178
|
+
(`sandbox.runner.protect_server_process`), which makes its `/proc` entries root-owned. Checked under WSL with the
|
|
179
|
+
namespace step disabled: before the call the read succeeded, after it the read fails with `Permission denied`.
|
|
180
|
+
- **At most 2 concurrent runs** (`LEANGATE_SANDBOX_CONCURRENCY`). Further runs wait up to 10 s, then get a 503.
|
|
181
|
+
|
|
182
|
+
Not protected: the container can still reach the network, because the service itself needs Token Factory, Tavily
|
|
183
|
+
and PyPI. Test code that escapes the Python-level block can make outbound requests, though it holds no keys.
|
|
184
|
+
It can also read files the `app` user can read, which includes the installed source but no secrets, since keys are
|
|
185
|
+
env vars of the server process only. Because it is the same user, it can also write the server's state dir
|
|
186
|
+
(`/home/app/.leangate`: the grounding cache) and send the server process a signal, so a hostile submission can
|
|
187
|
+
restart the service. `LEANGATE_PLAYGROUND_SANDBOX=0` removes that from the public playground; the token-protected
|
|
188
|
+
`/minimize` and `/prove_equivalent` still run code. For real isolation, run the sandbox in a separate network-less container or a
|
|
189
|
+
gVisor/Firecracker runtime. That's out of scope for the hackathon.
|
|
190
|
+
|
|
191
|
+
## Operational notes
|
|
192
|
+
- State: per-request playground state is created under a temp dir and deleted after each request. The engine's
|
|
193
|
+
grounding cache (`$LEANGATE_STATE_DIR/_shared/grounding.json`) is shared and **grows without bound**. It holds only
|
|
194
|
+
public doc verdicts, but on a long-lived deployment you should restart or clear it now and then.
|
|
195
|
+
- The rate limiter is in memory, per process. It resets on restart and is not shared across replicas.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# LeanGate verification service, for Nebius Serverless Endpoints or an AI Cloud VM.
|
|
2
|
+
# The sandbox runs untrusted code inside this container as a non-root user.
|
|
3
|
+
FROM python:3.12-slim
|
|
4
|
+
|
|
5
|
+
# git: the playground (/api/verify) builds a throwaway repo per request.
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
|
7
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
8
|
+
RUN useradd --create-home app
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
COPY pyproject.toml README.md LICENSE ./
|
|
11
|
+
COPY src ./src
|
|
12
|
+
RUN pip install --no-cache-dir ".[service,providers]"
|
|
13
|
+
|
|
14
|
+
USER app
|
|
15
|
+
ENV PORT=8000 LEANGATE_STATE_DIR=/home/app/.leangate PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
|
|
16
|
+
EXPOSE 8000
|
|
17
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
|
18
|
+
CMD python -c "import os, urllib.request; urllib.request.urlopen(f'http://127.0.0.1:{os.environ[\"PORT\"]}/health', timeout=4)"
|
|
19
|
+
CMD ["leangate-service"]
|
leangate-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LeanGate contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|