<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/CLAUDE.md, branch 0.7</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.7</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.7'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-24T14:43:18Z</updated>
<entry>
<title>docs: plan the Music group app (musicbay.md)</title>
<updated>2026-08-24T14:43:18Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T14:43:18Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=16bc07acf053d7d14f8182f5523da1d179154a15'/>
<id>urn:sha1:16bc07acf053d7d14f8182f5523da1d179154a15</id>
<content type='text'>
Proposal only, not implemented. Same plug-in mechanism as Videos
(apps.md), but no new streaming path — a track is downloaded and
decrypted like any other file, not transcoded/remuxed like a film.
Metadata: local tags first (mutagen), MusicBrainz/Cover Art Archive
as node-side fallback enrichment, no API key needed (unlike TMDB) —
just a rate-limited, self-identifying client. Player state (queue,
shuffle, repeat) moves up into the group-page shell so playback
survives a tab switch, mirroring how the video/preview modal is
already shell-owned.
</content>
</entry>
<entry>
<title>feat(node,hub): season-specific overviews, manual TMDB match correction, and wizard polish</title>
<updated>2026-08-24T12:33:20Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T12:33:20Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=0b0da86f1f9d6f0b1a27b5e1e1658c42de9f356a'/>
<id>urn:sha1:0b0da86f1f9d6f0b1a27b5e1e1658c42de9f356a</id>
<content type='text'>
Two operator-facing fixes for a real 3-season show whose automatic TMDB
match was wrong at the show level: per-season overview/air_date tabs in the
detail modal (falling back to the show-level text when a season's own is
empty), and a "Fix match…" search-and-correct affordance that re-resolves
every file sharing the corrected show's display_title. New signed op
OP_TMDB_OVERRIDE and two read-only pairs (season_meta_req/resp,
tmdb_search_req/resp), MNP_VERSION 0.5 -&gt; 0.6.

Also: the create-group wizard gets a spinning indexing indicator and an
app-selection step, group settings default the TMDB language to the
operator's own locale (never as a global default), and a file renamed
mid-session now re-triggers title parsing instead of being silently
skipped by the enrichment dedup guard.

Fixes two bugs found during this work: the search overlay's z-index lost
to the base video-overlay class and rendered invisibly, and season_meta's
own empty overview didn't fall back to the show-level one.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01LAmyXtc6dAADsH23ydXQpY
</content>
</entry>
<entry>
<title>feat(hub): split the group UI into a pluggable "applications" architecture</title>
<updated>2026-08-23T13:15:35Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-23T13:15:35Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9f02ee2c09652abf1308bdfa4a3eec4e9ca9ac83'/>
<id>urn:sha1:9f02ee2c09652abf1308bdfa4a3eec4e9ca9ac83</id>
<content type='text'>
GroupPage's 6620-line app.js carried Chat and Files wedged in directly, with
no way to add another group-level app without touching the shell itself. It
is now app.js (routing, non-group pages) plus nine focused files — apps.js
(the registry), chat-app.js, files-app.js, video-player.js, group-page.js
(the shell), group-settings.js, hub-client.js, icon.js and file-utils.js —
with docs/apps.md as the checklist for adding one (Videos/Music/Photos are
sketched there, not built).

Node side gained the matching enablement mechanism, mirroring
member_upload exactly: a roster setting, a signed apps_enabled op enforced
by _has_admin_authority, exposed in the handshake ack. Operators toggle
applications per group from Settings, which also gained a small reorder:
Invite, Pairing, Applications, Shared directories, Uploads, danger zone,
Your devices, Members.

Two bugs surfaced during the split, both missing an import across the new
file boundary and invisible to node --check or a module-load probe since
they only throw when the code path actually runs:

- group-page.js called onRefreshAuth on a stale-token handshake rejection,
  but app.js never imported refreshAccessToken from hub-client.js — so a
  brand new member (including a group's own creator) hit "Not a member of
  this group" and the retry silently failed, throwing before it could
  refresh the token.
- chat-app.js called getLocale() for message timestamps without importing
  it from i18n.js. Opening Chat on a group with real messages threw mid-
  render; uncaught, that appears to wedge Preact's render scheduler, so
  every button on the page stopped responding until reload.

Caught the second class of bug with a proper no-undef audit across all
split files (a temporarily installed ESLint 9, since the system one is too
old to parse this codebase's syntax) rather than trusting grep. 827 tests
pass; 6 new ones cover the apps_enabled policy.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
</content>
</entry>
<entry>
<title>feat(node): the operator can close uploading to everyone but themselves</title>
<updated>2026-08-18T16:21:57Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T16:21:57Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b86981f7b4ffe758136a527542ce256315823a46'/>
<id>urn:sha1:b86981f7b4ffe758136a527542ce256315823a46</id>
<content type='text'>
A group where every member may add files stays the default. Some groups want a
library the operator curates, and until now the only way to get one was to
designate no upload root at all — which refuses the operator too.

**The node enforces it; the interface merely stops offering it.** The Upload
button in the Files toolbar and the paperclip in the chat composer both
disappear, which is a courtesy to the people who are not trying. The control is
`_do_file_upload` refusing with `member_upload_off`, so a member on an old tab,
or one speaking MNP directly, gets the same answer. There is a test for each,
and the enforcement test is in the node package rather than beside the UI one so
nobody reads the hidden button as the mechanism.

**Changing it is a signed operator instruction** — `OP_MEMBER_UPLOAD`, on the
same path as removing a member. An unsigned one would let any member turn it
back on and make the setting a suggestion. The transcript's subject is `on` or
`off`: what the operator is shown before signing has to name the outcome, not
the operation.

**It lives on the node**, in a new `group_settings` table in `roster.db`. Not
the hub, which has no business deciding who may write to someone else's disk.
Not `node.toml` either: that file is hand-written and full of comments recording
decisions, `ops.py` appends to it rather than round-tripping it through a
writer, and a setting toggled from a panel must not rewrite the operator's file
or need a restart. The value is cached in the group context because the upload
path is synchronous, and the signed operation updates both — storing it without
applying it would make the panel say one thing while the node did another.

**Absent means allowed**, at every layer: no row in the table, no key in the
context, no field in `handshake_ack`. An older node and an older client both
behave exactly as before, and upgrading never silently closes a group. Each of
those three has its own test, because they fail independently.

The operator is always exempt — otherwise turning it off locks them out of their
own node with a config file and a restart as the only way back. `is_node_admin`
was being computed in two places by then and is now one function, since two
copies of "is this the operator" is how the ack and the gate come to disagree.

A change reaches everyone already connected via `member_upload_ack`, so the
button goes without a reconnection. That message is both a broadcast and the
reply to the request that caused it, which is why the client does not return
early on it.

Docs updated for a cold start: draft-v6 §2.1b and change 9, a new "Where Phase
13 stands" section in CLAUDE.md recording what is built, deployed and still
missing, the module map row, and desktop-client-v1 §10b on the Settings tab and
where group settings live.

883 tests pass.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix: the chat tab no longer scrolls, and a group is listed or invite-only</title>
<updated>2026-08-18T15:46:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T15:46:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cd2e89f5f5cccdb116db4fcb82d00b6325972782'/>
<id>urn:sha1:cd2e89f5f5cccdb116db4fcb82d00b6325972782</id>
<content type='text'>
**The chat tab was 8px too tall, at every window size.** The panel is sized from
JS to `viewport - top - 16`, which puts its bottom 16px above the fold — but it
sits inside `.main`, which adds 24px of padding below it. Eight pixels of
document past the window, whatever the window. Measured at 700, 900 and 1200:
`scrollHeight` 708, 908, 1208.

This is the second one of these — the sign-in card was `.page-center` and
`.layout` each reserving `100vh - 52px` — so it is now measured in the suite
rather than reasoned about. `tests/harness/scroll_probe.py` renders the real
markup against the real stylesheet and **runs the real `fit()` lifted out of
`app.js`**: a copy of the formula in a test would go on passing after the
original changed, which is exactly the bug being guarded.

The fix does not encode 24 anywhere. The first pass runs as before, then the
leftover is measured and taken off, so anything added below the panel later is
absorbed the same way. Now `scrollHeight == innerHeight` at all three heights,
nothing below the fold, and the panel still fills the room it has — that last
one has its own test, because shrinking the chat to 240px would satisfy every
other assertion here and be useless.

The Settings tab was measured too and is **not** a bug: it fits at 1200px and
overflows only when its content is genuinely taller than the window.

**Group creation asked one question twice.** Visibility and admission were
separate selectors that could only ever be set together — picking Public reached
over and set the policy — and two of the four combinations are meaningless. The
API already refused public+invite with a 422, so the form could build a request
that could not succeed. Private+open was accepted and should not have been: a
group anyone may join that nobody can find is a listing with the listing
removed, since joining goes through the node and there is no link to pass
around.

So: one selector, "who can join", and the request derives the rest. The API now
refuses the other impossible pair as well, with a message that says which way to
resolve it. Six locale strings the visibility box owned are deleted rather than
left unread in ten files, and the two surviving descriptions now say what each
choice means for who can *find* the group — with the word "public" gone from the
page, nothing else would have said it, and someone would publish a group without
meaning to.

865 tests pass.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(client): a film can go full-screen, and automatic saving is automatic</title>
<updated>2026-08-18T12:07:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T12:07:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c384878aa0d6ef7a33bda23887dc264b94386725'/>
<id>urn:sha1:c384878aa0d6ef7a33bda23887dc264b94386725</id>
<content type='text'>
**Full-screen was denied, and the denial was invisible.** The permission
handler was written from a true sentence — nothing here needs a camera, a
microphone or a location — and implemented as `callback(false)` for everything.
Chromium's own video controls ask for the `fullscreen` permission, so a film
could not be watched full-screen.

What made it hard to find, and what the test now pins: **a denied `fullscreen`
does not reject.** `requestFullscreen()` returns a promise that never settles.
No exception, no console message, nothing in the renderer that mentions a
permission — the button just does nothing. Measured rather than reasoned: the
probe reported `NEVER SETTLED` while the main process, instrumented for one run,
logged `PERMISSION ASKED: fullscreen`. After the fix the same probe reports
`granted` with `document.fullscreenElement` set.

The handler now enumerates what is *granted* — `fullscreen`, and nothing else —
so a camera, a microphone, a location, notifications and MIDI are still refused
and whatever Chromium adds next arrives refused rather than quietly allowed.
`Permissions.query` takes the other handler, so both now answer from the one
list instead of eventually disagreeing.

The old test asserted `callback(false)`, which is to say it locked in the bug.
It is replaced by three: what must stay denied, that `fullscreen` is granted,
and that both handlers read the same list.

**"Save automatically" opened a dialog.** The automatic path required a folder
to have been chosen first, and on a new profile nobody has chosen one — so the
very first download fell through to Save As, which is the one thing the setting
promises not to do. A browser does not make you pick a folder before it will
save a file; the system Downloads folder is the answer when there is no other.
Verified on a fresh profile with a home of its own: no dialog, 1024 bytes on
disk, destination reported as the default (`/home/…/Téléchargements` on this
machine, via the localized XDG directory).

A folder that *was* chosen and has since gone still asks. Silently redirecting
those files is worse than a dialog: someone who picked an external drive wants
to be told it is not there, not to find the film in their home directory a week
later. Settings shows the effective destination either way, and offers "forget"
only for a folder somebody actually chose.

813 tests pass.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(client): downloads stream to disk, and two rough edges on first run</title>
<updated>2026-08-18T11:40:15Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T11:40:15Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cf418a095b07c8127042390c748e721d1433879b'/>
<id>urn:sha1:cf418a095b07c8127042390c748e721d1433879b</id>
<content type='text'>
**Downloads were going through RAM.** `_openDownloadTarget` tries a granted
folder, then a service worker, then its floor: collect the whole file in the
page and hand the browser a blob. Both of the first two are absent in the
desktop application — `showDirectoryPicker` does not exist, and Chromium refuses
a service worker on a custom scheme — so every download under 512 MB took the
floor. A gigabyte of film meant a gigabyte of RAM, and the only visible symptom
was a Save As dialog at the *end* rather than the start, which is what the
operator noticed and asked about.

The main process now streams to disk: it honours "save automatically" with a
folder chosen once and no dialog, never overwrites (a colliding name gets a
suffix), awaits each write so the renderer cannot outrun the disk and queue the
file in memory anyway, and unlinks a cancelled download rather than leaving a
truncated file that looks complete to whoever opens it next. Settings now offers
the native folder picker instead of saying downloads are unsupported.

Measured in the running application: the file on disk grows 256 KB → 512 KB →
768 KB → 1 MB as the chunks arrive, and an aborted download leaves nothing
behind.

**A permanent scrollbar on sign-in.** `.layout` and `.page-center` each reserved
`100vh - 52px`, and `.page-center` sits inside `main`'s 24px vertical padding —
so the page overflowed by exactly 48px at every window size. Found by measuring
in the app rather than reading the stylesheet: `scrollHeight` 819 against a 771
viewport, then the bottom edge of every element. The centring page brings its
own padding, so main's is dropped for it and the duplicated arithmetic goes
rather than growing a third term. Now `scrollHeight == innerHeight`, no
overflowing elements.

**The first-run screen was unstyled.** It used a class name I invented
(`auth-page`) that appears nowhere in the stylesheet, so it had no card and the
button sat against the input. It now uses the same `page-center` + `login-card`
markup as sign-in, which is where the 12px gap comes from. The sign-in link in
the nav is hidden until a hub is chosen — it led to a page that could not work.

809 tests pass.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(client): three defects a real desktop found in ten minutes</title>
<updated>2026-08-18T10:54:29Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T10:54:29Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=06062d2e8352a205fa634970d260ab0f5de97f04'/>
<id>urn:sha1:06062d2e8352a205fa634970d260ab0f5de97f04</id>
<content type='text'>
All three came from the operator running the application on Ubuntu GNOME. None
would have been found by anything already in the suite.

**A second copy of the hub address.** `keyderive.js` carried
`const HUB = '' // same origin` — true of a page the hub served, false of one
loaded from a package, where the origin is `app://meshbay` and `/v1/users/register`
resolves against the application's own protocol handler. **Sign-up and sign-in,
the first two things anybody does, failed with "Not found."** The seam was
changed in `app.js` and in the signalling call and this was missed: the same
shape as the duplicate `MNP_VERSION` in `protocol.py`, a second copy of a
constant that is harmless until the context changes. `test_hub_address_seam.py`
refuses any file that decides where the hub is, and any `fetch('/v1/…')`
relative to the page origin.

**A window handler reading a variable another path reassigns.** Changing the hub
closes one window and opens another; `closed` arrives *after* the replacement is
assigned, so the outgoing window nulled the reference to the incoming one and
its `ready-to-show` crashed on it — a modal "A JavaScript error occurred in the
main process". Every handler now belongs to the window it was created with. The
CDP test wrote `config.json` in advance, so it never took the one path that
creates a second window; it does now, starting from an empty user-data dir.

**A first run that could not be undone.** The hub address was accepted on
anything URL-shaped and there was no way to change it afterwards — the prompt
only appears when none is set, so a typo meant editing JSON by hand. `https`
typed at a hub speaking `http` produced `TypeError: fetch failed`, which names
nothing. Now: the address is probed before being written, failures say which
URL and why ("does not speak https. If this hub is on your own machine, it is
probably http"), Settings can change it, and Electron's
"Error invoking remote method" wrapper is stripped from what a person reads.

Verified on the operator's desktop: **safeStorage really uses the GNOME
keyring** — Settings reports `gnome-libsecret`, and `secrets.bin` is written
0600 with Chromium's `v11` prefix, the marker for keyring-backed encryption
(the fixed-key fallback writes `v10`). Headless, the same code reports
`unavailable` and refuses to store rather than downgrading in silence, which is
now explained in Settings instead of shown as a bare word.

Unrelated but found while testing: `test_locales.py` assigned to
`globalThis.navigator`, which is read-only from Node 22. The client's build
already requires Node 22+, so the first CI machine configured for it would have
failed these tests for no visible reason. 809 tests pass on Node 18 and Node 24.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(client): hybrid sign-in — passphrase once, then this device's key</title>
<updated>2026-08-18T09:00:32Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T09:00:32Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=7c46b4e7dc2893974a37d6a701123a95803fcb95'/>
<id>urn:sha1:7c46b4e7dc2893974a37d6a701123a95803fcb95</id>
<content type='text'>
D4. The passphrase stays the account's credential and its only recovery path;
what changes is that it is not asked for on every launch.

**The renderer never holds the device key.** It is generated, stored and used
entirely in the main process, which signs `meshbay:user_auth:&lt;username&gt;:&lt;ts&gt;` on
request. Same rule as the save dialog, for the same reason: the renderer is the
part of this application that parses decrypted content from nodes, which is
attacker-controlled input. And this key is *not* a per-node identity key — those
are generated per node and never leave that relationship, so nothing here
correlates a person across operators.

First run asks which hub, with no default. A client that picks its own hub is a
client that can be pointed at one, and the address is the whole of what this
application trusts a hub for — the interface comes from the package.

Verified against a hub running this code, not against the deployed one:
register 201 → passphrase login 200 → device register 201 → **device sign-in 200
with a real session** → `/v1/users/me` 200 → a stranger's key 401. The
signature was also checked directly against the hub's own Python verifier before
any of that.

Inside the running application, over the debugging protocol: the bridge reaches
the main process, the renderer calls the hub **through it** (200 — the CORS fix
working end to end), and a call to a host that is not the configured hub is
refused.

**Not verified:** safeStorage persisting the key. This session has no secret
service, and standing one up in xvfb did not succeed. The application behaves
correctly there — it *refuses* rather than storing unprotected, and now says so
in Settings, which is a real case rather than a hypothetical one since it is
exactly what a headless or minimal desktop looks like.

Worth remembering for next time: meshbay.org runs whatever was last deployed. It
answered 405 on the Stage-C endpoints and reported MNP 0.2 while the tree had
0.3, so a local `uvicorn meshbay_hub.app:create_app --factory` on SQLite is what
tests hub changes. Nothing was deployed to production for this.

799 tests pass; e2e.py passes end to end.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(client): the desktop client runs, and running it corrected three things</title>
<updated>2026-08-18T08:30:59Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T08:30:59Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=68bfe56a19aeb4c16f8e185fc85d8eee61aef78f'/>
<id>urn:sha1:68bfe56a19aeb4c16f8e185fc85d8eee61aef78f</id>
<content type='text'>
Electron 42 / Chromium 148, launched under xvfb. The packaged interface mounts
over `app://` with a secure context, `crypto.subtle` present, Argon2 WASM
loaded, and no console errors. Three statements in the design were wrong, and
only launching it found them.

**A CSP in a `&lt;meta&gt;` tag silently drops `frame-ancestors`.** Chromium says so
in the console. A policy carrying a directive that does nothing is worse than
one without it, so the policy is sent as a header by the protocol handler —
which is also the only thing serving the interface, so one source instead of
two.

**`secure: true` is not what makes the service worker register.** Chromium
refuses a worker on a custom scheme whatever its privileges: "The URL protocol
of the current origin ('app://meshbay') is not supported". The application has
no service worker and needs none — it saves through a native dialog, which is
the better of the two paths. `sw.js` stays in the package because the same files
serve the browser, where it is one of only three ways to write a large file.

What `secure: true` is actually for was measured at the same time: without it
**the whole of `crypto.subtle` is undefined**. The first probe loaded a `data:`
URL and every algorithm failed with TypeError, AES-GCM included — which is why
the probe was rewritten before believing its answer. X25519 and Ed25519 are both
present on Chromium 148, settling the version floor left open as O6.

**The renderer cannot call the hub.** Its origin is `app://meshbay` and CORS
refuses it. The hub has *no CORS middleware at all* — its API is reachable from
no web origin whatever — and that is worth keeping. Widening it for
`app://meshbay` would be worse than it looks: that origin is not a credential,
since any Electron application can claim the same scheme and host name.

So every hub call leaves from the main process, exactly as saving a file does,
and it refuses any origin that is not the hub the user signed in to.
`platform.apiFetch()` is `fetch` in a browser and the bridge in the application,
so no caller has to know which it got. `transport.js` reaches it through a
global because it is a classic script, not a module — the alternative was a
second fetch path, which is how two callers of one hub start disagreeing about
how to reach it.

Verified from inside Electron: the main process gets 200 from
/v1/hub/version, the renderer is refused by CORS, and **a script served by the
hub is refused by the policy** — T3's mitigation demonstrated rather than
asserted.

Build note, written into the README because it will bite the next person:
**Ubuntu 24.04's nodejs 18 cannot install Electron at all** — the download
script `require()`s an ESM module, which Node gained in 22. Node 24 LTS,
checksum-verified against nodejs.org, is what this was built with.
package-lock.json is committed; builds use `npm ci`, not `npm install`.

799 tests pass, e2e.py still passes end to end. The session harness needed a
platform stub: it lifts `hubFetch` out of app.js as text and runs it, so the
adapter is now part of the environment it models.

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