diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-09 01:15:41 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-09 01:15:41 +0200 |
| commit | b3ea222ca8b6218e0187070fe94bc8fed7059a90 (patch) | |
| tree | 236ed70a1efffe4137078fa29410feb5bd459319 /packages/meshbay-node/src/meshbay_node | |
| parent | 77615ddb5fead3e74751a94847d3bcc99fc0a96d (diff) | |
| download | meshbay-b3ea222ca8b6218e0187070fe94bc8fed7059a90.tar.gz | |
fix(node): give the per-member transfer cap a door anyone can open
Reported as "the slots seem hardcoded to 2": `meshbay-node transfers set 8 8`
and still only two downloads at a time. Not hardcoded — that is the *per-member*
cap, which is a group's setting and is checked before the node's, so raising the
machine's total cannot move it. But the diagnosis was right in the way that
matters: nothing could change it.
`OP_TRANSFER_LIMITS` shipped with exactly one front door, the signed MNP
handler, and nothing anywhere opened it — no client call, no CLI verb, no
loopback route. So the cap sat at its default of 2 for ever, which from outside
is indistinguishable from a constant. CLAUDE.md states the rule this missed:
operator operations are one implementation with several front doors.
- `PUT /api/groups/{id}/transfer-limits`, calling the same
`ops.set_transfer_limits` the signed handler calls;
- `meshbay-node transfers per-member <downloads> <uploads> [--group X]`;
- `transfers show` now separates the node-wide pools from the per-group
per-member caps, and marks each `[set]` or `[default]`. It printed "2 per
member" with no indication of where the 2 came from, which is half of why
this looked like a constant.
Zero is refused here as everywhere else: it is not "unlimited", and a member who
may not transfer at all is a member the operator revokes.
Verified on a live node: the cap changes, survives a daemon restart, and
`transfer_probe.py --want 6` measures 4 granted against a cap of 4 where it
measured 2 before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/daemon.py | 58 | ||||
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/app.py | 10 |
2 files changed, 62 insertions, 6 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py index f7c1b33..e1cab61 100644 --- a/packages/meshbay-node/src/meshbay_node/daemon.py +++ b/packages/meshbay-node/src/meshbay_node/daemon.py @@ -1826,8 +1826,9 @@ def main() -> None: "| chat status|rotate|encrypt-history|prune " "| denylist show|clear " "| stun list|add|remove|reset " - "| transfers show|set: live transfer slots, and " - "the node-wide download/upload caps " + "| transfers show|set|per-member: live transfer " + "slots, the node-wide caps, and how many one " + "member may run at once in a group " "| reload: re-read node.toml (hot; systemd or the " "loopback API) | restart-daemon: restart the node " "(systemd unit, the Windows autostart launcher, or the " @@ -1845,7 +1846,7 @@ def main() -> None: "init|rotate for gek; " "list|rm for file; rematch for video; show|clear for " "denylist; list|add|remove|reset for stun; " - "show|set for transfers; " + "show|set|per-member for transfers; " "install|remove|start|stop|status for autostart and " "for service") parser.add_argument("target", nargs="?", @@ -2586,8 +2587,20 @@ def main() -> None: out = _daemon_api(cfg, "/api/transfers") for kind, pool in out.get("pools", {}).items(): print(f" {kind:<9} {pool['in_use']}/{pool['cap']} in use, " - f"{pool['queued']} queued, " - f"{pool['per_member']} per member") + f"{pool['queued']} queued (node-wide)") + # Per group, because that is the cap that decides how many one + # person runs at once — and it is not the node-wide number. An + # operator raising `transfers set 8 8` and still seeing two at a + # time is looking at this line, which used to print the node's + # default and say nothing about where it came from. + groups = out.get("groups") or [] + if groups: + print("\n per member, per group " + "(meshbay-node transfers per-member <dl> <ul> --group X):") + for g in groups: + how = "set" if g["set"] else "default" + print(f" {g['name']:<20} {g['download']} download(s), " + f"{g['upload']} upload(s) [{how}]") leases = out.get("leases", []) if not leases: print("\n nothing transferring") @@ -2624,7 +2637,40 @@ def main() -> None: f"(applied now, and kept in node.toml)") return - print("usage: meshbay-node transfers show|set <downloads> <uploads>") + if sub == "per-member": + # How many transfers ONE member may run at once in this group. Not + # the same knob as `set`, which is the machine's total — and the + # reason "I set 8 8 and still only get two" is the commonest + # confusion here: per-member is checked first, by design. + values = [v for v in (args.target, args.value) if v] + if len(values) != 2: + print("usage: meshbay-node transfers per-member <downloads> " + "<uploads> [--group NAME]") + sys.exit(1) + try: + downloads, uploads = int(values[0]), int(values[1]) + except ValueError: + print("error: both values must be whole numbers") + sys.exit(1) + if downloads < 1 or uploads < 1: + print("error: a cap below 1 is not 'unlimited'; it would stop " + "every transfer for that member. Revoke them instead.") + sys.exit(1) + group_id = _resolve_group(cfg, args.group) + out = _daemon_api(cfg, f"/api/groups/{group_id}/transfer-limits", + method="PUT", + body={"downloads": downloads, "uploads": uploads}) + got = out.get("limits", {}) + started = out.get("started") or [] + print(f"each member of this group may now run " + f"{got.get('download')} download(s) and " + f"{got.get('upload')} upload(s) at once") + if started: + print(f"{len(started)} waiting transfer(s) started at once") + return + + print("usage: meshbay-node transfers show|set <downloads> <uploads>|" + "per-member <downloads> <uploads> [--group NAME]") sys.exit(1) if args.command == "file": diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index 9b9307d..4ad3787 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -515,4 +515,14 @@ def create_ui_app(state: dict) -> FastAPI: async def get_transfers(): return await _op(lambda: ops.list_transfers(state)) + @app.put("/api/groups/{group_id}/transfer-limits") + async def set_transfer_limits(group_id: str, payload: dict): + # The same `ops.set_transfer_limits` the signed MNP handler calls. The + # op existed with only that one door, and nothing anywhere opened it — + # so the per-member cap sat at its default of 2 with no way to change + # it, which from outside is indistinguishable from a hardcoded 2. + return await _op(lambda: ops.set_transfer_limits( + state, group_id, + int(payload.get("downloads", 0)), int(payload.get("uploads", 0)))) + return app |