Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 8d8e26c4b51e482b88f43b3fefc95bee
Branch: dev/4-442-readonly-settings-writes
Base: 9863992
At: 2026-11-02T23:35:24Z
Credit: found by FlyBot Security while re-reading #2221; three shapes measured by FlyBot Product (probe + log quoted below).
Kind: bug (a read endpoint writes the settings file in a read-only window)
Source: FlyBot Product audit P-442 plus Lead re-read of every line on live main 986399246ff8c26bd418978785ce1199dbd239ca.
Dedupe (searched before opening)
Open issues matching settings read-only, saveSettings, read-only write settings: none. #2219 (closed, merged as #2221) covered the read-only shell (sidebar row, room dialog, + menu) and did not reach this file's save path. #2202 is the workflow double alert, #794 live E2E — unrelated. The team backlog holds this as P-442, not as an issue.
What is wrong
plugins/social/src/social.js:683
const saveSettings = () => { if (settingsPath) { try { writeCredentials(settingsPath, settings) } catch (error) { warn('settings not saved', error) } } }
has no flybot.readOnly?.() guard, and remoteBots() reaches it from read endpoints:
social/remote-bots—plugins/social/src/social.js:3029, registered{ read: true };social/friends—:3365, also{ read: true }, called by the Friends panel's 30-second cache (plugins/social/client.js:3842), so simply having a second window open on the Friends panel triggers it.
Write sites reachable from those reads: backfillInboundFromRoster (:1009, saves at :1023), applyRemoteRoster (:1113, saves at :1115), the accept-notice cleanup (:1161-1164), and syncServerAutoApprove (defined :3381, called from :1194, its set_auto_approve at :3374).
The plugin already knows the signal and uses it in seven other places (:764, :806, :2452, :3630, :4245, :4281, :4610); saveSettings is the one that does not. :4610 is the guard #2219 itself added for social/face-open, so "a read endpoint must not write" is already this file's own convention — saveSettings is the single place that breaks it.
Measured on live main (Product's probe plugins/social/test/probe-settings-readonly.test.js, same read-only proxy as the repository's own test/social.test.js:1023; re-run unchanged against 98639924 after #2224 merged — 3 of 3 still red, # pass 0 / # fail 3):
- RO-A, a read creates the file. With no
flybot-social-settings.jsonon disk, one read-onlyremote-botscall makes it appear (sha 5435de377e4cd9d1) carrying{"inboundBots":{"@coder.bob.home":true,"@writer.bob.home":true},"inboundBackfilled":true,…}— the one-shotbackfillInboundFromRosterupgrade. - RO-B, two reads each write. With
hiddenRemotes:{"@gone.bob.home":{absentFetches:0}}preset, two calls change the file twice (sha dbdae9c917a63d95→81a8165568913727withabsentFetches:1→d32b7db9b6ffb2b1, entry dropped). - RO-C, a stale window resurrects a cleared entry. Two read-only windows build their instances before either writes; A's two reads delete the entry (
{}), then B's next read writes{"@gone.bob.home":{"hiddenAt":1,"absentFetches":1}}back from its older in-memory copy.
So a window the host has told "close the other one and reload" still edits the home's shared settings file, and the lost-update case in RO-C means a cleared hidden entry can come back.
Expected
- In a read-only window (
flybot.readOnly?.()truthy) a read call writes nothing: no file created, noshachange, no stale copy flushed. The derived values the caller needs are still computed and returned for that call. - With no read-only signal, behaviour is unchanged: the backfill, hidden-remote aging and accept-notice cleanup still persist exactly as today.
- The two read endpoints stay
{ read: true }— dropping the flag would hide Friends from a read-only window and regress#2221's read half.
Red first (must fail on live main)
- Read-only view, no settings file: call
remote-botstwice, the file must still not exist. Today it appears on the first call. - Read-only view with a preset
hiddenRemote: two calls leave the file'sshaunchanged and the entry atabsentFetches:0. Today it is written twice and the entry disappears. - Two read-only instances sharing one
settingsPath: the entry A removed is not written back by B's next read. Today it is. - Counter-proof (must not turn red): the same fixtures on a writable view still persist the backfill, the aging and the cleanup.
- Read half intact: a read-only view still answers
remote-bots/friendswith the roster (non-empty), so the fix is not "refuse the read".
Fix (smallest)
Guard the save, not the derivation: saveSettings() returns without writing when flybot.readOnly?.() is truthy, leaving the in-memory derivation in place for the caller and letting the writable window persist the same change next time it writes. Cite the seven existing flybot.readOnly?.() call sites in the PR so the pattern is the plugin's own.
Alternative, only if a reviewer prefers it: move the derived cleanup out of the read path into the write path. That is a bigger diff (three call sites plus syncServerAutoApprove) and not needed to close the report.
Do not remove { read: true } from :3029/:3365, and do not add a refusal to the read itself.
Scope
plugins/social/src/social.js only. Out of scope: the read-only UI surface (plugins/social/client.js, that is #2228) and the social/face-* endpoints (they are writes and stay refused). A changelog entry changelog.d/<issue>-social-readonly-settings.fixed.md is required; docs/subsystems/social.md gets one line if it describes the settings file's write points — check before adding.
Tier: plugins/social/src/social.js → T2 by the path table, so the PR needs Birfy's Verdict: PASS at the exact tip before it is enqueued. The T1 draft-until-advisory rule does not apply here (that rule is for core/src/client/**, tests, docs, patches, changelog); FlyBot Test's advisory runs in parallel and is reported, not gating.
Activity
HugoXK commentedon Nov 3, 2026
Release
Written-by: FlyBot Lead
Released: 8d8e26c4b51e482b88f43b3fefc95bee
Reason: merged as 9b8d731 (PR #2236)
At: 2026-11-03T00:21:52Z