<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/tests/test_security_regressions.py, branch 0.14</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.14</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.14'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-13T14:29:36Z</updated>
<entry>
<title>fix(node): the node's own controls take no authority from a hub token</title>
<updated>2026-09-13T14:29:36Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-13T14:29:36Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9da3cfc5d31bc4e1b9c4ea70f56e07b9aed8bb85'/>
<id>urn:sha1:9da3cfc5d31bc4e1b9c4ea70f56e07b9aed8bb85</id>
<content type='text'>
`_is_node_admin()` is `self._user_id == node_user_id`, and `_user_id` is the
`sub` of a JWT the hub issued. Six node-wide controls were gated on that alone:
`node_status` — which lists every group on the machine with each root's
**absolute path** — plus `node_settings_set`, `roster_read`, `denylist_read`,
`denylist_clear` and `node_reload`.

So the answer to "are you the operator of this node" was "the hub says so",
which NS4 and M3 rule out in as many words: operator authority comes from the
node's roster and from nowhere else, and asking the hub is how the hub installs
itself as node administrator. The reach is bounded — a completed handshake also
needs the group key — but an active hub obtains one legitimately in an
open-join group, which §3.5 concedes, and from there it could read the
operator's directory layout or clear the denylist, which is the persisted
revocation H4 exists to keep.

`_operator_device()` requires both halves now: the account is the one the node
belongs to, *and* the device on this connection has proved a key the roster
holds as an operator. `device_hello` is signed over a transcript naming the
node, the group and this connection's nonce, and `operator_pks()` is rebuilt
from the roster on each call, so an unpinned browser and a revoked one are both
refused at once. The hub holds no user keys and cannot countersign a device.

Keeping the account check as well is deliberate: dropping it would widen these
node-wide controls to any paired operator of any group on the machine, which is
a separate decision. `_is_node_admin()` stays as what it is in the handshake
ack — a hint telling a client whether to offer the Node page — and says so.

Nothing changes for a paired operator: `device_hello` runs unconditionally
after the ack, and anyone using the Node page's controls is already paired,
since `root_add` and every other signed op has always verified against
`operator_pks()`. A browser that never paired now reads nothing there, which is
the state in which it could already write nothing.

test_node_status.py's fixture set the account and not the device, which is how
it went on passing; it now wires the device the way `device_hello` leaves it.
The adversary itself is in test_security_regressions.py — a token naming the
owner's account with no proved device, which the previous source answered with
`node_status_ack`.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UMxEQadpzPkYLFf5CYKhpW
</content>
</entry>
<entry>
<title>feat(mnp)!: seal the upload under the group key</title>
<updated>2026-09-07T15:46:33Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-07T15:46:33Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8980a8e42d94ab7c0bc9739283d39f938f8402b0'/>
<id>urn:sha1:8980a8e42d94ab7c0bc9739283d39f938f8402b0</id>
<content type='text'>
Downloads have been encrypted under a GEK-derived key since the beginning:
`file_chunk` and `stream_data` both go through `chunk_ciphertext`. Uploads
never were. `file_upload` carried the filename and the raw bytes in plain
msgpack, and `file_upload_ack` carried the name the node stored them under —
so the same file was ciphertext leaving a node and plaintext arriving at one.
There was no threat model behind that asymmetry.

Both halves now travel sealed under a third groupbox purpose,
HKDF(GEK, info="meshbay:upload:v1"). The filename, the destination folder and
the bytes are all inside the seal; only `upload_id` and `chunk_index` stay in
clear, because the node routes and orders on them before it can decrypt. This
direction seals *towards* the node — it holds the GEK for its own group — and
it opens the payload before it picks a destination or touches the disk.

What that forced, and why none of it is optional:

  - `filename` was the correlation key on both sides. It cannot be: matching an
    ack to its request by name would hand back exactly what the seal hides.
    `upload_id` replaces it — client-drawn, opaque to the node, unique within a
    connection, never an authorization input. The property it guarded (one
    refusal fails one upload, not every upload in flight) is unchanged.
  - Refusals can no longer quote what they refused. `No directory named 'X'`
    becomes `No such directory in this group` plus the `code` that was already
    there; the client knows what it sent.
  - No plaintext fallback. A path that still accepts plaintext is not a sealed
    path, so an unsealed `file_upload` is refused with `upload_not_sealed`.

Hardened while here, because what comes out of a seal is authenticated but not
validated — a member can seal anything: `filename` and `data` have their types
checked before any upload state is created, and `chunk_index`/`total_chunks`,
which are outside the seal by necessity, can no longer raise where a refusal
was meant.

Tests. `test_upload_sealed.py` pins the node half: nothing identifying on the
wire, tamper/wrong-key/wrong-group all refused with nothing written, and
multi-chunk reassembly unchanged. `test_upload_seal_client.py` drives the
shipped `uploadFile` over the shipped `crypto.js` under node and feeds its real
frames to the real `_do_file_upload` — the file lands intact, and the ack the
node actually produced comes back with the name it chose for a collision, which
is the half a source-reading test cannot see. Both upload purposes join the
JS/Python groupbox parity vectors.

BREAKING CHANGE: MNP 2.0. `file_upload`/`file_upload_ack` change shape on the
wire every deployed client speaks, which is MAJOR by the same rule 1.0 was —
but the break is confined to uploads. `MNP_MIN_SUPPORTED` stays at "1.0", so a
1.x peer still connects, browses, downloads, streams and chats; only its
uploads are refused, with a message saying which side is old. The client checks
the node's version before sending a chunk, so neither side meets this as a
timeout. This is the version negotiation shipped in 1.0 earning its keep: 1.0
cost a flag day, 2.0 costs a refusal code.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
</content>
</entry>
<entry>
<title>refactor(mnp)!: remove stream_seg, the last unencrypted content message</title>
<updated>2026-09-07T15:45:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-07T15:45:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=77dd077491aea50e71e21e0d17555a2f91cf818b'/>
<id>urn:sha1:77dd077491aea50e71e21e0d17555a2f91cf818b</id>
<content type='text'>
`stream_seg` answered with an MPEG-TS segment as base64 with no encryption at
all — the one message on the content plane that never went through a
GEK-derived key. Live on both transports, answering any authenticated member.

It predates `stream_data`, which does the same job properly (`chunk_ciphertext`,
keyed per segment, AES-256-GCM) and has since Phase 12. Its only browser caller,
`fetchStreamSegment`, was defined and never once invoked — a plaintext media
endpoint with no client. Removed rather than repaired.

Gone with it: `_extract_segment` and the ffmpeg semaphore in quic_server, the
`fetch_stream_segment` QUIC client method, and `_b64decode` in transport.js,
which had no other caller.

The H6 regression test lived on this handler — it pinned `_do_stream_segment_async`
to a coroutine so `subprocess.run` could not stall the event loop for thirty
seconds per request. It is replaced by the property that outlives the handler:
no transport carries media outside an AEAD, asserted on `stream_seg` and
`data_b64` across all three transport modules. The half of H6 that survives —
the live streaming path still spawns ffmpeg — keeps its own test.

BREAKING CHANGE: `stream_seg` is no longer answered on either transport. No
shipping client sends it. Recorded as part of MNP 2.0, whose other half — the
sealed upload — carries the version bump.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
</content>
</entry>
<entry>
<title>feat(node): an upload lands in the folder it was sent to</title>
<updated>2026-09-06T20:06:45Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-06T20:06:45Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f3fb449f3a943096a2569dc383f2819a612bccd5'/>
<id>urn:sha1:f3fb449f3a943096a2569dc383f2819a612bccd5</id>
<content type='text'>
There is no `uploads/` subdirectory any more, and the client names the folder
rather than the root.

It was the last of v5's quarantine — the per-user layer went on 2026-08-14 for
the same reason — and it goes on the same grounds: a folder appearing beside
the operator's library because somebody sent a file is the node deciding how
their disk is arranged. Somebody dropping a file into the folder they are
looking at expects it to be in that folder.

**What made the quarantine worth having was never the subdirectory.** It is the
filename allowlist, the size cap, the chunk ordering and the no-overwrite rule,
and all four are untouched: an existing file is never replaced, the second
sender of IMG_1234.jpg gets a free name, and the check still sits at the write.

Letting the client choose the destination is safe for one reason and only one:
it is resolved through `RootSet.resolve()`, which refuses `..`, absolute
segments and anything whose resolved form escapes its root, symlinks included.
A member answers "which of this group's folders", never "which path on the
operator's disk" — and the test that used to assert the node chose now asserts
that, with six shapes of escape.

`direct` goes with it. Its only job was to say "no subdirectory for this root",
which is now every root, and a config flag that does nothing is worse than none.

Chat's attachment folder finally does something: the directory the operator
picks in the Chat settings pane is where attachments are written, falling back
to the first writable root while they have not chosen one, or if the one they
chose has since been made read-only or ejected — a stale choice should not
become a refusal at send time.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
</content>
</entry>
<entry>
<title>fix(groups): finish Phase 1 — MNP root management, upload targets, eject state</title>
<updated>2026-09-06T15:48:36Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-06T15:48:36Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ea56b8c79538323875c00db2e7006b255f7cd494'/>
<id>urn:sha1:ea56b8c79538323875c00db2e7006b255f7cd494</id>
<content type='text'>
Review of the Phase 1 commit found the RO/RW model sound but three paths
unfinished, each of which broke the flow the phase exists to deliver. Plus
29 test failures it introduced and no coverage for anything it added.

Uploads went to the wrong directory. The node read a `root` field on
file_upload that no client ever sent, so every upload landed in the first
writable root while the Files toolbar offered its button based on the root
being browsed — with two writable roots, uploading from one wrote into the
other. Files now names the root it is showing; Chat names one chosen in the
shell (an operator-configured directory arrives in Phase 2); the node refuses
an unknown name rather than falling back, and refuses read-only and ejected
roots by code.

Shared directories were unreachable on the web. The table read its roots
only from the loopback API, which resolves to "not available" in a browser,
so the section rendered for nobody there — while the Uploads controls it
replaced had worked — and the transport.updateRoot/ejectRoot/plugRoot methods
beside it were dead. MNP is now the path, loopback the fallback for a local
node with no live connection, and adding a root over MNP takes a typed path
since no web page can browse a remote disk.

Ejecting updated nobody's screen. transport.js resolves an admin ack against
the pending request and returns, which is right for every op whose caller
knows the value it chose; the root acks carry state only the node can compute,
so the operator who clicked Eject was the one client that never saw it happen.
And the ejected flag reached roster.db but was never read back, so a restart
undid it and the next scan read an empty mount point as an erased library.

Also: the member-upload endpoint answered 200 and did nothing (removed); the
wizard ignored the first root's RW switch; reload compared roots on name and
path, so editing writable in node.toml did nothing; the table had no path
column, which is the only thing separating two libraries sharing a basename;
apps_enabled normalisation differed between the two sides of a signed subject.

Tests: eject/plug, per-root upload refusal and the node.toml rewrite had no
coverage at all. test_member_upload_policy.py is replaced by
test_root_writable_policy.py — it tested a removed feature — and every
property worth keeping from it moved rather than being dropped.

Docs: draft-v6 structural decision 9 is annotated as superseded (the operator
can no longer have a directory only they may write to — a real capability
removed, flagged rather than hidden), the man page documents the root verb and
the RO/RW fields, and refactor-groups.md §7b records what the plan got wrong.

Suite: 41 failures before, 13 after — all 13 pre-existing on main.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
</content>
</entry>
<entry>
<title>test(node): make the suite pass on Windows</title>
<updated>2026-09-04T00:20:57Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T00:20:57Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d11e571c5b6c24b586ef5b8fb2cfcf6a6bfa6d6d'/>
<id>urn:sha1:d11e571c5b6c24b586ef5b8fb2cfcf6a6bfa6d6d</id>
<content type='text'>
- `.read_text()` on source files now `encoding="utf-8"` — cp1252 chokes on
  the em dashes / box-drawing chars those files contain.
- test node.toml templates embed paths via `Path.as_posix()`: a raw Windows
  path in a basic TOML string is a parse error (`\U`, `\a`, ... are escapes).
- new `test_platform.py` covers `meshbay_node.platform` by mocking
  `sys.platform` / `os.environ` — runs on both OSes.
- `skipif(sys.platform == "win32")`, in `conftest.needs_subprocess` and
  inline, for the documented gaps: ffmpeg/ffprobe via asyncio subprocess
  (the win32 selector loop, forced for aiortc, cannot spawn one), the
  systemd `reload`/`restart-daemon` delegation (Windows path is W3), the
  keystore `st_mode == 600` assertion (NTFS ignores mode bits), and the
  symlink-escape test (needs Developer Mode).

Windows: 781 passed, 25 skipped. No change on Linux.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>refactor(node): JSON-only control API, Node page absorbs the admin dashboard</title>
<updated>2026-09-01T12:08:32Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T12:08:32Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cfc91e0a424163869c64d30e55d55a53f18a3dbf'/>
<id>urn:sha1:cfc91e0a424163869c64d30e55d55a53f18a3dbf</id>
<content type='text'>
Remove the node daemon's server-rendered admin UI (GET / and /audit, the
_render_* helpers and inline templates) and the `meshbay-node ui` CLI verb.
The loopback control API stays; it is now JSON only, ruff-clean, and 453
lines (was 1074). Also drop three never-wired endpoints (/api/config,
/api/chat/history, /ws/chat, plus broadcast_chat) and the pointless
18000/tcp firewall profiles.

The desktop client's Node page (static/node-page.js) takes over what the
dashboard showed, reorganised into six tabs (Overview, Groups, Roster,
Peers, Audit, Settings):
- Overview: version, node id, QUIC port, hub, index-cache maintenance
- Roster: node-wide view with unpin
- Peers and Audit: auto-load on open, no Load button
- Audit: real usernames and group names (resolved from the roster and
  node.toml), Previous/Next pagination newest-first, Export CSV of every
  matching row
- Settings: node settings, STUN, ICE, denylist, then Unlink from hub

Backend: audit.get_entries gains `offset`; /api/audit and /api/peers
resolve ids to names via a new _display_names helper; CSP tightened to
default-src 'none' now that no HTML is served. draft-v6 sections 2.11 and
2.12 corrected -- the Node page uses the loopback API, not MNP.

One capability is intentionally dropped: browser-based admin on a headless
server. The CLI covers every operation there.

See docs/refactor-node-ui.md.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01MQCaZnde4Bjjdu84dhSuF5
</content>
</entry>
<entry>
<title>fix: first-run wizard reliability and node startup performance</title>
<updated>2026-08-22T14:40:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-22T14:40:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9c136a0e37add42f5d0c8675a797969cfe690de0'/>
<id>urn:sha1:9c136a0e37add42f5d0c8675a797969cfe690de0</id>
<content type='text'>
Node daemon no longer blocks startup on slow directory scans — initial
indexing runs in the background so the node reaches "running" immediately
after transports are up. Fixes the wizard failing to detect the node when
large USB/NAS roots take minutes to scan.

Also: wizard key-linking deadlock resolved (main.js links during poll),
invite form stays in DOM during reconnects (disabled instead of destroyed),
pairing code bridges to renderer, and firewall docs for LAN casting added.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(node): several named roots per group, and one implementation per operation</title>
<updated>2026-08-18T00:15:02Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T00:15:02Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e9d5e979fdab9a1cc3c729d602e6f27207b9480c'/>
<id>urn:sha1:e9d5e979fdab9a1cc3c729d602e6f27207b9480c</id>
<content type='text'>
Stage A — a group's content is a set of named roots
---------------------------------------------------
`shared_dir` becomes a list of {name, path, kind}. The name is the directory's
basename, derived once at add time and *stored*: recomputing it would
re-identify a whole library the day someone renames a folder on disk. Duplicate
names are refused case-insensitively and no root may contain another — both
compared with NFC folding, because most of these directories live on exFAT or
NTFS where `Films` and `films` are one directory.

Every index path carries its root name, in a one-root group as much as in a
five-root one. One path shape has to be got right once; two have to be kept
right for ever.

**A root that goes away freezes; it never empties.** Unmounting a volume makes
watchdog report every file under it as deleted, or presents an empty directory
to the next scan. Acting on either propagates deletions for a whole library to
every member, as though the owner had erased it. So a deletion is acted on only
once its root is confirmed readable, and availability is tracked per root — one
unplugged drive leaves the others serving. 12 tests, verified to fail against an
indexer without the check.

Events are not trusted to be complete either: ReadDirectoryChangesW drops them
under load and inotify on a FUSE mount misses changes made outside it. A
periodic reconciliation sweep is the only thing that recovers a missed event.

MNP 0.2 → 0.3 (additive). The hub needs no change: SwarmSource carries a content
hash, a node id and an endpoint — no paths, no filenames — and private groups
register nothing (H7).

Stage B — one implementation behind every front door
----------------------------------------------------
C1 and C6 were both "a second path into the node with its own weaker
handshake". Two implementations of `revoke` with two authorization checks is
that shape one size down. `meshbay_node/ops.py` holds each operation once,
takes the daemon state, and knows nothing about HTTP, argv or MNP. The loopback
API is one `_op(...)` line per endpoint; the MNP handlers call the same
functions. test_ops.py asserts the shape rather than trusting it.

Phase 14 is finished on top of it — `group list`, `gek init|rotate`, `reload`
(SIGHUP), `denylist show|clear`, `file list|rm`. **No operator action requires a
browser any more.** Plus `gek_rotate` and `member_unpin` as operator-signed MNP
operations: rotation is the half of revocation that revocation cannot do, since
the ex-member holds the current key, and the node generates the replacement
with its own CSPRNG — no key material crosses the wire, which is what the C5b
rule is actually about.

Two bugs found by running it rather than by testing it
------------------------------------------------------
GroupIndex is keyed by **content hash**, so the same bytes at two paths are one
entry — which is also why a scan reports ten files and indexes nine.
Reconciliation compared paths, so it decided the second path was a missed event
every 60 s, rewrote the entry and pushed an index update to every connected
peer. Seen in a live node's log.

`meshbay-node reload` crashed on first use with `subprocess` unimported: the
module compiles fine, which is the "syntax, not names" trap already recorded for
the SPA. test_cli_dispatch.py now walks every verb and refuses to let one be
added to the parser without an entry there.

Also corrected: protocol.py declared a second MNP_VERSION of "0.1" while the
wire carried "0.2" — harmless only because nothing imported it. And
_do_dir_create/_do_dir_delete referenced an undefined `filename` on their error
path.

740 tests pass; QE/deploy/e2e.py passes end to end against the live deployment.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(node): stop losing transcode slots, and reap ffmpeg without deadlocking</title>
<updated>2026-08-16T13:28:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-16T13:28:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=188a76f52d2f30609147b1beee7754f2cbd1e778'/>
<id>urn:sha1:188a76f52d2f30609147b1beee7754f2cbd1e778</id>
<content type='text'>
Reported from a phone: play a video, close the viewer, open another — the second
hangs and the third is refused. Three separate causes, found by instrumenting
rather than guessing, after two fixes that addressed real but different bugs.

A task nobody holds can be collected mid-flight. asyncio keeps only a weak
reference, so `ensure_future` with the result discarded may be garbage-collected
while running — "Task was destroyed but it is pending!" — and `_stream_video`
never reached the exit of its `async with sem`. `_spawn` holds every background
task; all nineteen call sites go through it.

Losing the peer must stop its work. The connectionstatechange handler popped the
session from a dict and nothing else, so a closed tab went on transcoding for
the full 120 s credit timeout. Measured in the log: 91 s of ffmpeg after the
connection closed. `shutdown_tasks()` now runs on the way out, and the credit
wait checks the channel before sleeping and polls in slices instead of once.

And `await proc.wait()` after `kill()` still deadlocks. ffmpeg outruns a
credit-paced viewer and fills the stdout pipe; stop reading it and the transport
cannot finish closing, SIGKILL or not. Measured against the live node with a
169 MB video, closing the viewer after 20 segments and asking for the next one:
15.1 s then "Server busy" before, 0.1 s / 0.0 s / 0.0 s after.

Chunk replies wait for room on the channel. Eight megabyte-sized chunks answered
as they arrived queued 8 MB with nothing watching — measured at 7.3 MB of
bufferedAmount in milliseconds. Fine on a LAN, minutes of head-of-line delay on
a busy link.

Upload names accept any script. The rule was ASCII-only, so `été.txt` was
refused — and so was `rapport (1).pdf`, which is the form `_free_name` produces
itself, meaning the node rejected names it had chosen. Widened to Unicode with
the C5a and H2 protections intact, plus a refusal of names that lie about
themselves: trailing space or dot, and the right-to-left override. Errors now
name the file, so one bad name no longer fails every upload in flight.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
