| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The audience split stopped a member's node credential from opening the hub API.
It did not stop the credential being *replayed to another node*: the MNP token
carried the member's whole group set and named no node, so a token handed to
node A's operator could be presented to node B the member also belongs to. That
does not read content on B — the handshake still requires proving node B's group
key, which the operator lacks — but it reaches B's pre-proof window and fetches
the member's *encrypted* keypair bundle for B (offline-attackable, bounded,
audited): a disclosure §2.4 says should not follow from hosting a member on A.
The token now names the node it is minted for (a `node` claim = that node's
Ed25519 key), and authorize_token refuses one that names a different key. The
client already knows the target node's key (from /v1/groups/{id}/nodes) and asks
for a token bound to it: POST /v1/nodes/mnp-token takes node_pk, and
transport.connect threads it (group-page, the connection pool and rewrap pass
n.pk_node; reconnect preserves it). A token that names no node is still
accepted, because the hub only mints one for the authenticated requester, so an
unbound token grants nothing across accounts — which also keeps non-binding
callers working with no churn.
Done before deploy, so it folds into the MNP 4.0 flag day rather than needing
its own. Docs: §5.2, register E10, MESHBAY_NODE_PROTOCOL.md §6.3.
test_handshake.py and test_mnp_token.py hold the binding (a token for node A is
refused by node B, accepted by node A; an unbound token still works); red
before, green after. common/node/hub suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
failed node-key link
Two defensive gaps turned a routine reset-and-reonboard into "impossible de
démarrer le node":
1. daemon._login_with_retry retried a 401 (node key not linked yet) but `raise`d
on every other status, so a 429 — the daemon's own 5s retries hitting the
sign-in rate limit — or a 502/503 while the hub restarts during a deploy
killed the process, and systemd crash-looped it. Those statuses (429, 5xx)
are now retried with a back-off that respects Retry-After, so a freshly
reset node stays alive (the operator needs it up to read its key) instead of
dying. A genuine 4xx (400/422) still raises.
2. create-group's linkNodeKey swallowed every error as "already linked or same
key" — but PUT /me/node_key is idempotent and returns 200 on a re-link, so
there was no benign error to hide: the catch only ever hid a real failure
(a rejected session, a bad key), letting the wizard proceed against a node
that looked linked but was not, which then could not authenticate. The link
failure now surfaces (detectNode shows it).
test_login_retry_is_resilient.py holds the retry behaviour (429/5xx retried,
Retry-After honoured, 401 stays alive, 400 still raises); red before, green
after. common/node/hub suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
session token
A member authenticated to a node in the MNP handshake with its hub *session*
token — scope=user, valid at the hub API for hours. A node operator is in the
threat model, so this handed them a live hub credential for the member: enough
to enumerate the member's other groups, act as them, and (before the previous
commit closed it) take the account over. The node genuinely needs a hub-signed
membership assertion, so the fix is to make that a separate credential that
opens nothing at the hub API.
Two audiences signed by the one hub key (meshbay_common/tokens.py):
- HUB_API_AUD — session tokens (login, device-auth, node-auth, refresh), used
for hub calls and signaling. decode_access_token now binds this audience, so
an MNP token cannot be replayed against the hub API.
- MNP_AUD — a short-lived token a member presents to a node and nothing else,
from POST /v1/nodes/mnp-token. authorize_token now binds this audience, so a
session token presented to a node is refused. This closes the disclosure.
The node's own self-decode (hub_client.py) reads its node token with
audience=HUB_API_AUD. The client fetches the MNP token inside transport.connect()
(and on every reconnect) using the session token, so callers are unchanged and
signaling keeps using the session token.
No regression to a long session: the MNP token is checked once, at the
handshake, before any proof — a film already playing is not re-authenticated, so
a 15-minute token does not interrupt a 4-hour film; reconnects refetch a fresh
one. Denylist and membership checks are unchanged (the MNP token carries
sub/jti/groups).
Tests: authorize_token refuses a session/no-audience token and accepts an MNP
token; the hub API refuses an MNP token; POST /v1/nodes/mnp-token is minted only
for a member's own session. Verified red-before/green-after; common, node and
hub suites green (the pre-existing test_cli_golden failure is an argparse/pytest
prog artifact unrelated to this change).
Still to do before deploy (B2): bump the MNP version and client.minimum so a
stale desktop client is told to update rather than getting a handshake refusal,
update docs/MESHBAY_DESIGN.md and MESHBAY_NODE_PROTOCOL.md, and validate against
a real node locally, then deploy hub+node+SPA atomically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
A link is redeemable by whoever opens it first, so it can be sent by any
messaging app. The address is optional (mail + label only); a link lives
7 days, fixed. Adds a Share button; see MESHBAY_DESIGN.md §3.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
Each section of ops.py becomes a module of meshbay_node/ops/ (core,
node_toml, members, chat, groups, roots, files, settings, apps), cut as
text; ops/__init__.py keeps the docstring and re-exports every name, so
`ops.<name>` is unchanged for every caller. Logger name unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The _enrich_*/_reenrich_* methods and _on_enriched become
EnrichmentMixin in meshbay_node/enrichment.py, with the two directory
helpers only they use. APP_DIR_KEYS stays on NodeDaemon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
Each `if args.command == ...` branch of main() becomes a function in
cli/ (one module per family of verbs); the parser, the process setup
and a VERBS table go to cli/parser.py and cli/dispatch.py. daemon.main
runs the verb or starts the daemon. Tests patch the CLI through
conftest.patch_cli; the CLI golden is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
/api/peers imported _get_remote_ip, inside the function, from
webrtc_server, which no longer has it. A test now resolves every
meshbay import in the node's source, function bodies included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
_do_admin_response looked each operation up in a 28-branch elif, every
branch the same call. The admin cases of the dispatch golden are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
The pre-authentication guards stay explicit code, in the same order and
text. After the handshake, a table maps each type to its handler and to
whether it runs as a task, the choice each branch made; the three inline
blocks become StreamingMixin methods, unchanged. The dispatch golden is
identical, including types that are not strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
DispatchMixin in transport/webrtc/dispatch.py, with the pre-proof fetch
bound it enforces. The elif chain moves as it is; turning it into a table
is the next commit, on its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
SessionCore in transport/webrtc/core.py, last among the bases: state,
the data channel, task ownership, the peer registry, sending and teardown.
The facade keeps _dispatch_message and WebRTCTransport.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
webrtc_server
AdminMixin in transport/webrtc/admin.py: who the operator is, the signed
challenge, signature checks, and _do_admin_response moved unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
GroupOpsMixin in transport/webrtc/group_ops.py: member revocation and
unpinning, the group key rotation, apps and their directories with the
allow-list, and Search listing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
NodeOpsMixin in transport/webrtc/node_ops.py: status and settings, roster
and denylist, roots, hosted groups, reload, scan pacing and transfer
limits, with their class-level bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Three headers stayed in webrtc_server when their sections moved; they now
sit above the same methods in blobs.py and admission.py. Two that were only
a title over what is now a whole module are dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
HandshakeMixin in transport/webrtc/handshake.py: challenge, proof, channel
binding, and the sealed configuration a peer receives once admitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The peer-session ceiling no longer quotes the hub's old per-account number,
and the note on a single uploads/ directory goes: its constant left with
f3fb449, when an upload started landing in the folder it was sent to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
AdmissionMixin in transport/webrtc/admission.py, with the invite handle
pattern and the join attempt bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
UploadMixin in transport/webrtc/upload_handlers.py, with the upload cap;
_upload_chunk moved unchanged, _append_chunk joins webrtc/disk.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
TransferMixin in transport/webrtc/transfer_handlers.py, with the sweep
interval.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
FilesMixin in transport/webrtc/files.py; the blocking disk helpers join
_locate in webrtc/disk.py, and the lease states go to webrtc/limits.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
ChatMixin in transport/webrtc/chat.py: sealed messages, history, epoch
keys, link previews with their cache and rate bounds, and the operator's
chat ops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
BlobsMixin in transport/webrtc/blobs.py, with the blob caps and the kind
pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
StreamingMixin in transport/webrtc/apps/streaming.py: stream credit,
handover, the transcode slots and _stream_video_inner, moved unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
VideoMetaMixin in transport/webrtc/apps/video_meta.py: media and season
meta, TMDB search and its per-member bound, posters, and the operator's
TMDB ops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
| |
An operator op that exists for one app lives with that app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
MusicMixin in transport/webrtc/apps/music.py: tags and cover art, and the
audio transcode with the extension list that gates it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
SubtitlesMixin in transport/webrtc/apps/subtitles.py; _locate, which it
shares with the files, music and streaming handlers, in webrtc/disk.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
| |
transport/webrtc/channel.py, limits.py and media_tools.py, cut from
webrtc_server.py as text; the facade imports them back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
connectionstatechange dropped a closed or failed session from the
transport and stopped its tasks, but only close() took it out of the
group's peer set, and nothing on that path called it. Every broadcast
then went to closed channels, and each reconnect left a dead session
held until restart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A phone on 4G found one of five groups missing from Search on half its
visits, and all but one on a fifth of them. Every offer that reached the node
was answered in about a second; the hub refused the others with 429 — 93 of
them in half an hour on meshbay.org, all from that phone, all to nodes that
were up. Two ceilings did it. Three pending offers per account was exactly
what one Search page dialled at once, so the first tile or reconnection beside
the sweep was refused. Thirty offers a minute per address and per node was
spent by four or five reloads of a page whose groups share one node.
Sized now against an account with twenty groups on three devices:
- 32 offers pending per account, across devices and nodes.
- A budget per account and per node, a burst of 120 refilled at two a
second. This is what bounds a member's cost to one machine (H6), and it
leaves their other nodes alone. Counted by account, because a mobile
carrier puts many subscribers behind one IPv4 address.
- 600 a minute per address and per node, as a coarse guard in front of
authentication only.
Both refusals carry Retry-After, which the browser now honours. The node's
own ceiling on peer sessions is unchanged; its comment no longer quotes the
old per-account number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
The CLI makes both halves itself — the node's code, then the hub's
ticket bound to the address — and prints the link; a refused ticket
takes the code back, and cancel takes back both. The CLI never asks the
hub to mail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The plan was a working document; what it decided lives in
MESHBAY_DESIGN.md and MESHBAY_NODE_PROTOCOL.md. Code and tests now
cite those instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
New invite kind "link": member of one group, once, never operator, not
spendable by an active member, capped at 20 per group, cancellable by
handle. Signed ops invite_link_create / invite_cancel, loopback routes,
and the known-device join path now accepts a link code. Adds the plan,
docs/invite-links.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
node_pk in handshake_challenge is now signed over the channel binding
and both nonces, so a client can check the node key before a join
rather than only at the ack. Both transports; the browser and the QUIC
client refuse a wrong signature and treat an absent one as an older node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
A browser limits bytes, so a bound in seconds had to be sized for the
highest-bitrate file and every ordinary one then held a fraction of what
the same buffer would have taken. Floored at the old 90 s so nothing
pulls less than before, and walked up rather than declared. A refused
append now waits for an eviction instead of retrying on every tick.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
A member row appears only when a code is consumed, so revoking someone
invited to the wrong group was refused for having no row — and the node's
refusal aborted the browser's removal before its hub half, leaving them a
member everywhere with a live code. Revoking now cancels unredeemed codes
for that group, and a node refusal no longer cancels the hub removal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |\ |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
`RevocationSubscriber` kept its revocations in two in-memory sets. That is the
defect **H4** names: revocations that live only in memory mean a node restart
silently un-revokes everyone, and group revocations were dropped entirely. The
fix was the persisted `Denylist`, fed from `hub_client.maintain_ws` through
`daemon.py`'s `on_revocation`, which verifies the token against the hub key and
calls `deny_user` / `deny_group`.
So this is not merely unused code. It is the version the register records as
wrong, left beside the one that replaced it — and its docstring still opens with
"Usage in daemon:" and three lines of instructions for wiring it up, which is the
worst shape dead code takes: it reads as the way to do the thing. **L7** is about
exactly this, and the index cache removed two days ago was the same shape.
Certainty before deleting, since that was the condition: no import of it anywhere,
static or dynamic; no entry point; nothing in packaging, the systemd units, the
man page, QE or the documentation; and no test covers it. Every module in the
three packages still imports, ruff passes, and the suite is 2893 — unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The pass before this applied ruff's own fixes. These are the ones needing a
decision, and the point of doing them is that `ruff check .` now passes: a
linter reporting 98 known-acceptable findings reports nothing, because the next
real one arrives invisible.
**Lines over 100 (70).** Mostly wrapped where they stood. Two exceptions: the
aligned trailing comments in `protocol.py`'s message table were shortened rather
than wrapped, because wrapping one row of a table breaks the table; and in
`models.py` the column comments moved above their columns for the same reason.
**Imports below the first statement (14).** `csam.py` kept its FastAPI imports
under a section header halfway down the file; two node tests had a constant and
a `pytestmark` wedged between two import blocks. Moved, not suppressed.
**Bindings nothing reads (4).** Three in tests, where the call stays and only the
name goes — `_user(client, "listener")` is there to create the user, not to
return one. The fourth was in `revocation.py` and was not a lint finding at all:
`_connect_and_listen` opened an httpx stream to the WebSocket URL, did `pass`,
and then opened the real connection through the `websockets` library. One
pointless request per connect, left over from before that library was used
directly. Removed, and `httpx` with it.
**`l` as a name (4)**, **semicolons (6)** in the POC spikes, and the rest.
2893 passed, the same count as the two commits before it.
`meshbay_node/revocation.py` is worth a decision separately: 154 lines that
nothing imports, superseded by `hub_client.maintain_ws`'s `on_revocation`. This
commit only stopped it failing the linter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
`ruff check .` had gone unrun long enough to report 568 errors, which is the
same as having no linter: the next real finding would have been invisible in the
noise. This is the 521 it fixes by itself, in 173 files, and nothing else — the
98 it cannot fix are the next commit.
What actually changed: import sorting (225), imports nobody used (87, none of
them a re-export — no `__init__.py` is touched, which was the one way this could
have broken an import elsewhere), `datetime.timezone.utc` to `datetime.UTC` (69)
and `asyncio.TimeoutError` to `TimeoutError` (18), both plain aliases on the 3.12
this project requires, `Optional[X]` to `X | None` (24), and f-strings with
nothing to interpolate (19).
Checked rather than assumed: every module in the three packages still imports,
and the suite is 2893 passed — the same count, test for test, as the merge
before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
media_cache held video_meta, photo_meta and thumbs; the audio tags lived
only in the in-memory IndexEntry. So every start re-read every audio file
the node serves, and until that pass landed it served an index with no
artist on any track — one the Music app cannot group. Over a real
6176-file library the pass costs 27.8s cold and 6.4s from audio_meta.
Only what the bytes decided is stored. The filename and folder fallbacks
still run live, or a renamed file would get the old name's answer; the
sibling-cover scan reads the folder, so it stays live too; and a read
that failed is not cached, or one bad read becomes permanent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |\
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
One conflict, in §15.3's open list, and it was two changes agreeing rather than
disagreeing: this side removed the rows for the third-party search bound and the
node-announcement bound because both are now built (AV27, AV28), while the other
side kept them and added a new one. Resolved by keeping what is genuinely still
open — per-device revocation having no CLI — and leaving the two closed.
`webrtc_server.py` merged without conflict but the two sides met inside one
function: `_upload_chunk` gained the operator's `max_upload_gb` ceiling from
there and the per-group lock and `off_disk` calls from here. Read back rather
than trusted: the operator's ceiling now sits inside the critical section that
keeps chunk ordering, and the unlink beside it goes to the disk thread with
everything else.
2893 passed. The twelve `test_sticky_header.py[firefox]` setup errors are the
open Firefox on this machine, as before.
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
The daemon built the defaults dict for `Roster.node_settings` by hand and
left out `max_concurrent_downloads` and `max_concurrent_uploads`. Absent
from the dict, both resolved to None, were assigned back onto the config,
and the transport skipped them — so node.toml was parsed, validated, and
then replaced by `transfers.py`'s own 8. Invisible to anyone who left the
value at 8, which is the value the template suggests.
There were three copies of that dict and they all disagreed: node_status'
was missing those two and `max_upload_gb` besides. One builder now,
`config.node_settings_defaults`, and the resolver's key list is a class
attribute the tests hold it to, along with the writer's.
1429 passed against a baseline of 1423; the two new behavioural tests fail
with the builder reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Was a 4 GB constant in webrtc_server.py, the same on a small board and on
a machine holding a library. Now max_upload_gb in node.toml, on the Node
page and via `meshbay-node transfers max-size`, read from the transport
context per chunk so a change reaches an upload already running.
MESHBAY_DESIGN.md §6.4; §15.3 records a defect found beside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
`BROWSER_INCOMPATIBLE_AUDIO_EXTS` named the two formats no browser decodes and
was read by nothing. The player asked for `.wma` and `.mpc` and no others, and
`music-player.js` called itself "kept in sync with the node's" constant — so the
entire restriction lived in the caller, and a member's own message is not the
caller.
What that let through: this converts a whole file and holds a transcode slot
shared with video streaming while it runs. One message naming a two-hour film
spends minutes of the operator's CPU and a slot every other viewer is queued
behind. `AUDIO_TRANSCODE_MAX_BYTES` catches the result, after the work is done;
only the extension catches the work.
The client comment now says what it is — an optimisation that saves asking for a
refusal — rather than implying it is the rule.
Two tests: a film is refused before ffmpeg is reached, and a WMA still passes,
because a gate that refuses everything is not a gate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
the loop
Two things about the same three functions, which write to a temp file with ffmpeg
and then read it back.
**The read was on the event loop.** These files are ffmpeg's own, under
`tempfile.mkstemp` on the system disk, so they are not a group root and there is
no spun-down platter to serialise against — which is why they go through
`asyncio.to_thread` and not `roots.off_disk`. But a whole transcode read inline is
still tens of megabytes of blocking read while nothing else in the node is served.
The AST guard now covers the module with no exemption at all, and a second check
refuses a direct call to the reading helper: passed to `to_thread` it appears in
the syntax tree as a name, called inline it appears as a call.
**The audio transcode had no size ceiling**, where the subtitle path beside it
has had one all along. The bound is the media cache's rather than memory's:
`put_thumb` writes one SQLite row, and the store is 512 MB with least-recently-used
eviction, sized for thumbnails, posters and short transcodes. At 192 kbit/s a
three-hour source is ~260 MB — one row that evicts most of the cache to fit and is
evicted again by the next few thumbnails. Not a size this store can hold usefully.
64 MiB, about forty-five minutes: past any track, any single piece, most sets.
It takes away nothing that worked. `AUDIO_TRANSCODE_TIMEOUT_SECS` is 120, so a
source long enough to reach this was already liable to be killed mid-transcode;
what changes is that the refusal now names the limit it met and the size that met
it. Serving audio of that length properly means streaming the conversion instead
of buffering it, which is a different feature — recorded in §9.8 rather than left
as an implied promise.
The stat comes before the read, so an oversized result costs a stat rather than
the read and the memory behind it.
Twelve `test_sticky_header.py[firefox]` setup errors again: Firefox is still open
on this machine, and its `[chrome]` half passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|