@mcoda/mswarm 0.1.108 → 0.1.109
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.
- package/dist/codali-executor.d.ts.map +1 -1
- package/dist/codali-executor.js +17 -3
- package/dist/codali-executor.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +55 -2
- package/dist/runtime.js.map +1 -1
- package/dist/vendor/codali/agents/AgentInventory.d.ts +56 -0
- package/dist/vendor/codali/agents/AgentInventory.d.ts.map +1 -0
- package/dist/vendor/codali/agents/AgentInventory.js +131 -0
- package/dist/vendor/codali/agents/RoleResolution.d.ts +49 -0
- package/dist/vendor/codali/agents/RoleResolution.d.ts.map +1 -0
- package/dist/vendor/codali/agents/RoleResolution.js +166 -0
- package/dist/vendor/codali/api/ChatCompletionsAdapter.d.ts +76 -0
- package/dist/vendor/codali/api/ChatCompletionsAdapter.d.ts.map +1 -0
- package/dist/vendor/codali/api/ChatCompletionsAdapter.js +54 -0
- package/dist/vendor/codali/api/CodaliApi.d.ts +78 -0
- package/dist/vendor/codali/api/CodaliApi.d.ts.map +1 -0
- package/dist/vendor/codali/api/CodaliApi.js +273 -0
- package/dist/vendor/codali/api/CodaliServer.d.ts +42 -0
- package/dist/vendor/codali/api/CodaliServer.d.ts.map +1 -0
- package/dist/vendor/codali/api/CodaliServer.js +118 -0
- package/dist/vendor/codali/cli/AskCommand.d.ts +66 -0
- package/dist/vendor/codali/cli/AskCommand.d.ts.map +1 -0
- package/dist/vendor/codali/cli/AskCommand.js +536 -0
- package/dist/vendor/codali/cli/AuthCommand.d.ts +19 -0
- package/dist/vendor/codali/cli/AuthCommand.d.ts.map +1 -0
- package/dist/vendor/codali/cli/AuthCommand.js +161 -0
- package/dist/vendor/codali/cli/ServeCommand.d.ts +5 -0
- package/dist/vendor/codali/cli/ServeCommand.d.ts.map +1 -0
- package/dist/vendor/codali/cli/ServeCommand.js +87 -0
- package/dist/vendor/codali/cli/ToolsCommand.d.ts +22 -0
- package/dist/vendor/codali/cli/ToolsCommand.d.ts.map +1 -0
- package/dist/vendor/codali/cli/ToolsCommand.js +208 -0
- package/dist/vendor/codali/cli.d.ts.map +1 -1
- package/dist/vendor/codali/cli.js +29 -1
- package/dist/vendor/codali/connectors/http/HttpToolDefinition.d.ts +74 -0
- package/dist/vendor/codali/connectors/http/HttpToolDefinition.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/http/HttpToolDefinition.js +190 -0
- package/dist/vendor/codali/connectors/http/HttpToolSource.d.ts +18 -0
- package/dist/vendor/codali/connectors/http/HttpToolSource.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/http/HttpToolSource.js +29 -0
- package/dist/vendor/codali/connectors/mcp/McpClient.d.ts +132 -0
- package/dist/vendor/codali/connectors/mcp/McpClient.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/mcp/McpClient.js +215 -0
- package/dist/vendor/codali/connectors/mcp/McpServerRegistry.d.ts +75 -0
- package/dist/vendor/codali/connectors/mcp/McpServerRegistry.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/mcp/McpServerRegistry.js +180 -0
- package/dist/vendor/codali/connectors/mcp/McpToolAdapter.d.ts +48 -0
- package/dist/vendor/codali/connectors/mcp/McpToolAdapter.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/mcp/McpToolAdapter.js +101 -0
- package/dist/vendor/codali/connectors/mcp/McpToolSource.d.ts +27 -0
- package/dist/vendor/codali/connectors/mcp/McpToolSource.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/mcp/McpToolSource.js +33 -0
- package/dist/vendor/codali/connectors/media/ImageGenerationTool.d.ts +43 -0
- package/dist/vendor/codali/connectors/media/ImageGenerationTool.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/media/ImageGenerationTool.js +122 -0
- package/dist/vendor/codali/connectors/oauth/DeviceCodeAuth.d.ts +74 -0
- package/dist/vendor/codali/connectors/oauth/DeviceCodeAuth.d.ts.map +1 -0
- package/dist/vendor/codali/connectors/oauth/DeviceCodeAuth.js +153 -0
- package/dist/vendor/codali/docdex/DocdexClient.d.ts +2 -0
- package/dist/vendor/codali/docdex/DocdexClient.d.ts.map +1 -1
- package/dist/vendor/codali/docdex/DocdexClient.js +4 -0
- package/dist/vendor/codali/gateway/CodaliGateway.d.ts +51 -3
- package/dist/vendor/codali/gateway/CodaliGateway.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/CodaliGateway.js +226 -2
- package/dist/vendor/codali/gateway/CodaliGatewayTypes.d.ts +34 -0
- package/dist/vendor/codali/gateway/CodaliGatewayTypes.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/EvidenceNormalizer.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/EvidenceNormalizer.js +90 -1
- package/dist/vendor/codali/gateway/Finalizer.d.ts +50 -0
- package/dist/vendor/codali/gateway/Finalizer.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/Finalizer.js +79 -0
- package/dist/vendor/codali/gateway/GatewayPlanner.d.ts +13 -0
- package/dist/vendor/codali/gateway/GatewayPlanner.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/GatewayPlanner.js +81 -16
- package/dist/vendor/codali/gateway/GatewayStateMachine.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/GatewayStateMachine.js +6 -0
- package/dist/vendor/codali/gateway/GatewayTracer.d.ts +93 -0
- package/dist/vendor/codali/gateway/GatewayTracer.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/GatewayTracer.js +214 -0
- package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts +46 -0
- package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/LocalGatewayProvider.js +161 -0
- package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.d.ts +79 -0
- package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.js +354 -0
- package/dist/vendor/codali/gateway/RelevanceJudge.d.ts +14 -0
- package/dist/vendor/codali/gateway/RelevanceJudge.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/RelevanceJudge.js +87 -0
- package/dist/vendor/codali/gateway/ResponseSchema.d.ts +32 -0
- package/dist/vendor/codali/gateway/ResponseSchema.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/ResponseSchema.js +120 -0
- package/dist/vendor/codali/gateway/TemporalContext.d.ts +36 -0
- package/dist/vendor/codali/gateway/TemporalContext.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/TemporalContext.js +115 -0
- package/dist/vendor/codali/gateway/ToolCapabilityCompiler.d.ts +5 -0
- package/dist/vendor/codali/gateway/ToolCapabilityCompiler.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/ToolCapabilityCompiler.js +27 -2
- package/dist/vendor/codali/gateway/ToolDescriptorSource.d.ts +6 -0
- package/dist/vendor/codali/gateway/ToolDescriptorSource.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/ToolDescriptorSource.js +30 -0
- package/dist/vendor/codali/gateway/ToolExposure.d.ts +36 -0
- package/dist/vendor/codali/gateway/ToolExposure.d.ts.map +1 -0
- package/dist/vendor/codali/gateway/ToolExposure.js +154 -0
- package/dist/vendor/codali/index.d.ts +57 -0
- package/dist/vendor/codali/index.d.ts.map +1 -1
- package/dist/vendor/codali/index.js +45 -0
- package/dist/vendor/codali/providers/ClaudeCliProvider.d.ts +2 -0
- package/dist/vendor/codali/providers/ClaudeCliProvider.d.ts.map +1 -1
- package/dist/vendor/codali/providers/ClaudeCliProvider.js +2 -0
- package/dist/vendor/codali/providers/CodexCliProvider.d.ts +2 -0
- package/dist/vendor/codali/providers/CodexCliProvider.d.ts.map +1 -1
- package/dist/vendor/codali/providers/CodexCliProvider.js +2 -0
- package/dist/vendor/codali/providers/MswarmWorkerProvider.d.ts +2 -0
- package/dist/vendor/codali/providers/MswarmWorkerProvider.d.ts.map +1 -1
- package/dist/vendor/codali/providers/MswarmWorkerProvider.js +2 -0
- package/dist/vendor/codali/providers/OllamaRemoteProvider.d.ts +2 -0
- package/dist/vendor/codali/providers/OllamaRemoteProvider.d.ts.map +1 -1
- package/dist/vendor/codali/providers/OllamaRemoteProvider.js +2 -0
- package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts +2 -0
- package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
- package/dist/vendor/codali/providers/OpenAiCompatibleProvider.js +2 -0
- package/dist/vendor/codali/providers/ProviderTypes.d.ts +13 -0
- package/dist/vendor/codali/providers/ProviderTypes.d.ts.map +1 -1
- package/dist/vendor/codali/providers/ProviderTypes.js +1 -1
- package/dist/vendor/codali/runcontext/CredentialFile.d.ts +12 -0
- package/dist/vendor/codali/runcontext/CredentialFile.d.ts.map +1 -0
- package/dist/vendor/codali/runcontext/CredentialFile.js +81 -0
- package/dist/vendor/codali/runcontext/RunContextResolver.d.ts +126 -0
- package/dist/vendor/codali/runcontext/RunContextResolver.d.ts.map +1 -0
- package/dist/vendor/codali/runcontext/RunContextResolver.js +389 -0
- package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.d.ts +1 -1
- package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.d.ts.map +1 -1
- package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.js +12 -0
- package/dist/vendor/codali/tools/ToolRegistry.d.ts +14 -1
- package/dist/vendor/codali/tools/ToolRegistry.d.ts.map +1 -1
- package/dist/vendor/codali/tools/ToolRegistry.js +38 -1
- package/dist/vendor/codali/tools/ToolTypes.d.ts +35 -0
- package/dist/vendor/codali/tools/ToolTypes.d.ts.map +1 -1
- package/dist/vendor/codali/tools/ToolTypes.js +21 -0
- package/dist/vendor/codali/tools/TruncateResult.d.ts +29 -0
- package/dist/vendor/codali/tools/TruncateResult.d.ts.map +1 -0
- package/dist/vendor/codali/tools/TruncateResult.js +91 -0
- package/dist/vendor/codali/tools/diff/DiffTool.d.ts.map +1 -1
- package/dist/vendor/codali/tools/diff/DiffTool.js +2 -0
- package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts +21 -1
- package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts.map +1 -1
- package/dist/vendor/codali/tools/docdex/DocdexTools.js +108 -16
- package/dist/vendor/codali/tools/filesystem/FileTools.d.ts.map +1 -1
- package/dist/vendor/codali/tools/filesystem/FileTools.js +8 -0
- package/dist/vendor/codali/tools/search/SearchTool.d.ts.map +1 -1
- package/dist/vendor/codali/tools/search/SearchTool.js +3 -1
- package/dist/vendor/codali/tools/shell/ShellTool.d.ts.map +1 -1
- package/dist/vendor/codali/tools/shell/ShellTool.js +2 -0
- package/package.json +3 -3
- package/scripts/stable-diffusion-cpp-auth-guard.py +475 -0
- package/scripts/stable-diffusion-cpp-openai-server.py +44 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Authenticated loopback guard for a stable-diffusion.cpp OpenAI server."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import http.client
|
|
8
|
+
import ipaddress
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import os
|
|
12
|
+
import secrets
|
|
13
|
+
import signal
|
|
14
|
+
import socket
|
|
15
|
+
import stat
|
|
16
|
+
import threading
|
|
17
|
+
import urllib.parse
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
20
|
+
from socketserver import TCPServer
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
25
|
+
DEFAULT_PORT = 11445
|
|
26
|
+
DEFAULT_UPSTREAM_BASE_URL = "http://127.0.0.1:11451"
|
|
27
|
+
DEFAULT_MAX_REQUEST_BYTES = 2_097_152
|
|
28
|
+
DEFAULT_MAX_RESPONSE_BYTES = 134_217_728
|
|
29
|
+
DEFAULT_CLIENT_READ_TIMEOUT_SECONDS = 10.0
|
|
30
|
+
DEFAULT_UPSTREAM_TIMEOUT_SECONDS = 5.0
|
|
31
|
+
DEFAULT_GENERATION_TIMEOUT_SECONDS = 1_800.0
|
|
32
|
+
DEFAULT_MAX_HANDLER_THREADS = 8
|
|
33
|
+
ALLOWED_ENDPOINTS = {
|
|
34
|
+
("GET", "/v1/models"),
|
|
35
|
+
("POST", "/v1/images/generations"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class GuardState:
|
|
41
|
+
upstream_host: str
|
|
42
|
+
upstream_port: int
|
|
43
|
+
api_key: bytes
|
|
44
|
+
max_request_bytes: int
|
|
45
|
+
max_response_bytes: int
|
|
46
|
+
upstream_timeout_seconds: float
|
|
47
|
+
generation_timeout_seconds: float
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GuardHttpServer(ThreadingHTTPServer):
|
|
51
|
+
daemon_threads = True
|
|
52
|
+
request_queue_size = 8
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
server_address: tuple[str, int],
|
|
57
|
+
request_handler_class: type[BaseHTTPRequestHandler],
|
|
58
|
+
*,
|
|
59
|
+
client_read_timeout_seconds: float,
|
|
60
|
+
max_handler_threads: int,
|
|
61
|
+
bind_and_activate: bool = True,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.client_read_timeout_seconds = client_read_timeout_seconds
|
|
64
|
+
self._handler_slots = threading.BoundedSemaphore(max_handler_threads)
|
|
65
|
+
super().__init__(server_address, request_handler_class, bind_and_activate)
|
|
66
|
+
|
|
67
|
+
def server_bind(self) -> None:
|
|
68
|
+
TCPServer.server_bind(self)
|
|
69
|
+
host, port = self.server_address[:2]
|
|
70
|
+
self.server_name = str(host)
|
|
71
|
+
self.server_port = int(port)
|
|
72
|
+
|
|
73
|
+
def get_request(self) -> tuple[socket.socket, Any]:
|
|
74
|
+
request, client_address = super().get_request()
|
|
75
|
+
request.settimeout(self.client_read_timeout_seconds)
|
|
76
|
+
return request, client_address
|
|
77
|
+
|
|
78
|
+
def process_request(self, request: socket.socket, client_address: Any) -> None:
|
|
79
|
+
if not self._handler_slots.acquire(blocking=False):
|
|
80
|
+
try:
|
|
81
|
+
body = b'{"error":{"message":"server is busy","type":"server_error","code":"server_busy"}}'
|
|
82
|
+
request.sendall(
|
|
83
|
+
b"HTTP/1.1 503 Service Unavailable\r\n"
|
|
84
|
+
b"Content-Type: application/json\r\n"
|
|
85
|
+
+ f"Content-Length: {len(body)}\r\n".encode("ascii")
|
|
86
|
+
+ b"Connection: close\r\n\r\n"
|
|
87
|
+
+ body
|
|
88
|
+
)
|
|
89
|
+
except OSError:
|
|
90
|
+
pass
|
|
91
|
+
finally:
|
|
92
|
+
self.shutdown_request(request)
|
|
93
|
+
return
|
|
94
|
+
try:
|
|
95
|
+
super().process_request(request, client_address)
|
|
96
|
+
except BaseException:
|
|
97
|
+
self._handler_slots.release()
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
def process_request_thread(self, request: socket.socket, client_address: Any) -> None:
|
|
101
|
+
try:
|
|
102
|
+
super().process_request_thread(request, client_address)
|
|
103
|
+
finally:
|
|
104
|
+
self._handler_slots.release()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _read_api_key_file(path: str) -> bytes:
|
|
108
|
+
flags = os.O_RDONLY
|
|
109
|
+
flags |= getattr(os, "O_CLOEXEC", 0)
|
|
110
|
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
111
|
+
try:
|
|
112
|
+
descriptor = os.open(path, flags)
|
|
113
|
+
except OSError as error:
|
|
114
|
+
raise SystemExit("--api-key-file cannot be opened safely") from error
|
|
115
|
+
try:
|
|
116
|
+
metadata = os.fstat(descriptor)
|
|
117
|
+
if not stat.S_ISREG(metadata.st_mode):
|
|
118
|
+
raise SystemExit("--api-key-file must be a regular file")
|
|
119
|
+
if os.name == "posix":
|
|
120
|
+
if metadata.st_uid != os.geteuid():
|
|
121
|
+
raise SystemExit("--api-key-file must be owned by the service user")
|
|
122
|
+
if stat.S_IMODE(metadata.st_mode) & 0o077:
|
|
123
|
+
raise SystemExit("--api-key-file must not be accessible by group or others")
|
|
124
|
+
raw = os.read(descriptor, 4_097)
|
|
125
|
+
if len(raw) > 4_096 or os.read(descriptor, 1):
|
|
126
|
+
raise SystemExit("--api-key-file is too large")
|
|
127
|
+
finally:
|
|
128
|
+
os.close(descriptor)
|
|
129
|
+
try:
|
|
130
|
+
api_key = raw.decode("ascii").strip()
|
|
131
|
+
except UnicodeDecodeError as error:
|
|
132
|
+
raise SystemExit("--api-key-file must contain an ASCII token") from error
|
|
133
|
+
if not 32 <= len(api_key) <= 4_096 or any(character.isspace() for character in api_key):
|
|
134
|
+
raise SystemExit("--api-key-file contains an invalid token")
|
|
135
|
+
return api_key.encode("ascii")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _validate_loopback_ip(value: str, option: str) -> str:
|
|
139
|
+
try:
|
|
140
|
+
address = ipaddress.ip_address(value)
|
|
141
|
+
except ValueError as error:
|
|
142
|
+
raise SystemExit(f"{option} must be a loopback IP address") from error
|
|
143
|
+
if not address.is_loopback:
|
|
144
|
+
raise SystemExit(f"{option} must be a loopback IP address")
|
|
145
|
+
return str(address)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_upstream(value: str) -> tuple[str, int]:
|
|
149
|
+
parsed = urllib.parse.urlsplit(value)
|
|
150
|
+
try:
|
|
151
|
+
host = _validate_loopback_ip(parsed.hostname or "", "--upstream-base-url")
|
|
152
|
+
port = parsed.port
|
|
153
|
+
except ValueError as error:
|
|
154
|
+
raise SystemExit("--upstream-base-url must be a loopback HTTP origin") from error
|
|
155
|
+
if (
|
|
156
|
+
parsed.scheme != "http"
|
|
157
|
+
or port is None
|
|
158
|
+
or parsed.username
|
|
159
|
+
or parsed.password
|
|
160
|
+
or parsed.query
|
|
161
|
+
or parsed.fragment
|
|
162
|
+
or parsed.path not in ("", "/")
|
|
163
|
+
):
|
|
164
|
+
raise SystemExit("--upstream-base-url must be a loopback HTTP origin")
|
|
165
|
+
return host, port
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _handler_for(state: GuardState) -> type[BaseHTTPRequestHandler]:
|
|
169
|
+
class Handler(BaseHTTPRequestHandler):
|
|
170
|
+
protocol_version = "HTTP/1.1"
|
|
171
|
+
server_version = "mcoda-sd-auth-guard"
|
|
172
|
+
sys_version = ""
|
|
173
|
+
|
|
174
|
+
def log_message(self, format_string: str, *args: Any) -> None:
|
|
175
|
+
del format_string, args
|
|
176
|
+
|
|
177
|
+
def _send_json_error(
|
|
178
|
+
self,
|
|
179
|
+
status: int,
|
|
180
|
+
message: str,
|
|
181
|
+
code: str,
|
|
182
|
+
*,
|
|
183
|
+
error_type: str = "server_error",
|
|
184
|
+
authenticate: bool = False,
|
|
185
|
+
) -> None:
|
|
186
|
+
body = json.dumps(
|
|
187
|
+
{"error": {"message": message, "type": error_type, "code": code}},
|
|
188
|
+
separators=(",", ":"),
|
|
189
|
+
).encode("utf-8")
|
|
190
|
+
self.send_response(status)
|
|
191
|
+
self.send_header("Content-Type", "application/json")
|
|
192
|
+
self.send_header("Content-Length", str(len(body)))
|
|
193
|
+
if authenticate:
|
|
194
|
+
self.send_header("WWW-Authenticate", "Bearer")
|
|
195
|
+
self.send_header("Connection", "close")
|
|
196
|
+
self.end_headers()
|
|
197
|
+
try:
|
|
198
|
+
self.wfile.write(body)
|
|
199
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
200
|
+
pass
|
|
201
|
+
self.close_connection = True
|
|
202
|
+
|
|
203
|
+
def _authorized(self) -> bool:
|
|
204
|
+
header = self.headers.get("Authorization", "")
|
|
205
|
+
scheme, separator, token = header.partition(" ")
|
|
206
|
+
try:
|
|
207
|
+
candidate = token.encode("ascii")
|
|
208
|
+
except UnicodeEncodeError:
|
|
209
|
+
candidate = b""
|
|
210
|
+
matches = secrets.compare_digest(candidate, state.api_key)
|
|
211
|
+
return separator == " " and scheme.lower() == "bearer" and matches
|
|
212
|
+
|
|
213
|
+
def _require_authentication(self) -> bool:
|
|
214
|
+
if self._authorized():
|
|
215
|
+
return True
|
|
216
|
+
self._send_json_error(
|
|
217
|
+
401,
|
|
218
|
+
"invalid authentication credentials",
|
|
219
|
+
"invalid_api_key",
|
|
220
|
+
error_type="authentication_error",
|
|
221
|
+
authenticate=True,
|
|
222
|
+
)
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
def _read_request_body(self) -> bytes | None:
|
|
226
|
+
if self.headers.get("Transfer-Encoding") is not None:
|
|
227
|
+
self._send_json_error(
|
|
228
|
+
400,
|
|
229
|
+
"transfer encoding is unsupported",
|
|
230
|
+
"unsupported_transfer_encoding",
|
|
231
|
+
error_type="invalid_request_error",
|
|
232
|
+
)
|
|
233
|
+
return None
|
|
234
|
+
content_length = self.headers.get("Content-Length")
|
|
235
|
+
if content_length is None:
|
|
236
|
+
self._send_json_error(
|
|
237
|
+
411,
|
|
238
|
+
"content-length is required",
|
|
239
|
+
"content_length_required",
|
|
240
|
+
error_type="invalid_request_error",
|
|
241
|
+
)
|
|
242
|
+
return None
|
|
243
|
+
try:
|
|
244
|
+
length = int(content_length, 10)
|
|
245
|
+
except ValueError:
|
|
246
|
+
length = -1
|
|
247
|
+
if length < 0:
|
|
248
|
+
self._send_json_error(
|
|
249
|
+
400,
|
|
250
|
+
"content-length is invalid",
|
|
251
|
+
"invalid_content_length",
|
|
252
|
+
error_type="invalid_request_error",
|
|
253
|
+
)
|
|
254
|
+
return None
|
|
255
|
+
if length > state.max_request_bytes:
|
|
256
|
+
self._send_json_error(
|
|
257
|
+
413,
|
|
258
|
+
"request body is too large",
|
|
259
|
+
"request_too_large",
|
|
260
|
+
error_type="invalid_request_error",
|
|
261
|
+
)
|
|
262
|
+
return None
|
|
263
|
+
try:
|
|
264
|
+
body = self.rfile.read(length)
|
|
265
|
+
except (OSError, TimeoutError):
|
|
266
|
+
self._send_json_error(408, "request body timed out", "request_timeout")
|
|
267
|
+
return None
|
|
268
|
+
if len(body) != length:
|
|
269
|
+
self._send_json_error(400, "request body is incomplete", "incomplete_request")
|
|
270
|
+
return None
|
|
271
|
+
return body
|
|
272
|
+
|
|
273
|
+
def _proxy(self, method: str, path: str, body: bytes | None) -> None:
|
|
274
|
+
timeout = (
|
|
275
|
+
state.generation_timeout_seconds
|
|
276
|
+
if method == "POST"
|
|
277
|
+
else state.upstream_timeout_seconds
|
|
278
|
+
)
|
|
279
|
+
connection = http.client.HTTPConnection(
|
|
280
|
+
state.upstream_host,
|
|
281
|
+
state.upstream_port,
|
|
282
|
+
timeout=timeout,
|
|
283
|
+
)
|
|
284
|
+
headers = {"Accept": self.headers.get("Accept", "application/json")}
|
|
285
|
+
if body is not None:
|
|
286
|
+
headers["Content-Type"] = self.headers.get(
|
|
287
|
+
"Content-Type", "application/json"
|
|
288
|
+
)
|
|
289
|
+
headers["Content-Length"] = str(len(body))
|
|
290
|
+
try:
|
|
291
|
+
connection.request(method, path, body=body, headers=headers)
|
|
292
|
+
response = connection.getresponse()
|
|
293
|
+
raw = response.read(state.max_response_bytes + 1)
|
|
294
|
+
status = response.status
|
|
295
|
+
content_type = response.getheader("Content-Type")
|
|
296
|
+
retry_after = response.getheader("Retry-After")
|
|
297
|
+
except (OSError, http.client.HTTPException):
|
|
298
|
+
self._send_json_error(
|
|
299
|
+
502,
|
|
300
|
+
"stable-diffusion.cpp is unavailable",
|
|
301
|
+
"upstream_unavailable",
|
|
302
|
+
)
|
|
303
|
+
return
|
|
304
|
+
finally:
|
|
305
|
+
connection.close()
|
|
306
|
+
if len(raw) > state.max_response_bytes:
|
|
307
|
+
self._send_json_error(
|
|
308
|
+
502,
|
|
309
|
+
"stable-diffusion.cpp response is too large",
|
|
310
|
+
"upstream_response_too_large",
|
|
311
|
+
)
|
|
312
|
+
return
|
|
313
|
+
self.send_response(status)
|
|
314
|
+
if content_type is not None:
|
|
315
|
+
self.send_header("Content-Type", content_type)
|
|
316
|
+
if retry_after is not None:
|
|
317
|
+
self.send_header("Retry-After", retry_after)
|
|
318
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
319
|
+
self.send_header("Connection", "close")
|
|
320
|
+
self.end_headers()
|
|
321
|
+
try:
|
|
322
|
+
self.wfile.write(raw)
|
|
323
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
324
|
+
pass
|
|
325
|
+
self.close_connection = True
|
|
326
|
+
|
|
327
|
+
def do_GET(self) -> None:
|
|
328
|
+
if not self._require_authentication():
|
|
329
|
+
return
|
|
330
|
+
if ("GET", self.path) not in ALLOWED_ENDPOINTS:
|
|
331
|
+
self._send_json_error(
|
|
332
|
+
404,
|
|
333
|
+
"endpoint not found",
|
|
334
|
+
"not_found",
|
|
335
|
+
error_type="not_found_error",
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
self._proxy("GET", self.path, None)
|
|
339
|
+
|
|
340
|
+
def do_POST(self) -> None:
|
|
341
|
+
if not self._require_authentication():
|
|
342
|
+
return
|
|
343
|
+
if ("POST", self.path) not in ALLOWED_ENDPOINTS:
|
|
344
|
+
self._send_json_error(
|
|
345
|
+
404,
|
|
346
|
+
"endpoint not found",
|
|
347
|
+
"not_found",
|
|
348
|
+
error_type="not_found_error",
|
|
349
|
+
)
|
|
350
|
+
return
|
|
351
|
+
body = self._read_request_body()
|
|
352
|
+
if body is None:
|
|
353
|
+
return
|
|
354
|
+
self._proxy("POST", self.path, body)
|
|
355
|
+
|
|
356
|
+
def _method_not_allowed(self) -> None:
|
|
357
|
+
if not self._require_authentication():
|
|
358
|
+
return
|
|
359
|
+
self._send_json_error(
|
|
360
|
+
405,
|
|
361
|
+
"method not allowed",
|
|
362
|
+
"method_not_allowed",
|
|
363
|
+
error_type="invalid_request_error",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
do_DELETE = _method_not_allowed
|
|
367
|
+
do_HEAD = _method_not_allowed
|
|
368
|
+
do_OPTIONS = _method_not_allowed
|
|
369
|
+
do_PATCH = _method_not_allowed
|
|
370
|
+
do_PUT = _method_not_allowed
|
|
371
|
+
|
|
372
|
+
return Handler
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _parse_args() -> argparse.Namespace:
|
|
376
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
377
|
+
parser.add_argument("--host", default=DEFAULT_HOST)
|
|
378
|
+
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
|
379
|
+
parser.add_argument("--upstream-base-url", default=DEFAULT_UPSTREAM_BASE_URL)
|
|
380
|
+
parser.add_argument("--api-key-file", required=True)
|
|
381
|
+
parser.add_argument(
|
|
382
|
+
"--max-request-bytes", type=int, default=DEFAULT_MAX_REQUEST_BYTES
|
|
383
|
+
)
|
|
384
|
+
parser.add_argument(
|
|
385
|
+
"--max-response-bytes", type=int, default=DEFAULT_MAX_RESPONSE_BYTES
|
|
386
|
+
)
|
|
387
|
+
parser.add_argument(
|
|
388
|
+
"--client-read-timeout-seconds",
|
|
389
|
+
type=float,
|
|
390
|
+
default=DEFAULT_CLIENT_READ_TIMEOUT_SECONDS,
|
|
391
|
+
)
|
|
392
|
+
parser.add_argument(
|
|
393
|
+
"--upstream-timeout-seconds",
|
|
394
|
+
type=float,
|
|
395
|
+
default=DEFAULT_UPSTREAM_TIMEOUT_SECONDS,
|
|
396
|
+
)
|
|
397
|
+
parser.add_argument(
|
|
398
|
+
"--generation-timeout-seconds",
|
|
399
|
+
type=float,
|
|
400
|
+
default=DEFAULT_GENERATION_TIMEOUT_SECONDS,
|
|
401
|
+
)
|
|
402
|
+
parser.add_argument(
|
|
403
|
+
"--max-handler-threads", type=int, default=DEFAULT_MAX_HANDLER_THREADS
|
|
404
|
+
)
|
|
405
|
+
return parser.parse_args()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def main() -> int:
|
|
409
|
+
args = _parse_args()
|
|
410
|
+
host = _validate_loopback_ip(args.host, "--host")
|
|
411
|
+
if not 1 <= args.port <= 65_535 and args.port != 0:
|
|
412
|
+
raise SystemExit("--port must be 0 or between 1 and 65535")
|
|
413
|
+
upstream_host, upstream_port = _validate_upstream(args.upstream_base_url)
|
|
414
|
+
if args.port != 0 and host == upstream_host and args.port == upstream_port:
|
|
415
|
+
raise SystemExit("guard and upstream must use different loopback endpoints")
|
|
416
|
+
api_key = _read_api_key_file(args.api_key_file)
|
|
417
|
+
if args.max_request_bytes <= 0 or args.max_response_bytes <= 0:
|
|
418
|
+
raise SystemExit("request and response limits must be positive")
|
|
419
|
+
if args.max_handler_threads <= 0:
|
|
420
|
+
raise SystemExit("--max-handler-threads must be positive")
|
|
421
|
+
for value, name in (
|
|
422
|
+
(args.client_read_timeout_seconds, "client read timeout"),
|
|
423
|
+
(args.upstream_timeout_seconds, "upstream timeout"),
|
|
424
|
+
(args.generation_timeout_seconds, "generation timeout"),
|
|
425
|
+
):
|
|
426
|
+
if not math.isfinite(value) or value <= 0:
|
|
427
|
+
raise SystemExit(f"{name} must be positive")
|
|
428
|
+
|
|
429
|
+
state = GuardState(
|
|
430
|
+
upstream_host=upstream_host,
|
|
431
|
+
upstream_port=upstream_port,
|
|
432
|
+
api_key=api_key,
|
|
433
|
+
max_request_bytes=args.max_request_bytes,
|
|
434
|
+
max_response_bytes=args.max_response_bytes,
|
|
435
|
+
upstream_timeout_seconds=args.upstream_timeout_seconds,
|
|
436
|
+
generation_timeout_seconds=args.generation_timeout_seconds,
|
|
437
|
+
)
|
|
438
|
+
server = GuardHttpServer(
|
|
439
|
+
(host, args.port),
|
|
440
|
+
_handler_for(state),
|
|
441
|
+
client_read_timeout_seconds=args.client_read_timeout_seconds,
|
|
442
|
+
max_handler_threads=args.max_handler_threads,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def request_shutdown(signum: int, frame: Any) -> None:
|
|
446
|
+
del signum, frame
|
|
447
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
448
|
+
|
|
449
|
+
signal.signal(signal.SIGTERM, request_shutdown)
|
|
450
|
+
signal.signal(signal.SIGINT, request_shutdown)
|
|
451
|
+
bound_host, bound_port = server.server_address[:2]
|
|
452
|
+
print(
|
|
453
|
+
json.dumps(
|
|
454
|
+
{
|
|
455
|
+
"event": "listening",
|
|
456
|
+
"host": bound_host,
|
|
457
|
+
"port": bound_port,
|
|
458
|
+
"upstream": f"http://{upstream_host}:{upstream_port}",
|
|
459
|
+
"authentication": "bearer-file",
|
|
460
|
+
"max_request_bytes": state.max_request_bytes,
|
|
461
|
+
"max_response_bytes": state.max_response_bytes,
|
|
462
|
+
},
|
|
463
|
+
separators=(",", ":"),
|
|
464
|
+
),
|
|
465
|
+
flush=True,
|
|
466
|
+
)
|
|
467
|
+
try:
|
|
468
|
+
server.serve_forever(poll_interval=0.5)
|
|
469
|
+
finally:
|
|
470
|
+
server.server_close()
|
|
471
|
+
return 0
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
if __name__ == "__main__":
|
|
475
|
+
raise SystemExit(main())
|
|
@@ -10,10 +10,12 @@ import ipaddress
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import math
|
|
13
|
+
import os
|
|
13
14
|
import re
|
|
14
15
|
import secrets
|
|
15
16
|
import signal
|
|
16
17
|
import socket
|
|
18
|
+
import stat
|
|
17
19
|
import struct
|
|
18
20
|
import threading
|
|
19
21
|
import time
|
|
@@ -121,6 +123,7 @@ class UpstreamTimeout(UpstreamError):
|
|
|
121
123
|
@dataclass
|
|
122
124
|
class ServerState:
|
|
123
125
|
upstream_base_url: str
|
|
126
|
+
upstream_api_key: str | None
|
|
124
127
|
model_id: str
|
|
125
128
|
upstream_model_id: str
|
|
126
129
|
max_request_bytes: int
|
|
@@ -431,9 +434,12 @@ def _bounded_json_request(
|
|
|
431
434
|
timeout: float | None,
|
|
432
435
|
max_bytes: int,
|
|
433
436
|
payload: dict[str, Any] | None = None,
|
|
437
|
+
api_key: str | None = None,
|
|
434
438
|
) -> tuple[int, dict[str, Any]]:
|
|
435
439
|
body = None
|
|
436
440
|
headers = {"accept": "application/json"}
|
|
441
|
+
if api_key is not None:
|
|
442
|
+
headers["authorization"] = f"Bearer {api_key}"
|
|
437
443
|
if payload is not None:
|
|
438
444
|
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
439
445
|
headers["content-type"] = "application/json"
|
|
@@ -476,6 +482,7 @@ def _check_upstream_model(state: ServerState) -> None:
|
|
|
476
482
|
method="GET",
|
|
477
483
|
timeout=state.request_timeout_seconds,
|
|
478
484
|
max_bytes=1_048_576,
|
|
485
|
+
api_key=state.upstream_api_key,
|
|
479
486
|
)
|
|
480
487
|
data = payload.get("data")
|
|
481
488
|
if (
|
|
@@ -510,6 +517,7 @@ def _generate_image(state: ServerState, request: dict[str, Any]) -> dict[str, An
|
|
|
510
517
|
timeout=None,
|
|
511
518
|
max_bytes=max_json_bytes,
|
|
512
519
|
payload=_native_request(request),
|
|
520
|
+
api_key=state.upstream_api_key,
|
|
513
521
|
)
|
|
514
522
|
if status == 429:
|
|
515
523
|
raise ApiError(
|
|
@@ -980,11 +988,45 @@ def _arg_size(value: str) -> tuple[int, int]:
|
|
|
980
988
|
raise argparse.ArgumentTypeError("size must use WIDTHxHEIGHT") from error
|
|
981
989
|
|
|
982
990
|
|
|
991
|
+
def _read_api_key_file(path: str | None) -> str | None:
|
|
992
|
+
if path is None:
|
|
993
|
+
return None
|
|
994
|
+
flags = os.O_RDONLY
|
|
995
|
+
flags |= getattr(os, "O_CLOEXEC", 0)
|
|
996
|
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
997
|
+
try:
|
|
998
|
+
descriptor = os.open(path, flags)
|
|
999
|
+
except OSError as error:
|
|
1000
|
+
raise SystemExit("--upstream-api-key-file cannot be opened safely") from error
|
|
1001
|
+
try:
|
|
1002
|
+
metadata = os.fstat(descriptor)
|
|
1003
|
+
if not stat.S_ISREG(metadata.st_mode):
|
|
1004
|
+
raise SystemExit("--upstream-api-key-file must be a regular file")
|
|
1005
|
+
if os.name == "posix":
|
|
1006
|
+
if metadata.st_uid != os.geteuid():
|
|
1007
|
+
raise SystemExit("--upstream-api-key-file must be owned by the service user")
|
|
1008
|
+
if stat.S_IMODE(metadata.st_mode) & 0o077:
|
|
1009
|
+
raise SystemExit("--upstream-api-key-file must not be accessible by group or others")
|
|
1010
|
+
raw = os.read(descriptor, 4_097)
|
|
1011
|
+
if len(raw) > 4_096 or os.read(descriptor, 1):
|
|
1012
|
+
raise SystemExit("--upstream-api-key-file is too large")
|
|
1013
|
+
finally:
|
|
1014
|
+
os.close(descriptor)
|
|
1015
|
+
try:
|
|
1016
|
+
api_key = raw.decode("ascii").strip()
|
|
1017
|
+
except UnicodeDecodeError as error:
|
|
1018
|
+
raise SystemExit("--upstream-api-key-file must contain an ASCII token") from error
|
|
1019
|
+
if not 32 <= len(api_key) <= 4_096 or any(character.isspace() for character in api_key):
|
|
1020
|
+
raise SystemExit("--upstream-api-key-file contains an invalid token")
|
|
1021
|
+
return api_key
|
|
1022
|
+
|
|
1023
|
+
|
|
983
1024
|
def _parse_args() -> argparse.Namespace:
|
|
984
1025
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
985
1026
|
parser.add_argument("--host", default=DEFAULT_HOST)
|
|
986
1027
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
|
987
1028
|
parser.add_argument("--upstream-base-url", default=DEFAULT_UPSTREAM_BASE_URL)
|
|
1029
|
+
parser.add_argument("--upstream-api-key-file")
|
|
988
1030
|
parser.add_argument("--model-id", default=DEFAULT_MODEL_ID)
|
|
989
1031
|
parser.add_argument("--upstream-model-id", default=DEFAULT_MODEL_ID)
|
|
990
1032
|
parser.add_argument(
|
|
@@ -1048,6 +1090,7 @@ def main() -> int:
|
|
|
1048
1090
|
if not 1 <= args.port <= 65_535 and args.port != 0:
|
|
1049
1091
|
raise SystemExit("--port must be 0 or between 1 and 65535")
|
|
1050
1092
|
upstream_base_url = _validate_loopback_origin(args.upstream_base_url)
|
|
1093
|
+
upstream_api_key = _read_api_key_file(args.upstream_api_key_file)
|
|
1051
1094
|
if MODEL_ID_PATTERN.fullmatch(args.model_id) is None:
|
|
1052
1095
|
raise SystemExit("--model-id is invalid")
|
|
1053
1096
|
if MODEL_ID_PATTERN.fullmatch(args.upstream_model_id) is None:
|
|
@@ -1090,6 +1133,7 @@ def main() -> int:
|
|
|
1090
1133
|
|
|
1091
1134
|
state = ServerState(
|
|
1092
1135
|
upstream_base_url=upstream_base_url,
|
|
1136
|
+
upstream_api_key=upstream_api_key,
|
|
1093
1137
|
model_id=args.model_id,
|
|
1094
1138
|
upstream_model_id=args.upstream_model_id,
|
|
1095
1139
|
max_request_bytes=args.max_request_bytes,
|