Repository navigation
fix: review of today's merged PRs (#40-#51) - #52
Conversation
A link could be minted for a stopped CT; the upload was then received in
full, spent the link and failed in `pct push` ("can only push files to a
running CT"). The endpoint answered 502, which Cloudflare replaces with
its own "error code: 502" page, so the uploader never saw why.
- proxmox_upload_link, proxmox_fetch_url and the shared push path check
that the guest is running before any byte moves.
- The upload route answers 409 when the guest stopped since the link was
minted and 500 for other push failures, both with the JSON error.
- Document the Cloudflare body cap next to server.transfers_max_mb.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
- Refresh tokens: a rotated-out token is kept until its family ends and reuse is checked before its idle expiry. With a refresh_token_ttl below the family lifetime, a replay after the TTL used to come back "invalid" and leave the family alive. - TOTP lockout: a source that passed TOTP lately is exempt from the per-client cap (its own per-source cap still applies). Two attacker addresses at 5 strikes each could otherwise lock the owner out of every TOTP path, again every cooldown. - The lockout is keyed on the seed owner: derived (DCR) clients check codes against the owner's seed, and each one had its own guess budget. - Warn when X-Forwarded-For comes from a peer missing from trusted_proxies, not only when the list is empty. `cloudflare` covers the edge ranges, not a cloudflared tunnel's LAN address, so behind a tunnel every visitor shared one budget without a word. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
qemu-server's agent/file-write base64-encodes `content` itself unless encode=0, and agent/file-read decodes the agent's base64 unless decode=0. proxmox_write_file sent pre-encoded content with encode=1, so the guest got the base64 text instead of the file while the tool said success; proxmox_read_file decoded the already-decoded content a second time, failing on most text files and handing raw bytes back as content_base64. - write: send with encode=0 and cap the content at 45 KiB, what fits PVE's 60 KiB `content` limit once encoded (the 1 MB the tool used to advertise never worked past ~45 KiB). - read: ask for decode=0 and count=limit+1, decode once; fall back to the latin-1 bytes on PVE versions without `decode`. Checked against /usr/share/perl5/PVE/API2/Qemu/Agent.pm on PVE 9.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
read_ui_resource passed an AnyUrl to ClientSession.read_resource. In mcp 2.x the request's uri is a strict str, so every panel read failed validation and /app/api/mcp/panel answered 502: the VM, logs and cluster panels never rendered in the chat. Pass the str; the test fake now rejects anything else, like the real client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
The loopback/link-local guard resolved the host in an httpx request hook, then httpx resolved it again to connect. A name answering a public address first and 127.0.0.1 next (DNS rebinding, TTL 0) passed the check and the download read a loopback-only service into the guest. A failed first lookup was let through as well, so SERVFAIL-then-127.0.0.1 won every time. The fetch now uses a transport whose network backend resolves once, refuses local addresses, fails closed on resolution errors and connects to the checked IP (TLS still verifies the URL's hostname). Every redirect hop goes through it. It also stops HTTP(S)_PROXY from the environment applying, since a proxy would resolve the name out of the guard's reach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
… a directory dest - The staged copy on the node is removed even when the push is cancelled or the SFTP put fails half way. - A sha256sum that exits non-zero for any reason but "not installed" is an error instead of a silent success. - A QEMU push to a directory is refused with a clear message. - The upload handler always settles the link and writes the audit entry, even when the request is cancelled. - A loopback request base is never used to build a public upload URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
…y enforces With server.allowed_origins left out of beaconmcp.yaml, CORS used the built-in list (claude.ai, ...) but /oauth/authorize and DCR checked the empty YAML value, so the Claude connector got a 400 on a default install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
asyncssh retries a non-22 port without the port, so a forwarded port on the same address as another machine's port 22 was refused as a changed key (or accepted on the wrong key). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
A read timeout on vm_clone or snapshot_create was retried, which could start the task twice. Non-GET calls now retry only when the TCP connection never opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
…hat they claim
- systemd expands ${VAR} in a transient unit's ExecStart, script text
included, so ${10} reached sh empty and the settle re-check never
waited. The script now has no dollar-brace and a test keeps it so.
- Healthy now means /health keeps answering for 30 s with the same
MainPID and NRestarts: two samples could land on two incarnations of
a crash loop.
- The rollback pip is bounded by timeout(1).
- Release tags are mirrored into refs/beaconmcp/tags/ with --force
--prune: one re-pointed upstream tag made every later fetch --tags fail.
- A signed tag must carry its own name (a signed rc republished as a
release was accepted).
- No restart is scheduled when the server runs outside its unit.
- Docs: any key in the keyring counts, and GitHub's web-flow key proves
little on the branch channel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
|
LGTM 👍 Went through the src diff and checked the agent file I/O against Non-blocking nits, fine as a follow-up:
|
|
Thanks! All four notes are addressed, each with a test, in the follow-up #53. This branch no longer accepts direct pushes, so the follow-up is stacked on this one: merge #52 first, then #53 shows a single commit. Generated by Claude Code |
|
why the fuck are you opening new PRs? Just update this one it isnt merged yet. |
|
Sorry for the noise. I tried to push to this branch first, and GitHub refuses it. It just did again: So a repository ruleset covers every branch, this one included (https://github.com/Showdown76py/BeaconMCP/rules?ref=refs%2Fheads%2Fc%2Fmagical-turing-tqjhj2-giga), and that is why I opened #53. Two ways out:
Generated by Claude Code |
Review of every PR merged today (#40, #41, #42, #43, #45, #46, #47, #48, #49, #50, #51; #44 shipped inside #48), plus a live test of the transfers on a real PVE 9.2 host behind a Cloudflare tunnel. Every fix below comes with a test that fails on current
mainand passes here. Full suite: 705 passed, ruff clean.Transfers (#49, #50)
proxmox_fetch_url: the hostname was checked, then resolved again by httpx at connect time. A custom httpcore network backend now resolves once, refuses local addresses and connects to the address it checked. It fails closed when resolution errors.sha256sumthat exits non-zero for any reason other than "not installed" (127) is now an error instead of a silent success.destis refused with a clear message.transfers_max_mb.Guest file I/O (pre-existing, confirmed against PVE's
Agent.pm)proxmox_write_filesent raw text while PVE base64-decodes by default. It now sendsencode=0and caps content at 45 KiB, since PVE limitscontentto 60 KB. The error points to the upload tools.proxmox_read_filein binary mode returned PVE's decoded bytes as if they were base64. It now reads withdecode=0and returns real base64, and falls back on older PVE.Auth (#40, #42, #45)
server.trusted_proxies.OAuth (#41)
server.allowed_originsleft out ofbeaconmcp.yaml, CORS used the built-in list (claude.ai, ...) but/oauth/authorizeand DCR checked the empty YAML value. The Claude connector then got a 400 on a default install. Both checks now use the list the server actually enforces.SSH TOFU (#46)
[host]:2222was checked against the key ofhost:22, often another machine behind the same NAT address. Keys are now pinned and matched per host and port.Proxmox client (pre-existing)
vm_cloneorsnapshot_createwas retried and could start the task twice. Writes are now retried only when the TCP connection never opened._prune_exec_sessionsiterated a dict that other threads were writing to. It now iterates a snapshot.Dashboard (#48)
read_resourcenow takes astr).Self-update (#47)
${VAR}in a transient unit's ExecStart, script text included, so${10}reachedshempty. The script no longer contains a dollar-brace or$$, and a test enforces that./healthkeeps answering for 30 s with the sameMainPIDandNRestarts. Two samples could otherwise land on two incarnations of a crash loop.pip installis bounded bytimeout 900.refs/beaconmcp/tags/with--force --prune. Before, one re-pointed upstream tag made every laterfetch --tagsfail.v2.1.0-rc1republished asv2.1.0used to pass.Not changed
git statusindex.lock race during a rollback (a window of milliseconds, unverified).🤖 Generated with Claude Code
https://claude.ai/code/session_01HpjwiAg4NUssQYBz8D8Rbz
Generated by Claude Code