@mcoda/mswarm 0.1.107 → 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.
Files changed (162) hide show
  1. package/README.md +17 -0
  2. package/dist/codali-executor.d.ts.map +1 -1
  3. package/dist/codali-executor.js +17 -3
  4. package/dist/codali-executor.js.map +1 -1
  5. package/dist/runtime.d.ts +16 -5
  6. package/dist/runtime.d.ts.map +1 -1
  7. package/dist/runtime.js +292 -36
  8. package/dist/runtime.js.map +1 -1
  9. package/dist/server.d.ts.map +1 -1
  10. package/dist/server.js +4 -0
  11. package/dist/server.js.map +1 -1
  12. package/dist/vendor/codali/agents/AgentInventory.d.ts +56 -0
  13. package/dist/vendor/codali/agents/AgentInventory.d.ts.map +1 -0
  14. package/dist/vendor/codali/agents/AgentInventory.js +131 -0
  15. package/dist/vendor/codali/agents/RoleResolution.d.ts +49 -0
  16. package/dist/vendor/codali/agents/RoleResolution.d.ts.map +1 -0
  17. package/dist/vendor/codali/agents/RoleResolution.js +166 -0
  18. package/dist/vendor/codali/api/ChatCompletionsAdapter.d.ts +76 -0
  19. package/dist/vendor/codali/api/ChatCompletionsAdapter.d.ts.map +1 -0
  20. package/dist/vendor/codali/api/ChatCompletionsAdapter.js +54 -0
  21. package/dist/vendor/codali/api/CodaliApi.d.ts +78 -0
  22. package/dist/vendor/codali/api/CodaliApi.d.ts.map +1 -0
  23. package/dist/vendor/codali/api/CodaliApi.js +273 -0
  24. package/dist/vendor/codali/api/CodaliServer.d.ts +42 -0
  25. package/dist/vendor/codali/api/CodaliServer.d.ts.map +1 -0
  26. package/dist/vendor/codali/api/CodaliServer.js +118 -0
  27. package/dist/vendor/codali/cli/AskCommand.d.ts +66 -0
  28. package/dist/vendor/codali/cli/AskCommand.d.ts.map +1 -0
  29. package/dist/vendor/codali/cli/AskCommand.js +536 -0
  30. package/dist/vendor/codali/cli/AuthCommand.d.ts +19 -0
  31. package/dist/vendor/codali/cli/AuthCommand.d.ts.map +1 -0
  32. package/dist/vendor/codali/cli/AuthCommand.js +161 -0
  33. package/dist/vendor/codali/cli/ServeCommand.d.ts +5 -0
  34. package/dist/vendor/codali/cli/ServeCommand.d.ts.map +1 -0
  35. package/dist/vendor/codali/cli/ServeCommand.js +87 -0
  36. package/dist/vendor/codali/cli/ToolsCommand.d.ts +22 -0
  37. package/dist/vendor/codali/cli/ToolsCommand.d.ts.map +1 -0
  38. package/dist/vendor/codali/cli/ToolsCommand.js +208 -0
  39. package/dist/vendor/codali/cli.d.ts.map +1 -1
  40. package/dist/vendor/codali/cli.js +29 -1
  41. package/dist/vendor/codali/connectors/http/HttpToolDefinition.d.ts +74 -0
  42. package/dist/vendor/codali/connectors/http/HttpToolDefinition.d.ts.map +1 -0
  43. package/dist/vendor/codali/connectors/http/HttpToolDefinition.js +190 -0
  44. package/dist/vendor/codali/connectors/http/HttpToolSource.d.ts +18 -0
  45. package/dist/vendor/codali/connectors/http/HttpToolSource.d.ts.map +1 -0
  46. package/dist/vendor/codali/connectors/http/HttpToolSource.js +29 -0
  47. package/dist/vendor/codali/connectors/mcp/McpClient.d.ts +132 -0
  48. package/dist/vendor/codali/connectors/mcp/McpClient.d.ts.map +1 -0
  49. package/dist/vendor/codali/connectors/mcp/McpClient.js +215 -0
  50. package/dist/vendor/codali/connectors/mcp/McpServerRegistry.d.ts +75 -0
  51. package/dist/vendor/codali/connectors/mcp/McpServerRegistry.d.ts.map +1 -0
  52. package/dist/vendor/codali/connectors/mcp/McpServerRegistry.js +180 -0
  53. package/dist/vendor/codali/connectors/mcp/McpToolAdapter.d.ts +48 -0
  54. package/dist/vendor/codali/connectors/mcp/McpToolAdapter.d.ts.map +1 -0
  55. package/dist/vendor/codali/connectors/mcp/McpToolAdapter.js +101 -0
  56. package/dist/vendor/codali/connectors/mcp/McpToolSource.d.ts +27 -0
  57. package/dist/vendor/codali/connectors/mcp/McpToolSource.d.ts.map +1 -0
  58. package/dist/vendor/codali/connectors/mcp/McpToolSource.js +33 -0
  59. package/dist/vendor/codali/connectors/media/ImageGenerationTool.d.ts +43 -0
  60. package/dist/vendor/codali/connectors/media/ImageGenerationTool.d.ts.map +1 -0
  61. package/dist/vendor/codali/connectors/media/ImageGenerationTool.js +122 -0
  62. package/dist/vendor/codali/connectors/oauth/DeviceCodeAuth.d.ts +74 -0
  63. package/dist/vendor/codali/connectors/oauth/DeviceCodeAuth.d.ts.map +1 -0
  64. package/dist/vendor/codali/connectors/oauth/DeviceCodeAuth.js +153 -0
  65. package/dist/vendor/codali/docdex/DocdexClient.d.ts +2 -0
  66. package/dist/vendor/codali/docdex/DocdexClient.d.ts.map +1 -1
  67. package/dist/vendor/codali/docdex/DocdexClient.js +4 -0
  68. package/dist/vendor/codali/gateway/CodaliGateway.d.ts +51 -3
  69. package/dist/vendor/codali/gateway/CodaliGateway.d.ts.map +1 -1
  70. package/dist/vendor/codali/gateway/CodaliGateway.js +226 -2
  71. package/dist/vendor/codali/gateway/CodaliGatewayTypes.d.ts +34 -0
  72. package/dist/vendor/codali/gateway/CodaliGatewayTypes.d.ts.map +1 -1
  73. package/dist/vendor/codali/gateway/EvidenceNormalizer.d.ts.map +1 -1
  74. package/dist/vendor/codali/gateway/EvidenceNormalizer.js +90 -1
  75. package/dist/vendor/codali/gateway/Finalizer.d.ts +50 -0
  76. package/dist/vendor/codali/gateway/Finalizer.d.ts.map +1 -0
  77. package/dist/vendor/codali/gateway/Finalizer.js +79 -0
  78. package/dist/vendor/codali/gateway/GatewayPlanner.d.ts +13 -0
  79. package/dist/vendor/codali/gateway/GatewayPlanner.d.ts.map +1 -1
  80. package/dist/vendor/codali/gateway/GatewayPlanner.js +81 -16
  81. package/dist/vendor/codali/gateway/GatewayStateMachine.d.ts.map +1 -1
  82. package/dist/vendor/codali/gateway/GatewayStateMachine.js +6 -0
  83. package/dist/vendor/codali/gateway/GatewayTracer.d.ts +93 -0
  84. package/dist/vendor/codali/gateway/GatewayTracer.d.ts.map +1 -0
  85. package/dist/vendor/codali/gateway/GatewayTracer.js +214 -0
  86. package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts +46 -0
  87. package/dist/vendor/codali/gateway/LocalGatewayProvider.d.ts.map +1 -0
  88. package/dist/vendor/codali/gateway/LocalGatewayProvider.js +161 -0
  89. package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.d.ts +79 -0
  90. package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.d.ts.map +1 -0
  91. package/dist/vendor/codali/gateway/LocalGatewayTaskRunner.js +354 -0
  92. package/dist/vendor/codali/gateway/RelevanceJudge.d.ts +14 -0
  93. package/dist/vendor/codali/gateway/RelevanceJudge.d.ts.map +1 -0
  94. package/dist/vendor/codali/gateway/RelevanceJudge.js +87 -0
  95. package/dist/vendor/codali/gateway/ResponseSchema.d.ts +32 -0
  96. package/dist/vendor/codali/gateway/ResponseSchema.d.ts.map +1 -0
  97. package/dist/vendor/codali/gateway/ResponseSchema.js +120 -0
  98. package/dist/vendor/codali/gateway/TemporalContext.d.ts +36 -0
  99. package/dist/vendor/codali/gateway/TemporalContext.d.ts.map +1 -0
  100. package/dist/vendor/codali/gateway/TemporalContext.js +115 -0
  101. package/dist/vendor/codali/gateway/ToolCapabilityCompiler.d.ts +5 -0
  102. package/dist/vendor/codali/gateway/ToolCapabilityCompiler.d.ts.map +1 -1
  103. package/dist/vendor/codali/gateway/ToolCapabilityCompiler.js +27 -2
  104. package/dist/vendor/codali/gateway/ToolDescriptorSource.d.ts +6 -0
  105. package/dist/vendor/codali/gateway/ToolDescriptorSource.d.ts.map +1 -0
  106. package/dist/vendor/codali/gateway/ToolDescriptorSource.js +30 -0
  107. package/dist/vendor/codali/gateway/ToolExposure.d.ts +36 -0
  108. package/dist/vendor/codali/gateway/ToolExposure.d.ts.map +1 -0
  109. package/dist/vendor/codali/gateway/ToolExposure.js +154 -0
  110. package/dist/vendor/codali/index.d.ts +57 -0
  111. package/dist/vendor/codali/index.d.ts.map +1 -1
  112. package/dist/vendor/codali/index.js +45 -0
  113. package/dist/vendor/codali/providers/ClaudeCliProvider.d.ts +2 -0
  114. package/dist/vendor/codali/providers/ClaudeCliProvider.d.ts.map +1 -1
  115. package/dist/vendor/codali/providers/ClaudeCliProvider.js +2 -0
  116. package/dist/vendor/codali/providers/CodexCliProvider.d.ts +2 -0
  117. package/dist/vendor/codali/providers/CodexCliProvider.d.ts.map +1 -1
  118. package/dist/vendor/codali/providers/CodexCliProvider.js +2 -0
  119. package/dist/vendor/codali/providers/MswarmWorkerProvider.d.ts +2 -0
  120. package/dist/vendor/codali/providers/MswarmWorkerProvider.d.ts.map +1 -1
  121. package/dist/vendor/codali/providers/MswarmWorkerProvider.js +2 -0
  122. package/dist/vendor/codali/providers/OllamaRemoteProvider.d.ts +2 -0
  123. package/dist/vendor/codali/providers/OllamaRemoteProvider.d.ts.map +1 -1
  124. package/dist/vendor/codali/providers/OllamaRemoteProvider.js +2 -0
  125. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts +2 -0
  126. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
  127. package/dist/vendor/codali/providers/OpenAiCompatibleProvider.js +2 -0
  128. package/dist/vendor/codali/providers/ProviderTypes.d.ts +13 -0
  129. package/dist/vendor/codali/providers/ProviderTypes.d.ts.map +1 -1
  130. package/dist/vendor/codali/providers/ProviderTypes.js +1 -1
  131. package/dist/vendor/codali/runcontext/CredentialFile.d.ts +12 -0
  132. package/dist/vendor/codali/runcontext/CredentialFile.d.ts.map +1 -0
  133. package/dist/vendor/codali/runcontext/CredentialFile.js +81 -0
  134. package/dist/vendor/codali/runcontext/RunContextResolver.d.ts +126 -0
  135. package/dist/vendor/codali/runcontext/RunContextResolver.d.ts.map +1 -0
  136. package/dist/vendor/codali/runcontext/RunContextResolver.js +389 -0
  137. package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.d.ts +1 -1
  138. package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.d.ts.map +1 -1
  139. package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.js +12 -0
  140. package/dist/vendor/codali/tools/ToolRegistry.d.ts +14 -1
  141. package/dist/vendor/codali/tools/ToolRegistry.d.ts.map +1 -1
  142. package/dist/vendor/codali/tools/ToolRegistry.js +38 -1
  143. package/dist/vendor/codali/tools/ToolTypes.d.ts +35 -0
  144. package/dist/vendor/codali/tools/ToolTypes.d.ts.map +1 -1
  145. package/dist/vendor/codali/tools/ToolTypes.js +21 -0
  146. package/dist/vendor/codali/tools/TruncateResult.d.ts +29 -0
  147. package/dist/vendor/codali/tools/TruncateResult.d.ts.map +1 -0
  148. package/dist/vendor/codali/tools/TruncateResult.js +91 -0
  149. package/dist/vendor/codali/tools/diff/DiffTool.d.ts.map +1 -1
  150. package/dist/vendor/codali/tools/diff/DiffTool.js +2 -0
  151. package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts +21 -1
  152. package/dist/vendor/codali/tools/docdex/DocdexTools.d.ts.map +1 -1
  153. package/dist/vendor/codali/tools/docdex/DocdexTools.js +108 -16
  154. package/dist/vendor/codali/tools/filesystem/FileTools.d.ts.map +1 -1
  155. package/dist/vendor/codali/tools/filesystem/FileTools.js +8 -0
  156. package/dist/vendor/codali/tools/search/SearchTool.d.ts.map +1 -1
  157. package/dist/vendor/codali/tools/search/SearchTool.js +3 -1
  158. package/dist/vendor/codali/tools/shell/ShellTool.d.ts.map +1 -1
  159. package/dist/vendor/codali/tools/shell/ShellTool.js +2 -0
  160. package/package.json +3 -3
  161. package/scripts/stable-diffusion-cpp-auth-guard.py +475 -0
  162. 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,