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.
Files changed (152) hide show
  1. leangate-0.1.0/.claude-plugin/marketplace.json +13 -0
  2. leangate-0.1.0/.dockerignore +17 -0
  3. leangate-0.1.0/.github/scripts/action_report.py +77 -0
  4. leangate-0.1.0/.github/workflows/ci.yml +49 -0
  5. leangate-0.1.0/.github/workflows/publish.yml +43 -0
  6. leangate-0.1.0/.gitignore +19 -0
  7. leangate-0.1.0/.pre-commit-hooks.yaml +12 -0
  8. leangate-0.1.0/DEPLOY.md +195 -0
  9. leangate-0.1.0/Dockerfile +19 -0
  10. leangate-0.1.0/LICENSE +21 -0
  11. leangate-0.1.0/PKG-INFO +556 -0
  12. leangate-0.1.0/README.md +505 -0
  13. leangate-0.1.0/action.yml +137 -0
  14. leangate-0.1.0/bench/RESULTS.md +79 -0
  15. leangate-0.1.0/bench/RESULTS_claude.md +84 -0
  16. leangate-0.1.0/bench/RESULTS_fresh.md +109 -0
  17. leangate-0.1.0/bench/RESULTS_large.md +107 -0
  18. leangate-0.1.0/bench/RESULTS_new.md +63 -0
  19. leangate-0.1.0/bench/agent_bench.py +252 -0
  20. leangate-0.1.0/bench/claude_bench.py +304 -0
  21. leangate-0.1.0/bench/claude_bench_reps.py +415 -0
  22. leangate-0.1.0/bench/tasks_claude.json +270 -0
  23. leangate-0.1.0/bench/tasks_famous.json +26 -0
  24. leangate-0.1.0/bench/tasks_fresh.json +207 -0
  25. leangate-0.1.0/bench/tasks_fresh_reuse.json +57 -0
  26. leangate-0.1.0/bench/tasks_large.json +252 -0
  27. leangate-0.1.0/bench/tasks_new.json +250 -0
  28. leangate-0.1.0/bench/tasks_zulip.json +26 -0
  29. leangate-0.1.0/demo/agent_change/admin_export.py +50 -0
  30. leangate-0.1.0/demo/agent_change/billing.py +51 -0
  31. leangate-0.1.0/demo/agent_change/billing_lean.py +26 -0
  32. leangate-0.1.0/demo/agent_change/billing_sandbox_tests.py +36 -0
  33. leangate-0.1.0/demo/layout.ps1 +67 -0
  34. leangate-0.1.0/demo/run_demo.py +195 -0
  35. leangate-0.1.0/demo/sample_service/app/__init__.py +0 -0
  36. leangate-0.1.0/demo/sample_service/app/api.py +10 -0
  37. leangate-0.1.0/demo/sample_service/app/utils/__init__.py +0 -0
  38. leangate-0.1.0/demo/sample_service/app/utils/retry.py +21 -0
  39. leangate-0.1.0/demo/sample_service/pyproject.toml +5 -0
  40. leangate-0.1.0/editors/vscode/.gitignore +3 -0
  41. leangate-0.1.0/editors/vscode/.vscodeignore +11 -0
  42. leangate-0.1.0/editors/vscode/LICENSE +21 -0
  43. leangate-0.1.0/editors/vscode/README.md +78 -0
  44. leangate-0.1.0/editors/vscode/package-lock.json +4302 -0
  45. leangate-0.1.0/editors/vscode/package.json +105 -0
  46. leangate-0.1.0/editors/vscode/src/extension.ts +350 -0
  47. leangate-0.1.0/editors/vscode/test/runTest.js +47 -0
  48. leangate-0.1.0/editors/vscode/test/suite.js +67 -0
  49. leangate-0.1.0/editors/vscode/tsconfig.json +13 -0
  50. leangate-0.1.0/examples/AGENTS.md +11 -0
  51. leangate-0.1.0/examples/claude-settings.json +49 -0
  52. leangate-0.1.0/examples/codex-config.toml +4 -0
  53. leangate-0.1.0/examples/mcp.json +9 -0
  54. leangate-0.1.0/plugins/leangate/.claude-plugin/plugin.json +10 -0
  55. leangate-0.1.0/plugins/leangate/.mcp.json +9 -0
  56. leangate-0.1.0/plugins/leangate/README.md +13 -0
  57. leangate-0.1.0/plugins/leangate/hooks/hooks.json +17 -0
  58. leangate-0.1.0/pyproject.toml +63 -0
  59. leangate-0.1.0/scripts/list_models.py +13 -0
  60. leangate-0.1.0/src/leangate/__init__.py +17 -0
  61. leangate-0.1.0/src/leangate/__main__.py +192 -0
  62. leangate-0.1.0/src/leangate/analysis/__init__.py +4 -0
  63. leangate-0.1.0/src/leangate/analysis/bloat.py +507 -0
  64. leangate-0.1.0/src/leangate/analysis/deps.py +43 -0
  65. leangate-0.1.0/src/leangate/analysis/errors.py +389 -0
  66. leangate-0.1.0/src/leangate/analysis/helpers.py +54 -0
  67. leangate-0.1.0/src/leangate/analysis/imports.py +168 -0
  68. leangate-0.1.0/src/leangate/analysis/missing.py +184 -0
  69. leangate-0.1.0/src/leangate/analysis/questions.py +168 -0
  70. leangate-0.1.0/src/leangate/analysis/redundancy.py +64 -0
  71. leangate-0.1.0/src/leangate/analysis/ruff_rules.py +65 -0
  72. leangate-0.1.0/src/leangate/analysis/scopes.py +31 -0
  73. leangate-0.1.0/src/leangate/analysis/security.py +440 -0
  74. leangate-0.1.0/src/leangate/backend.py +67 -0
  75. leangate-0.1.0/src/leangate/cache.py +68 -0
  76. leangate-0.1.0/src/leangate/cloud/__init__.py +8 -0
  77. leangate-0.1.0/src/leangate/cloud/client.py +184 -0
  78. leangate-0.1.0/src/leangate/cloud/sanitize.py +65 -0
  79. leangate-0.1.0/src/leangate/config.py +149 -0
  80. leangate-0.1.0/src/leangate/diff.py +163 -0
  81. leangate-0.1.0/src/leangate/engine.py +261 -0
  82. leangate-0.1.0/src/leangate/grounding/__init__.py +4 -0
  83. leangate-0.1.0/src/leangate/grounding/ground.py +70 -0
  84. leangate-0.1.0/src/leangate/grounding/registry.py +65 -0
  85. leangate-0.1.0/src/leangate/grounding/tavily_search.py +61 -0
  86. leangate-0.1.0/src/leangate/grounding/versions.py +75 -0
  87. leangate-0.1.0/src/leangate/hooks/__init__.py +0 -0
  88. leangate-0.1.0/src/leangate/hooks/edit_hook.py +173 -0
  89. leangate-0.1.0/src/leangate/hooks/install.py +76 -0
  90. leangate-0.1.0/src/leangate/hooks/prompt_hook.py +66 -0
  91. leangate-0.1.0/src/leangate/hooks/session_hook.py +47 -0
  92. leangate-0.1.0/src/leangate/hooks/stop_hook.py +44 -0
  93. leangate-0.1.0/src/leangate/index/__init__.py +3 -0
  94. leangate-0.1.0/src/leangate/index/conventions.py +193 -0
  95. leangate-0.1.0/src/leangate/index/house.py +263 -0
  96. leangate-0.1.0/src/leangate/index/repo_index.py +580 -0
  97. leangate-0.1.0/src/leangate/judge/__init__.py +3 -0
  98. leangate-0.1.0/src/leangate/judge/jev.py +147 -0
  99. leangate-0.1.0/src/leangate/learning.py +220 -0
  100. leangate-0.1.0/src/leangate/live/__init__.py +180 -0
  101. leangate-0.1.0/src/leangate/live/server.py +238 -0
  102. leangate-0.1.0/src/leangate/live/static/live.html +114 -0
  103. leangate-0.1.0/src/leangate/live/static/live.js +385 -0
  104. leangate-0.1.0/src/leangate/llm/__init__.py +3 -0
  105. leangate-0.1.0/src/leangate/llm/nemotron.py +98 -0
  106. leangate-0.1.0/src/leangate/lsp/__init__.py +35 -0
  107. leangate-0.1.0/src/leangate/lsp/convert.py +280 -0
  108. leangate-0.1.0/src/leangate/lsp/server.py +243 -0
  109. leangate-0.1.0/src/leangate/mcp_server.py +152 -0
  110. leangate-0.1.0/src/leangate/memory/__init__.py +3 -0
  111. leangate-0.1.0/src/leangate/memory/store.py +134 -0
  112. leangate-0.1.0/src/leangate/models.py +199 -0
  113. leangate-0.1.0/src/leangate/pipeline.py +843 -0
  114. leangate-0.1.0/src/leangate/pricing.py +33 -0
  115. leangate-0.1.0/src/leangate/router.py +121 -0
  116. leangate-0.1.0/src/leangate/sandbox/__init__.py +3 -0
  117. leangate-0.1.0/src/leangate/sandbox/runner.py +240 -0
  118. leangate-0.1.0/src/leangate/service/__init__.py +0 -0
  119. leangate-0.1.0/src/leangate/service/app.py +246 -0
  120. leangate-0.1.0/src/leangate/service/limits.py +178 -0
  121. leangate-0.1.0/src/leangate/service/playground.py +97 -0
  122. leangate-0.1.0/src/leangate/service/static/app.css +503 -0
  123. leangate-0.1.0/src/leangate/service/static/app.js +481 -0
  124. leangate-0.1.0/src/leangate/service/static/example.js +16 -0
  125. leangate-0.1.0/src/leangate/service/static/favicon.svg +1 -0
  126. leangate-0.1.0/src/leangate/service/static/index.html +205 -0
  127. leangate-0.1.0/src/leangate/service/static/mock.js +262 -0
  128. leangate-0.1.0/src/leangate/service/static/render.js +334 -0
  129. leangate-0.1.0/tests/conftest.py +97 -0
  130. leangate-0.1.0/tests/test_checks.py +1047 -0
  131. leangate-0.1.0/tests/test_cli.py +187 -0
  132. leangate-0.1.0/tests/test_cloud.py +114 -0
  133. leangate-0.1.0/tests/test_core_polish.py +56 -0
  134. leangate-0.1.0/tests/test_cost_and_timing.py +98 -0
  135. leangate-0.1.0/tests/test_demo.py +30 -0
  136. leangate-0.1.0/tests/test_detectors.py +523 -0
  137. leangate-0.1.0/tests/test_edit_hook.py +98 -0
  138. leangate-0.1.0/tests/test_engine.py +65 -0
  139. leangate-0.1.0/tests/test_frontend.py +194 -0
  140. leangate-0.1.0/tests/test_house_rules.py +201 -0
  141. leangate-0.1.0/tests/test_index.py +280 -0
  142. leangate-0.1.0/tests/test_judge.py +122 -0
  143. leangate-0.1.0/tests/test_languages.py +28 -0
  144. leangate-0.1.0/tests/test_learning.py +287 -0
  145. leangate-0.1.0/tests/test_live.py +275 -0
  146. leangate-0.1.0/tests/test_lsp.py +384 -0
  147. leangate-0.1.0/tests/test_mcp_tools.py +19 -0
  148. leangate-0.1.0/tests/test_mission_control.py +77 -0
  149. leangate-0.1.0/tests/test_pipeline.py +277 -0
  150. leangate-0.1.0/tests/test_reuse_gaps.py +88 -0
  151. leangate-0.1.0/tests/test_scoping.py +95 -0
  152. 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,17 @@
1
+ .venv
2
+ .git
3
+ .github
4
+ **/__pycache__
5
+ *.pyc
6
+ *.egg-info
7
+ .pytest_cache
8
+ .env
9
+ .env.*
10
+ .leangate
11
+ tests
12
+ demo
13
+ docs
14
+ scripts
15
+ editors
16
+ *.vsix
17
+ leangate.env
@@ -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"
@@ -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.