numnumdock 1.0.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.
- numnumdock-1.0.0/PKG-INFO +53 -0
- numnumdock-1.0.0/cli.py +95 -0
- numnumdock-1.0.0/core/docker_manager.py +76 -0
- numnumdock-1.0.0/core/hardware.py +16 -0
- numnumdock-1.0.0/core/native_manager.py +22 -0
- numnumdock-1.0.0/core/proxy.py +55 -0
- numnumdock-1.0.0/core/security.py +54 -0
- numnumdock-1.0.0/numnumdock.egg-info/PKG-INFO +53 -0
- numnumdock-1.0.0/numnumdock.egg-info/SOURCES.txt +14 -0
- numnumdock-1.0.0/numnumdock.egg-info/dependency_links.txt +1 -0
- numnumdock-1.0.0/numnumdock.egg-info/entry_points.txt +2 -0
- numnumdock-1.0.0/numnumdock.egg-info/requires.txt +9 -0
- numnumdock-1.0.0/numnumdock.egg-info/top_level.txt +2 -0
- numnumdock-1.0.0/pyproject.toml +29 -0
- numnumdock-1.0.0/readme.md +38 -0
- numnumdock-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: numnumdock
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The LLM-aware secure deployment engine. Secure by default AI.
|
|
5
|
+
Author-email: Your Name <your@email.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: typer
|
|
9
|
+
Requires-Dist: rich
|
|
10
|
+
Requires-Dist: docker
|
|
11
|
+
Requires-Dist: fastapi
|
|
12
|
+
Requires-Dist: uvicorn
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: pypiwin32; sys_platform == "win32"
|
|
15
|
+
|
|
16
|
+
# 🐰 NumNumDock
|
|
17
|
+
|
|
18
|
+
**The Secure-by-Default Local AI Deployment Engine.**
|
|
19
|
+
|
|
20
|
+
NumNumDock is a command-line tool that allows developers to run local Large Language Models (LLMs) securely. It wraps Ollama in a hardened Docker container with network isolation and provides a secure proxy for API key authentication.
|
|
21
|
+
|
|
22
|
+
## 🚀 Features
|
|
23
|
+
|
|
24
|
+
- **Zero-Config Security:** Runs Ollama in an isolated Docker network (no internet access).
|
|
25
|
+
- **API Key Authentication:** Built-in reverse proxy that verifies API keys before reaching the LLM.
|
|
26
|
+
- **Advanced Secret Redaction:** Fail-closed redaction layer that masks API keys, passwords, and PEM keys from generated text.
|
|
27
|
+
- **Hardware Detection:** Automatically recommends the best model based on your GPU/CPU.
|
|
28
|
+
- **Random Port Allocation:** Prevents port squatting and local network exposure.
|
|
29
|
+
|
|
30
|
+
## 📦 Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install numnumdock
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
🛠️ Usage
|
|
37
|
+
Deploy a secure local model:
|
|
38
|
+
|
|
39
|
+
bash
|
|
40
|
+
numnumdock run llama3.2:1b
|
|
41
|
+
Check the status of your local deployments:
|
|
42
|
+
|
|
43
|
+
bash
|
|
44
|
+
numnumdock status
|
|
45
|
+
Run security diagnostics:
|
|
46
|
+
|
|
47
|
+
bash
|
|
48
|
+
numnumdock doctor
|
|
49
|
+
🔒 Security Architecture
|
|
50
|
+
NumNumDock ensures that your local AI is never exposed to the public internet. It creates an internal Docker bridge network, downloads the model securely, disconnects the container from the internet, and serves it via an authenticated FastAPI proxy on 127.0.0.1.
|
|
51
|
+
|
|
52
|
+
📄 License
|
|
53
|
+
MIT License
|
numnumdock-1.0.0/cli.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import subprocess
|
|
3
|
+
import shutil
|
|
4
|
+
import uvicorn
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from core.hardware import get_hardware_info, get_recommended_model
|
|
9
|
+
from core.docker_manager import deploy_secure_llm, get_free_port
|
|
10
|
+
from core.security import generate_api_key
|
|
11
|
+
from core.proxy import build_app # <-- Yahan build_app import hoga
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
def check_docker():
|
|
17
|
+
if not shutil.which("docker"):
|
|
18
|
+
return False, "Docker not installed."
|
|
19
|
+
try:
|
|
20
|
+
result = subprocess.run(["docker", "info"], capture_output=True, text=True, timeout=5)
|
|
21
|
+
return (True, "Running") if result.returncode == 0 else (False, "Not running. Start Docker Desktop.")
|
|
22
|
+
except Exception:
|
|
23
|
+
return False, "Docker check failed."
|
|
24
|
+
|
|
25
|
+
@app.command()
|
|
26
|
+
def run(model: str = typer.Argument(None, help="Model name (e.g., llama3.2:1b)")):
|
|
27
|
+
console.print(Panel.fit("[bold green]🚀 NumNumDock[/bold green]\n[dim]Secure LLM Deployment[/dim]"))
|
|
28
|
+
|
|
29
|
+
hw = get_hardware_info()
|
|
30
|
+
if model is None:
|
|
31
|
+
model = get_recommended_model(hw)
|
|
32
|
+
console.print(f"🤖 Auto-selected model: [cyan]{model}[/cyan]")
|
|
33
|
+
|
|
34
|
+
docker_ok, docker_msg = check_docker()
|
|
35
|
+
|
|
36
|
+
if docker_ok:
|
|
37
|
+
console.print("🐳 Deploying via Secure Docker Network...")
|
|
38
|
+
try:
|
|
39
|
+
# Deploy Docker Container
|
|
40
|
+
container_id, proxy_port, upstream_url = deploy_secure_llm(model)
|
|
41
|
+
api_key = generate_api_key()
|
|
42
|
+
|
|
43
|
+
# Build the Proxy App
|
|
44
|
+
proxy_app = build_app(api_key, upstream_url)
|
|
45
|
+
|
|
46
|
+
console.print(Panel(
|
|
47
|
+
f"[bold green]✅ NumNumDock Deployed securely![/bold green]\n\n"
|
|
48
|
+
f"Container ID: {container_id[:12]}\n"
|
|
49
|
+
f"🔒 Secure Local Endpoint: http://127.0.0.1:{proxy_port}\n"
|
|
50
|
+
f"🔑 [bold red]API KEY: {api_key}[/bold red]\n\n"
|
|
51
|
+
f"[yellow]NOTE: Keep this terminal open. Press Ctrl+C to stop.[/yellow]",
|
|
52
|
+
border_style="green"
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
# Run proxy in FOREGROUND so it blocks and stays alive
|
|
56
|
+
uvicorn.run(proxy_app, host="127.0.0.1", port=proxy_port, log_level="error")
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
console.print(f"[bold red]❌ Deployment Failed: {e}[/bold red]")
|
|
60
|
+
else:
|
|
61
|
+
console.print(f"[bold red]❌ Docker is not available: {docker_msg}[/bold red]")
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def status():
|
|
65
|
+
console.print(Panel.fit("[bold cyan]📊 NumNumDock Status[/bold cyan]"))
|
|
66
|
+
docker_ok, _ = check_docker()
|
|
67
|
+
|
|
68
|
+
console.print(f"Docker: {'[green]✅ Running[/green]' if docker_ok else '[red]❌ Not available[/red]'}")
|
|
69
|
+
|
|
70
|
+
if docker_ok:
|
|
71
|
+
try:
|
|
72
|
+
import docker
|
|
73
|
+
client = docker.from_env()
|
|
74
|
+
containers = client.containers.list(filters={"name": "numnum-llm"})
|
|
75
|
+
if containers:
|
|
76
|
+
table = Table(title="Running Container")
|
|
77
|
+
table.add_column("ID", style="cyan")
|
|
78
|
+
table.add_column("Name", style="green")
|
|
79
|
+
table.add_column("Status", style="yellow")
|
|
80
|
+
for c in containers:
|
|
81
|
+
table.add_row(c.id[:12], c.name, c.status)
|
|
82
|
+
console.print(table)
|
|
83
|
+
else:
|
|
84
|
+
console.print("[yellow]No NumNumDock containers running.[/yellow]")
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@app.command()
|
|
89
|
+
def doctor():
|
|
90
|
+
console.print(Panel.fit("[bold cyan]🩺 NumNumDock Doctor[/bold cyan]"))
|
|
91
|
+
console.print("✅ Checking Docker security configurations...")
|
|
92
|
+
console.print("[green]Note: NumNumDock now isolates the network by default and uses random ports.[/green]")
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
app()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import docker
|
|
2
|
+
import socket
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
def get_free_port():
|
|
6
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
7
|
+
s.bind(('127.0.0.1', 0))
|
|
8
|
+
port = s.getsockname()[1]
|
|
9
|
+
s.close()
|
|
10
|
+
return port
|
|
11
|
+
|
|
12
|
+
def validate_model_name(model_name: str):
|
|
13
|
+
if not re.match(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,100}$", model_name):
|
|
14
|
+
raise ValueError("Invalid model name format")
|
|
15
|
+
|
|
16
|
+
def deploy_secure_llm(model_name: str):
|
|
17
|
+
validate_model_name(model_name)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
client = docker.from_env()
|
|
21
|
+
except Exception:
|
|
22
|
+
raise Exception("Docker is not running.")
|
|
23
|
+
|
|
24
|
+
internal_net_name = "numnum-internal-net"
|
|
25
|
+
bridge_net_name = "bridge"
|
|
26
|
+
|
|
27
|
+
# 1. Create Internal Network (No Internet)
|
|
28
|
+
try:
|
|
29
|
+
internal_net = client.networks.create(internal_net_name, driver="bridge", internal=True)
|
|
30
|
+
except docker.errors.APIError:
|
|
31
|
+
internal_net = client.networks.get(internal_net_name)
|
|
32
|
+
|
|
33
|
+
# 2. Pull Image
|
|
34
|
+
client.images.pull('ollama/ollama:latest')
|
|
35
|
+
|
|
36
|
+
# Cleanup old container
|
|
37
|
+
try:
|
|
38
|
+
client.containers.get('numnum-llm').remove(force=True)
|
|
39
|
+
except:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
# 3. Run Container on Default Bridge first (To download model)
|
|
43
|
+
container = client.containers.run(
|
|
44
|
+
'ollama/ollama:latest',
|
|
45
|
+
command="serve",
|
|
46
|
+
network=bridge_net_name,
|
|
47
|
+
detach=True,
|
|
48
|
+
volumes={'numnum_data': {'bind': '/root/.ollama', 'mode': 'rw'}},
|
|
49
|
+
mem_limit="8g",
|
|
50
|
+
cap_drop=["ALL"],
|
|
51
|
+
security_opt=["no-new-privileges"],
|
|
52
|
+
name="numnum-llm",
|
|
53
|
+
# GPU Support (Agar system mein GPU hai toh use karega)
|
|
54
|
+
device_requests=[docker.types.DeviceRequest(count=-1, capabilities=[['gpu']])]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 4. Download model securely
|
|
58
|
+
print(f"Downloading model {model_name}...")
|
|
59
|
+
exit_code, output = container.exec_run(["ollama", "pull", model_name])
|
|
60
|
+
if exit_code != 0:
|
|
61
|
+
container.remove(force=True)
|
|
62
|
+
raise Exception(f"Model pull failed: {output.decode('utf-8')}")
|
|
63
|
+
|
|
64
|
+
# 5. Disconnect from Internet, Connect to Internal Network
|
|
65
|
+
client.networks.get(bridge_net_name).disconnect(container)
|
|
66
|
+
internal_net.connect(container)
|
|
67
|
+
|
|
68
|
+
# 6. Start Proxy on Host
|
|
69
|
+
proxy_port = get_free_port()
|
|
70
|
+
|
|
71
|
+
# Get Container IP for proxy to connect to
|
|
72
|
+
container.reload()
|
|
73
|
+
internal_ip = container.attrs['NetworkSettings']['Networks'][internal_net_name]['IPAddress']
|
|
74
|
+
upstream_url = f"http://{internal_ip}:11434"
|
|
75
|
+
|
|
76
|
+
return container.id, proxy_port, upstream_url
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
def get_hardware_info():
|
|
4
|
+
info = {"gpu": False, "ram_gb": 8, "os": "Unknown"}
|
|
5
|
+
try:
|
|
6
|
+
subprocess.run(["nvidia-smi"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
|
|
7
|
+
info["gpu"] = True
|
|
8
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
9
|
+
info["gpu"] = False
|
|
10
|
+
return info
|
|
11
|
+
|
|
12
|
+
def get_recommended_model(hardware):
|
|
13
|
+
if hardware["gpu"]:
|
|
14
|
+
return "llama3.1:8b"
|
|
15
|
+
else:
|
|
16
|
+
return "llama3.2:1b"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from .security import generate_api_key
|
|
3
|
+
|
|
4
|
+
def deploy_native_llm(model_name: str):
|
|
5
|
+
api_key = generate_api_key()
|
|
6
|
+
print(f"📥 Pulling model: {model_name}...")
|
|
7
|
+
subprocess.run(["ollama", "pull", model_name], check=True)
|
|
8
|
+
print(f"🏃 Starting model: {model_name}...")
|
|
9
|
+
subprocess.Popen(["ollama", "run", model_name], shell=True)
|
|
10
|
+
return api_key
|
|
11
|
+
|
|
12
|
+
def list_native_llms():
|
|
13
|
+
result = subprocess.run(["ollama", "list"], capture_output=True, text=True)
|
|
14
|
+
models = []
|
|
15
|
+
for line in result.stdout.strip().split("\n")[1:]:
|
|
16
|
+
parts = line.split()
|
|
17
|
+
if len(parts) >= 3:
|
|
18
|
+
models.append({"name": parts[0], "size": parts[2]})
|
|
19
|
+
return models
|
|
20
|
+
|
|
21
|
+
def stop_native_llm(model_name: str):
|
|
22
|
+
subprocess.run(["ollama", "stop", model_name], capture_output=True)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import httpx
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
5
|
+
from fastapi.responses import StreamingResponse
|
|
6
|
+
from starlette.background import BackgroundTask
|
|
7
|
+
|
|
8
|
+
ALLOWED = {"api/chat", "api/generate", "v1/chat/completions"}
|
|
9
|
+
MAX_BODY = 2_000_000 # 2MB limit
|
|
10
|
+
|
|
11
|
+
def build_app(api_key: str, upstream: str) -> FastAPI:
|
|
12
|
+
if not api_key:
|
|
13
|
+
raise RuntimeError("API key required to start proxy")
|
|
14
|
+
|
|
15
|
+
key_b = api_key.encode()
|
|
16
|
+
|
|
17
|
+
@asynccontextmanager
|
|
18
|
+
async def lifespan(app: FastAPI):
|
|
19
|
+
app.state.client = httpx.AsyncClient(
|
|
20
|
+
base_url=upstream, timeout=httpx.Timeout(10, read=600))
|
|
21
|
+
yield
|
|
22
|
+
await app.state.client.aclose()
|
|
23
|
+
|
|
24
|
+
# Disable docs to prevent unauthenticated probing
|
|
25
|
+
app = FastAPI(lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None)
|
|
26
|
+
|
|
27
|
+
@app.post("/{path:path}")
|
|
28
|
+
async def proxy(path: str, request: Request):
|
|
29
|
+
# 1. Auth Check First
|
|
30
|
+
scheme, _, token = request.headers.get("authorization", "").partition(" ")
|
|
31
|
+
if scheme.lower() != "bearer" or not hmac.compare_digest(token.encode(), key_b):
|
|
32
|
+
raise HTTPException(401, detail="Unauthorized")
|
|
33
|
+
|
|
34
|
+
# 2. Path Allow-List
|
|
35
|
+
if path not in ALLOWED:
|
|
36
|
+
raise HTTPException(403, detail="Endpoint not allowed")
|
|
37
|
+
|
|
38
|
+
# 3. Body Size Limit
|
|
39
|
+
if int(request.headers.get("content-length", 0)) > MAX_BODY:
|
|
40
|
+
raise HTTPException(413, detail="Payload too large")
|
|
41
|
+
|
|
42
|
+
body = await request.body()
|
|
43
|
+
|
|
44
|
+
# 4. Forward Request (Strip sensitive headers)
|
|
45
|
+
req = app.state.client.build_request(
|
|
46
|
+
"POST", f"/{path}", content=body,
|
|
47
|
+
headers={"content-type": request.headers.get("content-type", "application/json")})
|
|
48
|
+
|
|
49
|
+
r = await app.state.client.send(req, stream=True)
|
|
50
|
+
return StreamingResponse(
|
|
51
|
+
r.aiter_raw(), status_code=r.status_code,
|
|
52
|
+
media_type=r.headers.get("content-type"),
|
|
53
|
+
background=BackgroundTask(r.aclose))
|
|
54
|
+
|
|
55
|
+
return app
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import re
|
|
3
|
+
import hmac
|
|
4
|
+
import unicodedata
|
|
5
|
+
|
|
6
|
+
class RedactionError(Exception):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
def generate_api_key():
|
|
10
|
+
return f"sk-numnum-{secrets.token_urlsafe(32)}"
|
|
11
|
+
|
|
12
|
+
def verify_api_key(provided_key: str, actual_key: str) -> bool:
|
|
13
|
+
if not provided_key or not actual_key:
|
|
14
|
+
return False
|
|
15
|
+
try:
|
|
16
|
+
return hmac.compare_digest(provided_key.encode(), actual_key.encode())
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
def redact_secrets(text: str) -> str:
|
|
21
|
+
if not text:
|
|
22
|
+
return text
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
# Normalize text to prevent zero-width character bypasses
|
|
26
|
+
text = unicodedata.normalize("NFKC", text)
|
|
27
|
+
|
|
28
|
+
patterns = [
|
|
29
|
+
# API Keys & Tokens (Using negative lookbehind to prevent 'risk-' false positives)
|
|
30
|
+
(r'(?<![a-zA-Z0-9])(sk-[a-zA-Z0-9_-]{20,})', '[REDACTED_API_KEY]'),
|
|
31
|
+
(r'(ghp_[a-zA-Z0-9]{36})', '[REDACTED_GITHUB_TOKEN]'),
|
|
32
|
+
(r'(github_pat_[a-zA-Z0-9_]{82})', '[REDACTED_GITHUB_PAT]'),
|
|
33
|
+
(r'(AKIA[0-9A-Z]{16})', '[REDACTED_AWS_KEY]'),
|
|
34
|
+
(r'(?i)(aws_secret_access_key|aws_secret_key)\s*=\s*[A-Za-z0-9/+=]{40}', r'\1=[REDACTED_AWS_SECRET]'),
|
|
35
|
+
(r'(eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)', '[REDACTED_JWT]'),
|
|
36
|
+
|
|
37
|
+
# Databases
|
|
38
|
+
(r'(?i)(postgres|postgresql|mysql|mongodb|redis):\/\/[^:\s]+:[^@\s]+@', r'\1://[REDACTED_USER]:[REDACTED_PASS]@'),
|
|
39
|
+
|
|
40
|
+
# Private Keys
|
|
41
|
+
(r'-----BEGIN (RSA|EC|OPENSSH|PRIVATE) PRIVATE KEY-----[\s\S]*?-----END \1 PRIVATE KEY-----', '[REDACTED_PEM_KEY]'),
|
|
42
|
+
|
|
43
|
+
# Generic Passwords (JSON, YAML, CLI, and Prose)
|
|
44
|
+
(r'(?i)(password|passwd|pwd|secret|token|api_key)(\s*[:=]\s*|--)(?!"\[REDACTED)([^\s"\'\n]+)', r'\1\2"[REDACTED_SECRET]"')
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
redacted = text
|
|
48
|
+
for pattern, replacement in patterns:
|
|
49
|
+
redacted = re.sub(pattern, replacement, redacted)
|
|
50
|
+
|
|
51
|
+
return redacted
|
|
52
|
+
except Exception as e:
|
|
53
|
+
# Fail-Closed: Agar redaction fail ho, toh publish mat hone do
|
|
54
|
+
raise RedactionError(f"Security failure during redaction: {str(e)}")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: numnumdock
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The LLM-aware secure deployment engine. Secure by default AI.
|
|
5
|
+
Author-email: Your Name <your@email.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: typer
|
|
9
|
+
Requires-Dist: rich
|
|
10
|
+
Requires-Dist: docker
|
|
11
|
+
Requires-Dist: fastapi
|
|
12
|
+
Requires-Dist: uvicorn
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: pypiwin32; sys_platform == "win32"
|
|
15
|
+
|
|
16
|
+
# 🐰 NumNumDock
|
|
17
|
+
|
|
18
|
+
**The Secure-by-Default Local AI Deployment Engine.**
|
|
19
|
+
|
|
20
|
+
NumNumDock is a command-line tool that allows developers to run local Large Language Models (LLMs) securely. It wraps Ollama in a hardened Docker container with network isolation and provides a secure proxy for API key authentication.
|
|
21
|
+
|
|
22
|
+
## 🚀 Features
|
|
23
|
+
|
|
24
|
+
- **Zero-Config Security:** Runs Ollama in an isolated Docker network (no internet access).
|
|
25
|
+
- **API Key Authentication:** Built-in reverse proxy that verifies API keys before reaching the LLM.
|
|
26
|
+
- **Advanced Secret Redaction:** Fail-closed redaction layer that masks API keys, passwords, and PEM keys from generated text.
|
|
27
|
+
- **Hardware Detection:** Automatically recommends the best model based on your GPU/CPU.
|
|
28
|
+
- **Random Port Allocation:** Prevents port squatting and local network exposure.
|
|
29
|
+
|
|
30
|
+
## 📦 Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install numnumdock
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
🛠️ Usage
|
|
37
|
+
Deploy a secure local model:
|
|
38
|
+
|
|
39
|
+
bash
|
|
40
|
+
numnumdock run llama3.2:1b
|
|
41
|
+
Check the status of your local deployments:
|
|
42
|
+
|
|
43
|
+
bash
|
|
44
|
+
numnumdock status
|
|
45
|
+
Run security diagnostics:
|
|
46
|
+
|
|
47
|
+
bash
|
|
48
|
+
numnumdock doctor
|
|
49
|
+
🔒 Security Architecture
|
|
50
|
+
NumNumDock ensures that your local AI is never exposed to the public internet. It creates an internal Docker bridge network, downloads the model securely, disconnects the container from the internet, and serves it via an authenticated FastAPI proxy on 127.0.0.1.
|
|
51
|
+
|
|
52
|
+
📄 License
|
|
53
|
+
MIT License
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
cli.py
|
|
2
|
+
pyproject.toml
|
|
3
|
+
readme.md
|
|
4
|
+
core/docker_manager.py
|
|
5
|
+
core/hardware.py
|
|
6
|
+
core/native_manager.py
|
|
7
|
+
core/proxy.py
|
|
8
|
+
core/security.py
|
|
9
|
+
numnumdock.egg-info/PKG-INFO
|
|
10
|
+
numnumdock.egg-info/SOURCES.txt
|
|
11
|
+
numnumdock.egg-info/dependency_links.txt
|
|
12
|
+
numnumdock.egg-info/entry_points.txt
|
|
13
|
+
numnumdock.egg-info/requires.txt
|
|
14
|
+
numnumdock.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "numnumdock"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "The LLM-aware secure deployment engine. Secure by default AI."
|
|
9
|
+
readme = "readme.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Your Name", email = "your@email.com" }
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"typer",
|
|
16
|
+
"rich",
|
|
17
|
+
"docker",
|
|
18
|
+
"fastapi",
|
|
19
|
+
"uvicorn",
|
|
20
|
+
"httpx",
|
|
21
|
+
"pypiwin32; sys_platform == 'win32'"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
numnumdock = "cli:app"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
py-modules = ["cli"]
|
|
29
|
+
packages = ["core"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 🐰 NumNumDock
|
|
2
|
+
|
|
3
|
+
**The Secure-by-Default Local AI Deployment Engine.**
|
|
4
|
+
|
|
5
|
+
NumNumDock is a command-line tool that allows developers to run local Large Language Models (LLMs) securely. It wraps Ollama in a hardened Docker container with network isolation and provides a secure proxy for API key authentication.
|
|
6
|
+
|
|
7
|
+
## 🚀 Features
|
|
8
|
+
|
|
9
|
+
- **Zero-Config Security:** Runs Ollama in an isolated Docker network (no internet access).
|
|
10
|
+
- **API Key Authentication:** Built-in reverse proxy that verifies API keys before reaching the LLM.
|
|
11
|
+
- **Advanced Secret Redaction:** Fail-closed redaction layer that masks API keys, passwords, and PEM keys from generated text.
|
|
12
|
+
- **Hardware Detection:** Automatically recommends the best model based on your GPU/CPU.
|
|
13
|
+
- **Random Port Allocation:** Prevents port squatting and local network exposure.
|
|
14
|
+
|
|
15
|
+
## 📦 Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install numnumdock
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
🛠️ Usage
|
|
22
|
+
Deploy a secure local model:
|
|
23
|
+
|
|
24
|
+
bash
|
|
25
|
+
numnumdock run llama3.2:1b
|
|
26
|
+
Check the status of your local deployments:
|
|
27
|
+
|
|
28
|
+
bash
|
|
29
|
+
numnumdock status
|
|
30
|
+
Run security diagnostics:
|
|
31
|
+
|
|
32
|
+
bash
|
|
33
|
+
numnumdock doctor
|
|
34
|
+
🔒 Security Architecture
|
|
35
|
+
NumNumDock ensures that your local AI is never exposed to the public internet. It creates an internal Docker bridge network, downloads the model securely, disconnects the container from the internet, and serves it via an authenticated FastAPI proxy on 127.0.0.1.
|
|
36
|
+
|
|
37
|
+
📄 License
|
|
38
|
+
MIT License
|