frutebox-node 0.1.0__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.
- frutebox/adminheader.py +59 -0
- frutebox/attest.py +43 -0
- frutebox/audiogen.py +364 -0
- frutebox/config.py +367 -0
- frutebox/deltasync.py +211 -0
- frutebox/economics.py +270 -0
- frutebox/engine_guard.py +718 -0
- frutebox/envelope.py +77 -0
- frutebox/envvars.py +167 -0
- frutebox/gpuprices.py +641 -0
- frutebox/gpuproviders.py +4489 -0
- frutebox/gui.py +270 -0
- frutebox/imagegen.py +152 -0
- frutebox/inference.py +624 -0
- frutebox/manifest.py +218 -0
- frutebox/mediaserving.py +603 -0
- frutebox/packaging.py +542 -0
- frutebox/pqctransport.py +660 -0
- frutebox/provider.py +1867 -0
- frutebox/providers.py +942 -0
- frutebox/registry/__init__.py +189 -0
- frutebox/registry/generate.py +311 -0
- frutebox/registry/generate_providers.py +120 -0
- frutebox/registry/models.yaml +1759 -0
- frutebox/registry/providers.yaml +518 -0
- frutebox/registry/registry.gen.json +2043 -0
- frutebox/registry_gen.py +3 -0
- frutebox/runpod_templates.py +143 -0
- frutebox/serving.py +691 -0
- frutebox/signedconfig.py +259 -0
- frutebox/signing.py +208 -0
- frutebox/thinkbudget.py +155 -0
- frutebox/videogen.py +118 -0
- frutebox_node-0.1.0.dist-info/METADATA +100 -0
- frutebox_node-0.1.0.dist-info/RECORD +36 -0
- frutebox_node-0.1.0.dist-info/WHEEL +4 -0
frutebox/envelope.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""What remains of the Python envelope: two SHAPE checks, and no cryptography.
|
|
2
|
+
|
|
3
|
+
THE CRYPTOGRAPHY LIVED HERE AND WAS NEVER USED. `master_key`, `field_key`, `encrypt_field` and
|
|
4
|
+
`decrypt_field` were deleted on 2026-09-22 (owner ruling) because nothing called them and nothing
|
|
5
|
+
could. Their only Python consumers were `providers.encrypt_credentials`/`decrypt_credentials`,
|
|
6
|
+
themselves uncalled, and a caller could not have been created without breaking the constraint the
|
|
7
|
+
whole design rests on: **`frutebox-directory` holds neither `DATABASE_URL` nor `STEVAI_DATA_KEY`
|
|
8
|
+
by design**, so it never sees a `valueEnc` row and `master_key()` raised on it. That is exactly the
|
|
9
|
+
28 §O1 failure that had the live doctor reporting `EnvelopeError` for every provider on
|
|
10
|
+
2026-09-18.
|
|
11
|
+
|
|
12
|
+
DEAD CRYPTOGRAPHY IS WORSE THAN NO CRYPTOGRAPHY. It reads as protection while protecting nothing,
|
|
13
|
+
and it is the first thing someone wires up in a hurry — which here would mean handing the
|
|
14
|
+
provisioning service the key that decrypts every encrypted field in the product, to solve a
|
|
15
|
+
problem that is only about provider API keys.
|
|
16
|
+
|
|
17
|
+
THE REAL ENVELOPE IS `web/src/lib/crypto/identity-core.ts`. It runs in
|
|
18
|
+
`web/src/app/api/internal/provider-credentials/route.ts`: `encryptField` on the PUT,
|
|
19
|
+
`decryptField` on the GET, both under the master key the web app legitimately holds. That is where
|
|
20
|
+
a credential is encrypted at rest (readme rule 10, no cloud KMS), and where it should be changed.
|
|
21
|
+
|
|
22
|
+
WHAT IS LEFT, AND WHY IT IS NOT CRYPTOGRAPHY
|
|
23
|
+
- `mask()` — the doctor prints PRESENCE, never a value, a length or a tail.
|
|
24
|
+
- `looks_like_field_token()` — a SHAPE check so `resolve_credentials` can REFUSE ciphertext
|
|
25
|
+
rather than decrypt it. It reads the version byte and a length; it holds no key and decides
|
|
26
|
+
nothing safe.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import base64
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
#: Matches `VERSION` in identity-core.ts. A version byte on every token so the format can change
|
|
34
|
+
#: without a migration that must guess what it is reading.
|
|
35
|
+
VERSION = 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def mask(secret: Optional[str]) -> str:
|
|
39
|
+
"""What `providers doctor` prints: PRESENCE, and nothing else.
|
|
40
|
+
|
|
41
|
+
It used to print `set (50 chars, ends 8l7y)`. The intent was "enough to recognise a key,
|
|
42
|
+
never enough to use one", and that reasoning is wrong twice over. The length narrows the
|
|
43
|
+
search space and identifies which credential format a provider issued; the last four
|
|
44
|
+
characters are secret material outright. The standing rule is that a secret is never printed
|
|
45
|
+
— not its value, not its length, not any part of it — and a doctor endpoint exists to be
|
|
46
|
+
pasted into a report, which is precisely where those details would travel.
|
|
47
|
+
|
|
48
|
+
"set" answers the only question the doctor is for: is a credential installed. Anything more
|
|
49
|
+
is the caller's curiosity, not the operator's need.
|
|
50
|
+
"""
|
|
51
|
+
return "set" if (secret or "").strip() else "(unset)"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def looks_like_field_token(value: str) -> bool:
|
|
55
|
+
"""True when `value` has the shape of an `encrypt_field` token rather than a plaintext secret.
|
|
56
|
+
|
|
57
|
+
WHY THIS EXISTS (file 28 §O1). The provider-credential path decrypted twice: the web endpoint
|
|
58
|
+
returns plaintext, and `resolve_credentials` then decrypted it again. The two halves were each
|
|
59
|
+
tested in isolation and never against each other, so the break only ever showed as
|
|
60
|
+
`EnvelopeError` in a live doctor run.
|
|
61
|
+
|
|
62
|
+
Now that `resolve_credentials` treats what it receives as plaintext, the same class of mistake
|
|
63
|
+
would be SILENT and worse: ciphertext handed to a provider as an API key. This makes it loud.
|
|
64
|
+
It is a shape check, not a security boundary — the caller uses it to refuse, never to decide
|
|
65
|
+
that something is safe.
|
|
66
|
+
|
|
67
|
+
A token is base64url of `VERSION || iv(12) || tag(16) || ct`, so a plaintext key would have to
|
|
68
|
+
be valid base64url, decode to at least 29 bytes, AND begin with exactly our version byte.
|
|
69
|
+
"""
|
|
70
|
+
s = (value or "").strip()
|
|
71
|
+
if not s:
|
|
72
|
+
return False
|
|
73
|
+
try:
|
|
74
|
+
packed = base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
77
|
+
return len(packed) >= 29 and packed[0] == VERSION
|
frutebox/envvars.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Dual-read environment lookup: FRUTEBOX_* first, STEVAI_* second.
|
|
2
|
+
|
|
3
|
+
WHY THIS EXISTS
|
|
4
|
+
The brand rename touches ~320 distinct STEVAI_* variable names that are set on
|
|
5
|
+
LIVE, RUNNING services. Renaming a variable in code while the deployed service still sets the old
|
|
6
|
+
name breaks that service the instant the new code ships — silently, because a missing env var
|
|
7
|
+
usually reads as "unset" rather than as an error.
|
|
8
|
+
|
|
9
|
+
This helper decouples the two halves. Code can move to FRUTEBOX_* today; deployments can keep
|
|
10
|
+
setting STEVAI_* until someone gets round to them, in any order, with no flag day. When the last
|
|
11
|
+
deployment has moved, the STEVAI_* arm becomes dead and can be deleted — and until then it is the
|
|
12
|
+
only thing keeping production alive.
|
|
13
|
+
|
|
14
|
+
env("ADMIN_TOKEN") -> $FRUTEBOX_ADMIN_TOKEN, else $STEVAI_ADMIN_TOKEN, else None
|
|
15
|
+
env("ADMIN_TOKEN", "dev") -> ... else "dev"
|
|
16
|
+
env_bool("REQUIRE_ADMISSION") -> True for 1/true/yes/on (case-insensitive)
|
|
17
|
+
|
|
18
|
+
Pass the BARE suffix, not the prefixed name: `env("ADMIN_TOKEN")`, never `env("STEVAI_ADMIN_TOKEN")`.
|
|
19
|
+
Passing a prefixed name is a mistake that would otherwise look up FRUTEBOX_STEVAI_ADMIN_TOKEN and
|
|
20
|
+
quietly find nothing, so it raises instead.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
NEW_PREFIX = "FRUTEBOX_"
|
|
28
|
+
OLD_PREFIX = "STEVAI_"
|
|
29
|
+
|
|
30
|
+
_TRUE = {"1", "true", "yes", "on"}
|
|
31
|
+
_FALSE = {"0", "false", "no", "off"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _names(suffix: str) -> tuple[str, str]:
|
|
35
|
+
s = (suffix or "").strip()
|
|
36
|
+
if not s:
|
|
37
|
+
raise ValueError("env() needs a variable name")
|
|
38
|
+
if s.startswith(NEW_PREFIX) or s.startswith(OLD_PREFIX):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"pass the bare suffix, not {s!r} — e.g. env('ADMIN_TOKEN'), not env('{s}')"
|
|
41
|
+
)
|
|
42
|
+
return NEW_PREFIX + s, OLD_PREFIX + s
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def env(suffix: str, default: Optional[str] = None) -> Optional[str]:
|
|
46
|
+
"""FRUTEBOX_<suffix>, else STEVAI_<suffix>, else `default`.
|
|
47
|
+
|
|
48
|
+
An EMPTY value counts as set and wins, exactly as `os.environ.get` would treat it: a deployment
|
|
49
|
+
that deliberately blanks a variable must not silently fall through to the old one.
|
|
50
|
+
"""
|
|
51
|
+
new, old = _names(suffix)
|
|
52
|
+
if new in os.environ:
|
|
53
|
+
return os.environ[new]
|
|
54
|
+
if old in os.environ:
|
|
55
|
+
return os.environ[old]
|
|
56
|
+
return default
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MissingConfig(RuntimeError):
|
|
60
|
+
"""A required variable is unset. Raised at import, so the process refuses to start."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def env_required(suffix: str, *, why: str = "") -> str:
|
|
64
|
+
"""FRUTEBOX_<suffix>, else STEVAI_<suffix>. **No default. Unset is FATAL.**
|
|
65
|
+
|
|
66
|
+
WHY THERE IS NO DEFAULT HERE, AND WHY IT MATTERS MORE THAN IT LOOKS
|
|
67
|
+
These variables used to default to `https://stevai.com`. That domain is being removed, so
|
|
68
|
+
every one of those defaults was a time-bomb whose fuse is a deletion the owner is entitled
|
|
69
|
+
to perform without consulting the code.
|
|
70
|
+
|
|
71
|
+
`revocations.py` shows the shape of the damage. Admission FAILS CLOSED: an unreachable
|
|
72
|
+
revocation list refuses every node, which is correct. Point it at a dead domain and the
|
|
73
|
+
network stops admitting anyone at all — safely, and with logs blaming a revocation service
|
|
74
|
+
rather than a decommissioned domain. Safe and completely unreadable is the worst failure
|
|
75
|
+
mode this codebase produces.
|
|
76
|
+
|
|
77
|
+
An unset variable is a QUESTION. The honest answer is to stop and name it, not to guess the
|
|
78
|
+
old answer. Same rule `deploy-gateway.sh` already follows.
|
|
79
|
+
|
|
80
|
+
NOTE the deliberate asymmetry with code we SHIP: an SDK, desktop app or extension keeps a
|
|
81
|
+
default and points it at the new host, because a user's upgrade is not supervised and making
|
|
82
|
+
their client throw would be a worse failure than the one being removed.
|
|
83
|
+
"""
|
|
84
|
+
new, old = _names(suffix)
|
|
85
|
+
val = os.environ.get(new, os.environ.get(old))
|
|
86
|
+
if val is None or not val.strip():
|
|
87
|
+
raise MissingConfig(
|
|
88
|
+
f"{new} is not set (nor {old}). {why or 'It has no default: the old default was '
|
|
89
|
+
'https://stevai.com, a domain being removed.'} Set it, e.g.:\n"
|
|
90
|
+
f" export {new}=https://frutebox-web-qmatfu5ovq-ew.a.run.app")
|
|
91
|
+
return val
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def env_bool(suffix: str, default: bool = False) -> bool:
|
|
95
|
+
"""The same lookup, parsed as a flag. Unrecognised values fall back to `default`."""
|
|
96
|
+
raw = env(suffix)
|
|
97
|
+
if raw is None:
|
|
98
|
+
return default
|
|
99
|
+
v = raw.strip().lower()
|
|
100
|
+
if v in _TRUE:
|
|
101
|
+
return True
|
|
102
|
+
if v in _FALSE:
|
|
103
|
+
return False
|
|
104
|
+
return default
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def env_int(suffix: str, default: int = 0) -> int:
|
|
108
|
+
"""The same lookup, parsed as an int. Unparseable values fall back to `default`."""
|
|
109
|
+
raw = env(suffix)
|
|
110
|
+
if raw is None:
|
|
111
|
+
return default
|
|
112
|
+
try:
|
|
113
|
+
return int(raw.strip())
|
|
114
|
+
except (TypeError, ValueError):
|
|
115
|
+
return default
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def source_of(suffix: str) -> str:
|
|
119
|
+
"""Which arm answered: "frutebox", "stevai" or "default". For migration reporting."""
|
|
120
|
+
new, old = _names(suffix)
|
|
121
|
+
if new in os.environ:
|
|
122
|
+
return "frutebox"
|
|
123
|
+
if old in os.environ:
|
|
124
|
+
return "stevai"
|
|
125
|
+
return "default"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# --- the on-disk config directory -----------------------------------------------------------
|
|
129
|
+
# Same problem as the variable names, one level up: every existing install has its state in
|
|
130
|
+
# ~/.stevai. Renaming the path blind would not migrate that state, it would ORPHAN it — the
|
|
131
|
+
# software would start up looking healthy and empty, which is worse than failing.
|
|
132
|
+
|
|
133
|
+
import shutil
|
|
134
|
+
from pathlib import Path
|
|
135
|
+
|
|
136
|
+
NEW_DIR = ".frutebox"
|
|
137
|
+
OLD_DIR = ".stevai"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def config_dir(home: Optional[str] = None, *, migrate: bool = False) -> Path:
|
|
141
|
+
"""Where this machine keeps its config: ~/.frutebox, else ~/.stevai, else ~/.frutebox.
|
|
142
|
+
|
|
143
|
+
Resolution mirrors `env()`: the new name wins if it exists, the old one is honoured while
|
|
144
|
+
it is all that exists, and a fresh install gets the new name. `migrate=True` performs a
|
|
145
|
+
one-time MOVE of the old directory to the new one and is what a CLI should pass on startup;
|
|
146
|
+
it is off by default so that merely *reading* the path can never mutate the disk.
|
|
147
|
+
"""
|
|
148
|
+
root = Path(home) if home else Path.home()
|
|
149
|
+
new, old = root / NEW_DIR, root / OLD_DIR
|
|
150
|
+
if new.exists():
|
|
151
|
+
return new
|
|
152
|
+
if old.exists():
|
|
153
|
+
if migrate:
|
|
154
|
+
shutil.move(str(old), str(new)) # atomic on the same filesystem
|
|
155
|
+
return new
|
|
156
|
+
return old
|
|
157
|
+
return new
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def config_dir_source(home: Optional[str] = None) -> str:
|
|
161
|
+
"""Which directory answered: "frutebox", "stevai" or "new". For migration reporting."""
|
|
162
|
+
root = Path(home) if home else Path.home()
|
|
163
|
+
if (root / NEW_DIR).exists():
|
|
164
|
+
return "frutebox"
|
|
165
|
+
if (root / OLD_DIR).exists():
|
|
166
|
+
return "stevai"
|
|
167
|
+
return "new"
|