fadenstack 0.4.2__py3-none-any.whl

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 (99) hide show
  1. faden_cli/__init__.py +16 -0
  2. faden_cli/__main__.py +9 -0
  3. faden_cli/bundle/shared/.gitignore +25 -0
  4. faden_cli/bundle/shared/LICENSE +187 -0
  5. faden_cli/bundle/shared/NOTICE +15 -0
  6. faden_cli/bundle/shared/OFFICIAL.md +9 -0
  7. faden_cli/bundle/shared/README.md +16 -0
  8. faden_cli/bundle/shared/agent-binaries/.gitignore +5 -0
  9. faden_cli/bundle/shared/alloy/config.alloy +40 -0
  10. faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +26 -0
  11. faden_cli/bundle/shared/docker-compose.gpu.yaml +34 -0
  12. faden_cli/bundle/shared/docker-compose.yaml +998 -0
  13. faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +2099 -0
  14. faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +10 -0
  15. faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +211 -0
  16. faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +9 -0
  17. faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +15 -0
  18. faden_cli/bundle/shared/initdb/01-init.sql +32 -0
  19. faden_cli/bundle/shared/loki/local-config.yaml +32 -0
  20. faden_cli/bundle/shared/nginx/locations.conf +245 -0
  21. faden_cli/bundle/shared/nginx/maintenance.html +239 -0
  22. faden_cli/bundle/shared/nginx/nginx-tls.conf +79 -0
  23. faden_cli/bundle/shared/nginx/nginx.conf +44 -0
  24. faden_cli/bundle/shared/nginx/rate-limits.conf +42 -0
  25. faden_cli/bundle/shared/nginx/security-headers.conf +22 -0
  26. faden_cli/bundle/shared/nginx/select-config.sh +37 -0
  27. faden_cli/bundle/shared/prometheus/prometheus.yml +40 -0
  28. faden_cli/bundle/shared/rabbitmq/definitions.json.example +37 -0
  29. faden_cli/bundle/shared/tls/README.md +30 -0
  30. faden_cli/cli.py +158 -0
  31. faden_cli/commands/__init__.py +1 -0
  32. faden_cli/commands/admin.py +161 -0
  33. faden_cli/commands/backup.py +159 -0
  34. faden_cli/commands/config.py +146 -0
  35. faden_cli/commands/deploy.py +949 -0
  36. faden_cli/commands/dev/__init__.py +1 -0
  37. faden_cli/commands/dev/dev_doctor.py +46 -0
  38. faden_cli/commands/dev/dev_down.py +241 -0
  39. faden_cli/commands/dev/dev_group.py +24 -0
  40. faden_cli/commands/dev/dev_init.py +710 -0
  41. faden_cli/commands/dev/dev_pull.py +297 -0
  42. faden_cli/commands/dev/dev_status.py +151 -0
  43. faden_cli/commands/dev/dev_up.py +1392 -0
  44. faden_cli/commands/doctor.py +225 -0
  45. faden_cli/commands/down.py +75 -0
  46. faden_cli/commands/logs_cmd.py +41 -0
  47. faden_cli/commands/maintenance.py +74 -0
  48. faden_cli/commands/module.py +113 -0
  49. faden_cli/commands/observe.py +95 -0
  50. faden_cli/commands/ports.py +91 -0
  51. faden_cli/commands/rebuild.py +158 -0
  52. faden_cli/commands/restore.py +139 -0
  53. faden_cli/commands/runtime_images.py +174 -0
  54. faden_cli/commands/status.py +71 -0
  55. faden_cli/commands/tls.py +722 -0
  56. faden_cli/commands/tune.py +37 -0
  57. faden_cli/commands/up.py +64 -0
  58. faden_cli/commands/upgrade.py +470 -0
  59. faden_cli/commands/version.py +78 -0
  60. faden_cli/core/__init__.py +1 -0
  61. faden_cli/core/api_client.py +77 -0
  62. faden_cli/core/backup.py +717 -0
  63. faden_cli/core/bootstrap.py +291 -0
  64. faden_cli/core/bundle.py +110 -0
  65. faden_cli/core/clickhouse.py +61 -0
  66. faden_cli/core/compose.py +484 -0
  67. faden_cli/core/console.py +106 -0
  68. faden_cli/core/detect.py +580 -0
  69. faden_cli/core/dev_modules.py +149 -0
  70. faden_cli/core/docker_env.py +147 -0
  71. faden_cli/core/edge.py +94 -0
  72. faden_cli/core/env_gen.py +598 -0
  73. faden_cli/core/git.py +219 -0
  74. faden_cli/core/install.py +288 -0
  75. faden_cli/core/installation.py +186 -0
  76. faden_cli/core/local_node.py +719 -0
  77. faden_cli/core/maintenance.py +82 -0
  78. faden_cli/core/private_files.py +133 -0
  79. faden_cli/core/registry.py +574 -0
  80. faden_cli/core/release_feed.py +342 -0
  81. faden_cli/core/rmq.py +135 -0
  82. faden_cli/core/runtime_images.py +309 -0
  83. faden_cli/core/schedule.py +278 -0
  84. faden_cli/core/settings.py +159 -0
  85. faden_cli/core/sysinfo.py +303 -0
  86. faden_cli/core/tls.py +790 -0
  87. faden_cli/core/tune.py +154 -0
  88. faden_cli/core/update_check.py +254 -0
  89. faden_cli/core/workspace.py +155 -0
  90. faden_cli/templates/env.j2 +33 -0
  91. faden_cli/tui/__init__.py +1 -0
  92. faden_cli/tui/widgets/__init__.py +1 -0
  93. faden_cli/tui/wizard/__init__.py +1 -0
  94. fadenstack-0.4.2.dist-info/METADATA +304 -0
  95. fadenstack-0.4.2.dist-info/RECORD +99 -0
  96. fadenstack-0.4.2.dist-info/WHEEL +4 -0
  97. fadenstack-0.4.2.dist-info/entry_points.txt +3 -0
  98. fadenstack-0.4.2.dist-info/licenses/LICENSE +187 -0
  99. fadenstack-0.4.2.dist-info/licenses/NOTICE +15 -0
@@ -0,0 +1,239 @@
1
+ <!doctype html>
2
+ <!--
3
+ Fadenstack -- the page nginx answers with while the platform is updated
4
+ (faden upgrade), switched to maintenance (faden maintenance on), or while a
5
+ service behind it does not answer. Self-contained: nothing it needs may come
6
+ from a service that is restarting. It asks every few seconds whether the
7
+ page it stands in for is back, and reloads it when it is. It takes the
8
+ light/dark mode and accent the console stored for this browser.
9
+ -->
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1">
14
+ <meta name="robots" content="noindex">
15
+ <meta name="color-scheme" content="light dark">
16
+ <title>Fadenstack · Maintenance</title>
17
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%230A0E1A'/%3E%3Cg fill='none' stroke-linecap='round'%3E%3Cpath d='M22 26H78' stroke='%23F3F4F6' stroke-width='13'/%3E%3Cpath d='M22 50H62' stroke='%23FF4D6D' stroke-width='13'/%3E%3Cpath d='M22 74H34' stroke='%23F3F4F6' stroke-width='13'/%3E%3C/g%3E%3C/svg%3E">
18
+ <style>
19
+ :root {
20
+ --bg: #f5f7fb; --card: #ffffff; --text: #111827; --muted: #5b6474;
21
+ --border: #e3e7ee; --plate: #0a0e1a; --thread: #e2325a; --accent: #4c6a92;
22
+ --note: #f1f4f9; --shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 12px 32px rgba(16, 24, 40, .08);
23
+ }
24
+ :root[data-mode="dark"] {
25
+ --bg: #0a0e1a; --card: #111827; --text: #e5e7eb; --muted: #9aa3b2;
26
+ --border: #1f2937; --plate: #f3f4f6; --thread: #ff4d6d; --accent: #81a1c1;
27
+ --note: #0d1322; --shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 16px 40px rgba(0, 0, 0, .35);
28
+ }
29
+ :root[data-accent="graphite"] { --accent: #1f2937; }
30
+ :root[data-mode="dark"][data-accent="graphite"] { --accent: #e5e7eb; }
31
+ :root[data-accent="aurora"] { --accent: #7c4dff; }
32
+
33
+ * { box-sizing: border-box; }
34
+ html, body { margin: 0; min-height: 100%; }
35
+ body {
36
+ min-height: 100vh; min-height: 100dvh;
37
+ display: flex; align-items: center; justify-content: center; padding: 24px 16px;
38
+ background:
39
+ radial-gradient(1200px 600px at 50% -10%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 70%),
40
+ var(--bg);
41
+ color: var(--text);
42
+ font: 15px/1.6 "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
43
+ -webkit-font-smoothing: antialiased;
44
+ }
45
+ main {
46
+ width: 100%; max-width: 520px; background: var(--card);
47
+ border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow);
48
+ padding: 36px 32px 20px;
49
+ }
50
+
51
+ /* ── centred: who and what ── */
52
+ header { text-align: center; }
53
+ .mark { width: 72px; height: 72px; display: block; margin: 0 auto 4px; }
54
+ .mark .plate { stroke: var(--plate); }
55
+ .mark .thread { stroke: var(--thread); stroke-dasharray: 70; animation: thread 2.4s ease-in-out infinite; }
56
+ @keyframes thread { 0% { stroke-dashoffset: 70; } 45%, 60% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: -70; } }
57
+ h1 { margin: 12px 0 0; font-size: 1.45rem; line-height: 1.3; font-weight: 650; letter-spacing: -0.01em; }
58
+ .bar { position: relative; height: 4px; margin: 20px auto 0; max-width: 220px; border-radius: 999px;
59
+ background: color-mix(in srgb, var(--accent) 18%, transparent); overflow: hidden; }
60
+ .bar::after { content: ""; position: absolute; inset: 0; width: 40%; border-radius: 999px;
61
+ background: var(--accent); animation: slide 1.6s ease-in-out infinite; }
62
+ @keyframes slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(250%); } }
63
+
64
+ /* ── left: the details ── */
65
+ .body { margin-top: 24px; text-align: left; }
66
+ .body p { margin: 0 0 12px; color: var(--muted); }
67
+ .note {
68
+ margin: 4px 0 12px; padding: 12px 14px; border-radius: 12px; background: var(--note);
69
+ border: 1px solid var(--border); border-left: 3px solid var(--accent);
70
+ }
71
+ .note-label { display: block; font-size: .78rem; font-weight: 600; text-transform: uppercase;
72
+ letter-spacing: .04em; color: var(--muted); margin-bottom: 2px; }
73
+ .note-text { color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }
74
+
75
+ /* ── left and right: where it stands ── */
76
+ footer {
77
+ display: flex; justify-content: space-between; align-items: center; gap: 12px;
78
+ margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--border);
79
+ font-size: .85rem; color: var(--muted);
80
+ }
81
+ .status { display: inline-flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; }
82
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1.4s ease-in-out infinite; }
83
+ @keyframes pulse { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
84
+ .since { flex: none; margin-left: auto; text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
85
+ .back .dot { background: #2e7d32; animation: none; }
86
+
87
+ @media (max-width: 480px) {
88
+ main { padding: 28px 20px 16px; } h1 { font-size: 1.25rem; }
89
+ footer { flex-wrap: wrap; row-gap: 4px; }
90
+ }
91
+ @media (prefers-reduced-motion: reduce) {
92
+ .mark .thread, .bar::after, .dot { animation: none; }
93
+ .mark .thread { stroke-dashoffset: 0; }
94
+ .bar::after { width: 100%; opacity: .5; }
95
+ }
96
+ </style>
97
+ <script>
98
+ // The console's own choices for this browser (theme-mode.tsx), else the system's.
99
+ (function () {
100
+ var root = document.documentElement, mode = null, accent = null;
101
+ try {
102
+ mode = localStorage.getItem("faden-theme-mode");
103
+ accent = localStorage.getItem("faden-accent");
104
+ } catch (e) {}
105
+ if (mode !== "light" && mode !== "dark") {
106
+ mode = window.matchMedia && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
107
+ }
108
+ root.dataset.mode = mode;
109
+ if (accent === "graphite" || accent === "aurora") root.dataset.accent = accent;
110
+ })();
111
+ </script>
112
+ </head>
113
+ <body>
114
+ <main>
115
+ <header>
116
+ <svg class="mark" viewBox="0 0 100 100" aria-hidden="true">
117
+ <g transform="translate(0 6)" fill="none" stroke-linejoin="round" stroke-linecap="round" stroke-width="8">
118
+ <path class="plate" d="M50 10L86 28L50 46L14 28Z"/>
119
+ <path class="thread" d="M14 44L50 62L72.3 50.8"/>
120
+ <path class="plate" d="M14 60L50 78L57.9 74"/>
121
+ </g>
122
+ </svg>
123
+ <h1 id="title">Fadenstack is being updated</h1>
124
+ <div class="bar" role="progressbar" aria-label="In progress"></div>
125
+ </header>
126
+ <div class="body">
127
+ <p id="detail">The platform is restarting with the new version. This usually takes a few minutes; nothing you saved is lost.</p>
128
+ <div class="note" id="note" hidden>
129
+ <span class="note-label" id="note-label">Note from the administrator</span>
130
+ <span class="note-text" id="message"></span>
131
+ </div>
132
+ </div>
133
+ <footer>
134
+ <span class="status" id="status-row" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span id="status">Reloads by itself when it is back.</span></span>
135
+ <span class="since" id="since" hidden></span>
136
+ </footer>
137
+ </main>
138
+ <script>
139
+ (function () {
140
+ var text = {
141
+ en: {
142
+ upgrade: ["Fadenstack is being updated", "The platform is restarting with the new version. This usually takes a few minutes; nothing you saved is lost."],
143
+ manual: ["Fadenstack is under maintenance", "An administrator is working on the platform. It will be back shortly."],
144
+ down: ["Fadenstack is not reachable right now", "A service is restarting or not answering. If this lasts, tell your administrator."],
145
+ note: "Note from the administrator", since: "Since {t}", status: "Reloads by itself when it is back.", back: "Back. Reloading…"
146
+ },
147
+ de: {
148
+ upgrade: ["Fadenstack wird aktualisiert", "Die Plattform startet mit der neuen Version neu. Das dauert meist ein paar Minuten; nichts Gespeichertes geht verloren."],
149
+ manual: ["Fadenstack wird gewartet", "Ein Administrator arbeitet an der Plattform. Sie ist gleich wieder da."],
150
+ down: ["Fadenstack ist gerade nicht erreichbar", "Ein Dienst startet neu oder antwortet nicht. Wenn das anhält, wenden Sie sich an Ihren Administrator."],
151
+ note: "Hinweis des Administrators", since: "Seit {t}", status: "Lädt neu, sobald es wieder da ist.", back: "Wieder da. Wird neu geladen…"
152
+ },
153
+ es: {
154
+ upgrade: ["Fadenstack se está actualizando", "La plataforma se reinicia con la nueva versión. Suele tardar unos minutos; no se pierde nada de lo guardado."],
155
+ manual: ["Fadenstack está en mantenimiento", "Un administrador está trabajando en la plataforma. Volverá en breve."],
156
+ down: ["Fadenstack no está disponible ahora mismo", "Un servicio se está reiniciando o no responde. Si continúa, avise a su administrador."],
157
+ note: "Nota del administrador", since: "Desde las {t}", status: "Se recarga sola cuando vuelva.", back: "De vuelta. Recargando…"
158
+ },
159
+ fr: {
160
+ upgrade: ["Fadenstack est en cours de mise à jour", "La plateforme redémarre avec la nouvelle version. Cela prend généralement quelques minutes ; rien de ce qui est enregistré n'est perdu."],
161
+ manual: ["Fadenstack est en maintenance", "Un administrateur intervient sur la plateforme. Elle sera bientôt de retour."],
162
+ down: ["Fadenstack n'est pas joignable pour le moment", "Un service redémarre ou ne répond pas. Si cela dure, prévenez votre administrateur."],
163
+ note: "Note de l'administrateur", since: "Depuis {t}", status: "Se recharge dès son retour.", back: "De retour. Rechargement…"
164
+ },
165
+ zh: {
166
+ upgrade: ["Fadenstack 正在更新", "平台正在以新版本重新启动,通常需要几分钟;已保存的内容不会丢失。"],
167
+ manual: ["Fadenstack 正在维护", "管理员正在维护平台,很快就会恢复。"],
168
+ down: ["Fadenstack 暂时无法访问", "某个服务正在重启或没有响应。如果持续,请联系管理员。"],
169
+ note: "管理员留言", since: "开始于 {t}", status: "恢复后自动重新加载。", back: "已恢复,正在重新加载…"
170
+ }
171
+ };
172
+ var lang = (navigator.language || "en").slice(0, 2).toLowerCase();
173
+ var t = text[lang] || text.en;
174
+ document.documentElement.lang = text[lang] ? lang : "en";
175
+ var $ = function (id) { return document.getElementById(id); };
176
+ var startedAt = null;
177
+
178
+ function elapsed(ms) {
179
+ var minutes = Math.floor(ms / 60000);
180
+ if (minutes < 1) return "<1 min";
181
+ if (minutes < 60) return minutes + " min";
182
+ return Math.floor(minutes / 60) + " h " + (minutes % 60) + " min";
183
+ }
184
+ function showSince() {
185
+ if (!startedAt) { $("since").hidden = true; return; }
186
+ var clock = new Date(startedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
187
+ $("since").textContent = t.since.replace("{t}", clock) + " · " + elapsed(Date.now() - startedAt);
188
+ $("since").hidden = false;
189
+ }
190
+
191
+ function show(kind, state) {
192
+ var words = t[kind];
193
+ $("title").textContent = words[0];
194
+ $("detail").textContent = words[1];
195
+ document.title = "Fadenstack · " + words[0];
196
+ var message = state && typeof state.message === "string" ? state.message : "";
197
+ $("message").textContent = message;
198
+ $("note").hidden = !message;
199
+ var when = state && Date.parse(state.since);
200
+ startedAt = when || null;
201
+ showSince();
202
+ }
203
+
204
+ // Why this page is up: the flag the CLI puts down says, else a service is not answering.
205
+ function readState() {
206
+ return fetch("/_faden/maintenance.json", { cache: "no-store" })
207
+ .then(function (r) { return r.ok ? r.json() : null; })
208
+ .catch(function () { return null; })
209
+ .then(function (state) {
210
+ show(state ? (state.reason === "upgrade" ? "upgrade" : "manual") : "down", state);
211
+ });
212
+ }
213
+
214
+ // Back once the page this one stands in for answers with anything but a 5xx.
215
+ function check() {
216
+ fetch(location.href, { cache: "no-store", headers: { Accept: "text/html" } })
217
+ .then(function (r) {
218
+ if (r.status < 500) {
219
+ $("status-row").classList.add("back");
220
+ $("status").textContent = t.back;
221
+ setTimeout(function () { location.reload(); }, 400);
222
+ } else {
223
+ readState();
224
+ schedule();
225
+ }
226
+ })
227
+ .catch(schedule);
228
+ }
229
+ function schedule() { setTimeout(check, 5000); }
230
+
231
+ $("note-label").textContent = t.note;
232
+ $("status").textContent = t.status;
233
+ setInterval(showSince, 30000);
234
+ readState();
235
+ schedule();
236
+ })();
237
+ </script>
238
+ </body>
239
+ </html>
@@ -0,0 +1,79 @@
1
+ # Fadenstack — nginx reverse proxy, HTTPS
2
+ #
3
+ # Installed by select-config.sh when the install's ./tls directory holds
4
+ # edge.crt and edge.key: uploaded through the console's TLS admin API,
5
+ # or copied in by the operator (docs/installing.md, "Enabling HTTPS").
6
+ # Plain HTTP then answers the container health check and sends
7
+ # everything else to HTTPS. The routes are in locations.conf.
8
+ #
9
+ # __HTTPS_PORT_SUFFIX__ and __HSTS_MAX_AGE__ are filled in by
10
+ # select-config.sh from FADEN_HTTPS_PORT and FADEN_HSTS_MAX_AGE.
11
+
12
+ # No nginx version in the Server header or on error pages.
13
+ server_tokens off;
14
+
15
+ # Limits on sign-in and account requests (limit_req in locations.conf).
16
+ include /etc/nginx/faden/rate-limits.conf;
17
+
18
+ # WebSocket connection upgrade map (must precede server block)
19
+ map $http_upgrade $connection_upgrade {
20
+ default upgrade;
21
+ '' close;
22
+ }
23
+
24
+ server {
25
+ listen 80;
26
+ server_name _;
27
+
28
+ # The container health check, answered here so it never depends on TLS.
29
+ location /healthz {
30
+ access_log off;
31
+ return 200 'ok';
32
+ }
33
+
34
+ # 307, not 301: browsers keep a 301 for good, so after HTTPS is switched
35
+ # off again they would still go to a port nothing answers on. 307 also
36
+ # keeps the method and body, which a POST to /v1 needs.
37
+ location / {
38
+ return 307 https://$host__HTTPS_PORT_SUFFIX__$request_uri;
39
+ }
40
+ }
41
+
42
+ server {
43
+ listen 443 ssl;
44
+ http2 on;
45
+ server_name _;
46
+
47
+ # edge.crt followed by edge-chain.pem (when present), put together by
48
+ # select-config.sh: nginx wants the leaf and its chain in one file.
49
+ ssl_certificate /etc/nginx/faden-tls/fullchain.pem;
50
+ ssl_certificate_key /etc/faden/tls/edge.key;
51
+ ssl_protocols TLSv1.2 TLSv1.3;
52
+ ssl_prefer_server_ciphers off;
53
+ ssl_session_cache shared:faden_ssl:10m;
54
+ ssl_session_timeout 1d;
55
+ ssl_session_tickets off;
56
+
57
+ # HSTS is opt-in: FADEN_HSTS_MAX_AGE in .env (Help: HTTPS). Off by default,
58
+ # max-age=0, which also tells browsers to forget an earlier one. Many
59
+ # installs run on a self-signed certificate or a private CA, and while a
60
+ # host is under HSTS a browser offers no way past a certificate warning:
61
+ # every browser that does not yet trust that CA would be locked out, and
62
+ # so would anyone after HTTPS is switched off again.
63
+ add_header Strict-Transport-Security "max-age=__HSTS_MAX_AGE__" always;
64
+ include /etc/nginx/faden/security-headers.conf;
65
+
66
+ # Docker embedded DNS — re-resolve container IPs dynamically so
67
+ # restarting an upstream container doesn't require an nginx restart.
68
+ resolver 127.0.0.11 valid=10s ipv6=off;
69
+
70
+ # Max upload size (file attachments, RAG documents)
71
+ client_max_body_size 100m;
72
+
73
+ location /healthz {
74
+ access_log off;
75
+ return 200 'ok';
76
+ }
77
+
78
+ include /etc/nginx/faden/locations.conf;
79
+ }
@@ -0,0 +1,44 @@
1
+ # Fadenstack — nginx reverse proxy, plain HTTP
2
+ #
3
+ # Single public entrypoint unifying frontend + backend + API gateway
4
+ # under one origin so the frontend's relative-path API calls (/api/*)
5
+ # work from any browser, local or remote.
6
+ #
7
+ # This is the server nginx runs while no edge certificate is present.
8
+ # With one in the install's ./tls directory, select-config.sh installs
9
+ # nginx-tls.conf instead (docs/installing.md, "Enabling HTTPS"). The
10
+ # routes are in locations.conf, shared by both.
11
+
12
+ # No nginx version in the Server header or on error pages.
13
+ server_tokens off;
14
+
15
+ # Limits on sign-in and account requests (limit_req in locations.conf).
16
+ include /etc/nginx/faden/rate-limits.conf;
17
+
18
+ # WebSocket connection upgrade map (must precede server block)
19
+ map $http_upgrade $connection_upgrade {
20
+ default upgrade;
21
+ '' close;
22
+ }
23
+
24
+ server {
25
+ listen 80;
26
+ server_name _;
27
+
28
+ # Docker embedded DNS — re-resolve container IPs dynamically so
29
+ # restarting an upstream container doesn't require an nginx restart.
30
+ resolver 127.0.0.11 valid=10s ipv6=off;
31
+
32
+ # Max upload size (file attachments, RAG documents)
33
+ client_max_body_size 100m;
34
+
35
+ include /etc/nginx/faden/security-headers.conf;
36
+
37
+ # ── Health check (local only, no upstream) ────────────────────
38
+ location /healthz {
39
+ access_log off;
40
+ return 200 'ok';
41
+ }
42
+
43
+ include /etc/nginx/faden/locations.conf;
44
+ }
@@ -0,0 +1,42 @@
1
+ # Fadenstack -- how often one address may try to sign in.
2
+ #
3
+ # Included at http level by nginx.conf and nginx-tls.conf; the limit_req
4
+ # lines are in locations.conf (/api/). Each zone counts only the requests
5
+ # its key is set for: every other request has an empty key, which nginx
6
+ # does not count. $uri is the normalised path, so /api//auth/... and
7
+ # %-encoded spellings count the same.
8
+ #
9
+ # Per client address, and generous on purpose: an office behind one
10
+ # address, or an install whose Docker proxy hands nginx a single address
11
+ # for everyone, shares one count. Over the limit nginx answers 429 at
12
+ # once (nodelay) and the next try a few seconds later goes through.
13
+ #
14
+ # faden_signin console and API sign-in: 20 a minute, 20 at once.
15
+ # faden_account forgotten and reset passwords, e-mail verification and
16
+ # bootstrap: 10 a minute, 10 at once.
17
+ # faden_refresh agents renewing their tokens: 300 a minute, 200 at once.
18
+ # Each running agent renews about every 10 minutes
19
+ # (FADEN_BACKEND_AGENT_ACCESS_TOKEN_TTL_S); the SDKs keep
20
+ # their sign-in on a 429 and try again on the next call.
21
+
22
+ map "$request_method $uri" $faden_signin_key {
23
+ default "";
24
+ "~^POST /api/auth/(jwt|cookie)/login$" $binary_remote_addr;
25
+ }
26
+
27
+ map "$request_method $uri" $faden_account_key {
28
+ default "";
29
+ "~^POST /api/auth/(forgot-password|reset-password|request-verify-token|verify)$" $binary_remote_addr;
30
+ "~^POST /api/bootstrap$" $binary_remote_addr;
31
+ }
32
+
33
+ map "$request_method $uri" $faden_refresh_key {
34
+ default "";
35
+ "~^POST /api/agent-tokens/refresh$" $binary_remote_addr;
36
+ }
37
+
38
+ limit_req_zone $faden_signin_key zone=faden_signin:10m rate=20r/m;
39
+ limit_req_zone $faden_account_key zone=faden_account:10m rate=10r/m;
40
+ limit_req_zone $faden_refresh_key zone=faden_refresh:10m rate=300r/m;
41
+ limit_req_status 429;
42
+ limit_req_log_level warn;
@@ -0,0 +1,22 @@
1
+ # Fadenstack -- security headers on every answer of the console's origin.
2
+ #
3
+ # Included at server level, and again in every location of locations.conf
4
+ # that has an add_header of its own: nginx drops the server's add_header
5
+ # lines in a location that sets any itself.
6
+
7
+ # Framing: only pages of this origin may frame these pages. The console
8
+ # frames Grafana (/grafana/) and the gateway's API docs (/gateway-docs/)
9
+ # itself, from the same origin; nothing else frames it, and no other site
10
+ # can put the console -- an approval button, say -- under a page of its own.
11
+ # X-Frame-Options for browsers that do not read the CSP.
12
+ add_header Content-Security-Policy "frame-ancestors 'self'" always;
13
+ add_header X-Frame-Options "SAMEORIGIN" always;
14
+ add_header X-Content-Type-Options "nosniff" always;
15
+ # A link to another site does not tell it which console page it came from.
16
+ add_header Referrer-Policy "same-origin" always;
17
+
18
+ # The gateway sends its own copies of these (X-Frame-Options DENY on /v1).
19
+ # The edge's are the ones that count; the copies are dropped, not sent twice.
20
+ proxy_hide_header X-Frame-Options;
21
+ proxy_hide_header X-Content-Type-Options;
22
+ proxy_hide_header Referrer-Policy;
@@ -0,0 +1,37 @@
1
+ #!/bin/sh
2
+ # Fadenstack -- choose the nginx server config at container start.
3
+ #
4
+ # HTTPS when the edge material is in place (edge.crt and edge.key under
5
+ # /etc/faden/tls, the install's ./tls directory), plain HTTP otherwise.
6
+ # The container command runs this before nginx itself, so restarting the
7
+ # container is all it takes to switch either way; the backend does that
8
+ # when material is activated or removed through /api/admin/tls.
9
+ set -eu
10
+
11
+ SRC=/etc/nginx/faden
12
+ TLS=/etc/faden/tls
13
+ OUT=/etc/nginx/conf.d/default.conf
14
+ CHAIN_DIR=/etc/nginx/faden-tls
15
+
16
+ if [ -s "$TLS/edge.crt" ] && [ -s "$TLS/edge.key" ]; then
17
+ # nginx wants the leaf and its chain in one file; the backend writes them apart.
18
+ mkdir -p "$CHAIN_DIR"
19
+ cat "$TLS/edge.crt" > "$CHAIN_DIR/fullchain.pem"
20
+ if [ -n "$(tail -c 1 "$CHAIN_DIR/fullchain.pem")" ]; then
21
+ echo >> "$CHAIN_DIR/fullchain.pem"
22
+ fi
23
+ if [ -s "$TLS/edge-chain.pem" ]; then
24
+ cat "$TLS/edge-chain.pem" >> "$CHAIN_DIR/fullchain.pem"
25
+ fi
26
+ chmod 644 "$CHAIN_DIR/fullchain.pem"
27
+
28
+ port="${FADEN_HTTPS_PORT:-443}"
29
+ if [ "$port" = "443" ]; then suffix=""; else suffix=":$port"; fi
30
+ sed -e "s|__HTTPS_PORT_SUFFIX__|$suffix|g" \
31
+ -e "s|__HSTS_MAX_AGE__|${FADEN_HSTS_MAX_AGE:-0}|g" \
32
+ "$SRC/nginx-tls.conf" > "$OUT"
33
+ echo "Fadenstack nginx: HTTPS on, edge material found in $TLS"
34
+ else
35
+ cp "$SRC/nginx.conf" "$OUT"
36
+ echo "Fadenstack nginx: plain HTTP, no edge material in $TLS"
37
+ fi
@@ -0,0 +1,40 @@
1
+ # Fadenstack runtime metrics — Prometheus scrape config.
2
+ #
3
+ # The single `llm-runtimes` job discovers targets from targets.json
4
+ # (file_sd). That file is GENERATED by the backend's monitoring
5
+ # provisioner on the shared-stack host directory (see
6
+ # faden_backend/services/llm/monitoring.py). It is seeded with `[]`
7
+ # and is never hand-edited.
8
+ #
9
+ # Each entry in targets.json looks like:
10
+ # [
11
+ # {
12
+ # "targets": ["192.168.1.50:5001"],
13
+ # "labels": {
14
+ # "runtime_id": "…uuid…",
15
+ # "runtime_name": "dgx1-moondream",
16
+ # "provider_name": "DGX Spark 1",
17
+ # "model_name": "moonshotai/Kimi-K2-Instruct",
18
+ # "node_host": "192.168.1.50"
19
+ # }
20
+ # }
21
+ # ]
22
+ #
23
+ # Reloading: config changes are picked up via POST http://<prom>:9090/-/reload
24
+ # (--web.enable-lifecycle) and, as a backstop, file_sd refresh_interval.
25
+
26
+ global:
27
+ scrape_interval: 15s
28
+ evaluation_interval: 30s
29
+ external_labels:
30
+ monitor: Fadenstack
31
+
32
+ scrape_configs:
33
+ - job_name: llm-runtimes
34
+ metrics_path: /metrics
35
+ scrape_interval: 15s
36
+ scrape_timeout: 10s
37
+ file_sd_configs:
38
+ - files:
39
+ - /etc/prometheus/targets.json
40
+ refresh_interval: 15s
@@ -0,0 +1,37 @@
1
+ {
2
+ "users": [
3
+ {
4
+ "name": "admin",
5
+ "password_hash": "",
6
+ "hashing_algorithm": "rabbit_password_hashing_sha256",
7
+ "tags": "administrator"
8
+ },
9
+ {
10
+ "name": "faden-backend",
11
+ "password_hash": "",
12
+ "hashing_algorithm": "rabbit_password_hashing_sha256",
13
+ "tags": ""
14
+ },
15
+ {
16
+ "name": "faden-gateway",
17
+ "password_hash": "",
18
+ "hashing_algorithm": "rabbit_password_hashing_sha256",
19
+ "tags": ""
20
+ },
21
+ {
22
+ "name": "faden-pii",
23
+ "password_hash": "",
24
+ "hashing_algorithm": "rabbit_password_hashing_sha256",
25
+ "tags": ""
26
+ }
27
+ ],
28
+ "vhosts": [
29
+ {"name": "/"}
30
+ ],
31
+ "permissions": [
32
+ {"user": "admin", "vhost": "/", "configure": ".*", "write": ".*", "read": ".*"},
33
+ {"user": "faden-backend", "vhost": "/", "configure": ".*", "write": ".*", "read": ".*"},
34
+ {"user": "faden-gateway", "vhost": "/", "configure": ".*", "write": ".*", "read": ".*"},
35
+ {"user": "faden-pii", "vhost": "/", "configure": ".*", "write": ".*", "read": ".*"}
36
+ ]
37
+ }
@@ -0,0 +1,30 @@
1
+ # Edge certificate
2
+
3
+ nginx serves the console, the API and the OpenAI-compatible gateway over
4
+ HTTPS as soon as this directory holds:
5
+
6
+ | File | What it is |
7
+ |------------------|---------------------------------------------------------|
8
+ | `edge.crt` | the server certificate, PEM |
9
+ | `edge.key` | its private key, PEM (keep it `0600`) |
10
+ | `edge-chain.pem` | the issuer's intermediates, PEM, when there are any |
11
+ | `ca.crt` | the install's own authority, when `faden tls generate` made the certificate: what browsers, API clients and agents are told to trust |
12
+
13
+ Put them here with the CLI, which checks them and restarts the proxy:
14
+
15
+ ```bash
16
+ faden tls generate # a certificate made here for this machine
17
+ faden tls use CERT KEY # your own (--ca ROOT for a private issuer)
18
+ faden tls renew # renew what is due; runs every morning by itself where there is systemd or cron
19
+ faden tls off # back to plain HTTP
20
+ ```
21
+
22
+ or upload them through the console's TLS admin API, which writes them here
23
+ and restarts the proxy itself. Copied in by hand, they are served after
24
+ `docker restart faden-nginx`. See `docs/help/pages/operate/https.md`, "Switch HTTPS on".
25
+
26
+ The authority's key is kept in `../tls-ca/`, outside every container, with the
27
+ authority before a rotation, an operator's private issuer and where an
28
+ operator's certificate was imported from. `edge.json` here is the CLI's note of
29
+ this host's addresses and ports, which the backend cannot see from its container.
30
+ The certificate files and `edge.json` are ignored by git; only this README is tracked.