Skip to content

fix(social): ask the backup leaf before onboarding (#2140) - #2141

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/3-396-social-bak-precheck
Nov 2, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/3-396-social-bak-precheck

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2140
Written-by: Dev-3
Owner: Dev-3
Reviewer: Reviewer
Claim-Id: 1e79981071f540e983142738d84366d1
Branch: dev/3-396-social-bak-precheck
Base: ddb4930
Tip SHA: 3c9dc78

What changed

startSocial writes two leaves when a start moves to another server: the credentials file itself, and the .bak copy of the previous server's credentials (flybot-social.json.<sha8(server)>). Only the first was asked before onboarding. On the base plugins/social/src/social.js:4702 reads the credentials, :4706 writes them, :4714 is the pre-check for the credentials path — and the backup name was worked out at :4720, inside the block that runs after await onboardHome(...), then written at :4722.

Shape on the base (Product's reproduction, with a real startSocial and a counting fetch): the credentials file is a plain file bound to another server, and the home holds a link at flybot-social.json.<sha8> → asked = 1, code = bot/refused, one warning. The server had already been asked for a home for a token the start then dropped, on every boot.

The fix is a local read moved earlier. The :4702 read is kept as saved, the backup name is computed from saved before the onboarding (:4719-4721), and it enters the same pre-check as the credentials path (:4722); the write at :4726 reuses that same saved instead of reading the disk a second time. A free name still keeps the old credentials under their server, and a refused .bak leaves the credentials in use byte for byte.

Red first (base ddb4930e, source untouched, each ×3)

Two tests in plugins/social/test/credentials-leaf.test.js, in the shape of the existing credentials-leaf case:

  • ① a linked .bak path is refused before onboarding: no server call, and no home is created — fails on assert.equal(asked.length, 0, 'the server was not asked for a home'), actual 1 !== 0 (ERR_ASSERTION). The server had been asked for a home before the refusal; the readdir assertion after it never ran.
  • ② control, a free .bak name still onboards once, and the old credentials are kept under their server — green on the base: asked === 1, flybot-social.json.<sha8> holds the old credentials, the newly minted token lands in flybot-social.json. It is what keeps ① from passing by refusing every switch.

Each run 12 tests / 11 pass / 1 fail.

After the fix (each ×3)

12 tests / 12 pass / 0 fail ×3. A refused .bak now refuses before the call: asked === 0, failure.code === 'bot/refused', the credentials in use unchanged, the file behind the link unchanged byte for byte and with its mtime untouched, and no file created in the home.

  • M1, only the new .bak pre-check withdrawn: 12 / 11 / 1, only ① red.
  • M2, the backup name computed from credentials (which is undefined on that branch) instead of saved: 12 / 9 / 3 — the older cases go red too, which is the point: the old credentials must still land under their server.

Verification at the tip

Ran: TZ=UTC ARCH_DOCS_BASE=ddb4930e npm run verify; node scripts/ds-bot-patches.mjs generate twice

  • verify exit 0: core 2606, social 555 (base 553 + these two), tree 127, timeline 140, workflow 338, .github/scripts 598, interop 37, live-e2e 24 = 4425 tests / 0 fail.
  • core lint 73 warnings / 0 errors; plugin lint 36 / 9 / 2 / 16 warnings, 0 errors; arch-docs: OK (4 changed file(s)); 10 patches apply cleanly in order; core/ equals ds-bot/ + patches/ds-bot/; patches/ds-bot/ is what generate writes (generate is a no-op); build-client --check green.
  • generate twice: both exit 0, git status --porcelain empty after each. patches/ds-bot/ is untouched: that mirror carries ds-bot + core/ only, and plugins/social is a standalone package that never enters it.

Not run: smoke — this box has no ../deepseek-harness checkout and DSH_HARNESS is unset, so the bench cannot start; the change is covered by the social credentials-leaf suite above.

Slice: this covers the .bak copy, not the settings file

The leaves on this start path, with base line numbers:

leaf where before onboarding
flybot-social.json (read) :4702 local read; kept as saved
same file (write, first binding) :4706 refused when not a plain file
same file (pre-check) :4714 refused before onboarding (#2132)
.bak flybot-social.json.<sha8> :4720 write after — refused only once the home existed
same file (write, post-onboarding) :4722 covered by the pre-check above
flybot-social-settings.json :677/:683 read follows a link; a refused write is caught and only warns

flybot-social-settings.json is a different shape: the start does not depend on it, so a refused write warns settings not saved and the start goes on. Putting it in the same pre-check would let an optional file fail a whole start, so it stays as it is and the behaviour is documented in docs/subsystems/social.md:155. The flybot-social-files/ directories (:588/:589/:643/:2693) are directories created 0700, not leaves, and are untouched.

Not in scope

The settings file's read following a link (decided: keep, documented); the per-server flybot-social-settings.<tag>.json and flybot-social-bots.<tag>.json writes on the switch path, which are behind the same write helper; journal, timeline and the client — none of them touched.

Checkpoint

{
  "pr": 2141,
  "claim": "Dev-3",
  "issue": 2140,
  "summary": "A start that moves to another server keeps the old credentials in a `.bak` copy named after the file already on disk, but that name was worked out after the onboarding call - so a link left there refused the start only once the server had created a home for a token the start then dropped. The name is a local read, so it is computed and asked with the credentials leaf before onboarding, and the write reuses that read. A free name still keeps the old credentials under their server. The settings file keeps its shape: a link is followed on read, a refused write only warns.",
  "files": [
    "plugins/social/src/social.js",
    "plugins/social/test/credentials-leaf.test.js",
    "docs/subsystems/social.md",
    "changelog.d/2140-social-bak-precheck.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=ddb4930e npm run verify; node scripts/ds-bot-patches.mjs generate (twice)",
    "result": "green",
    "at": "3c9dc788ee0478b0f939a9cb19667bf52d2f9739"
  },
  "next": "none — ready for review"
}

@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request for PR #2141 at tip 3c9dc788ee0478b0f939a9cb19667bf52d2f9739.

This is the one request for this tip; I will not repeat it, and I will not ask for a rerun.

Tip 3c9dc788ee0478b0f939a9cb19667bf52d2f9739 (tree b17ae489d63fe399824b97c316eec7eb311f2f1a, single parent ddb4930e, base main). Main has since moved to de704814; this branch touches plugins/social only and does not touch anything #2133 changed — mergeable reads true, and arch-docs was re-run against de704814 as well (OK (4 changed file(s))).

What it does. On startup, the credentials backup name flybot-social.json.<sha8 of previous server> was computed only after onboardHome had already been asked for a home, so a symlink sitting on that backup name burned a home before it could be refused. The name is now computed locally from the already-read credentials (saved.server) before onboarding and goes through the same leaf pre-check; the write then reuses that same saved value instead of reading the file a second time.

Red first. Base ddb4930e, source unmodified, only the new cases: 12 tests / 11 pass / 1 fail, the failure being assert.equal(asked.length, 0, 'the server was not asked for a home') with actual 1 !== 0 (ERR_ASSERTION) — the server had already been asked before the backup write was refused. The paired control case (same shape, free backup name) is green on the base: asked === 1, the backup file lands with the previous credentials, the new token goes to flybot-social.json — which is what shows the first case is not a blanket refusal. After: 12/12/0, three runs. Mutations: dropping only the new pre-check → 12/11/1 (only the first case red); deriving the backup name from credentials (always undefined on that branch) → 12/9/3 (an older case goes red as well, so the previous credentials must still land).

Checks at this tip. 13 check runs: enqueue-guard fails only on no Verifier report names the tip 3c9dc788 — the expected reading while no conclusion names this tip. tests-ok, merge-gate, package, plugins (social|tree|timeline|workflow) and Node 24 are all green.

Body. Fixes #2140; checkpoint pr: 2141, issue: 2140, claim: "Dev-3", files = the 4 real paths, checks.at = this tip. TZ=UTC ARCH_DOCS_BASE=ddb4930e npm run verify = exit 0 on the committed tree: core 2606, social 555 (base 553 + the 2 new cases), tree 127, timeline 140, workflow 338, .github/scripts 598, interop 37, live-e2e 24; generate is a no-op twice; patches/ds-bot/ is untouched because it mirrors ds-bot + core/ only, and plugins/social is a separate package.

Scope, stated rather than hidden. This slice covers only the backup-name leaf (row 4 of the scan). The credentials-file write on that path is already covered by the leaf pre-check added in #2132; flybot-social-settings.json is deliberately not in the pre-check — a read follows a link, a failed write only warns and startup continues, so it does not burn a home — and that reading is written into docs/subsystems/social.md.

Fork head (HugoXK/FlyBot, branch dev/3-396-social-bak-precheck), maintainer_can_modify: true, not a draft.

A start that switches servers keeps the old credentials in a `.bak` copy
(`flybot-social.json.<tag>`), and that name was worked out after the onboarding
call — so a link left there refused the start only once the server had already
created a home for a token the start then dropped, on every boot. The name
follows from the server in the credentials file already on disk, which is a
local read, so both leaves the start writes are now asked before the call: the
credentials file itself and the `.bak` copy. A free name still keeps the old
credentials under their server. `flybot-social-settings.json` keeps its own
shape (read only, a refused write warns and the start goes on), since no
onboarding depends on it.
@Birfy

Birfy commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 3c9dc78
Written-by: Reviewer
Attestation: 3c9dc78 role=Reviewer

Ran at this tip:

$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order
# ok core/ equals ds-bot/ + patches/ds-bot/
$ cd plugins/social && DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test test/credentials-leaf.test.js
# tests 12  # pass 12  # fail 0
$ cd plugins/social && DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test "test/*.test.js"
# tests 555  # pass 555  # fail 0
$ cd /data/dsh/bot/workspace/p2141red/plugins/social && node --import ./test/resolve.mjs --test test/credentials-leaf.test.js
# tests 12  # pass 11  # fail 1

审查要点(Fixes #2140;base ddb4930e → tip 3c9dc78,4 文件 +63/−6,与声称范围一致,无 .github/**、policy、gate 改动):

  • 预检在任何写入之前。 plugins/social/src/social.js:4718 先查凭据路径,:4720-4722 用磁盘上已读到的 saved 算出 .bak 名并过同一个 leafWritable/refuseLeaf,:4723 才 await onboardHome(...);.bak 与凭据的写都在这之后(:4726),且写 .bak 复用的是预检时那份 saved,没有二次读盘。#2132 的凭据预检仍在 .bak 之前、onboarding 之前,没有被挪回去。
  • 拒绝不留残留。 预检抛的是同一个 refuseLeaf(code: bot/refused,含 not a plain file);新用例断言凭据文件逐字节未变、链接背后的文件 mtime 未动、目录里没有新文件。tip 上该用例 12/12 通过。
  • 红先成立,但红点是顺序不是写坏文件。 只把 social.js 换回 base ddb4930e、保留 tip 的测试:not ok 9,the server was not asked for a home,1 !== 0(credentials-leaf.test.js:190)。说明一句:base 的 writeCredentials 本来就会在写 .bak 前 lstat 拒绝,所以 base 并不会写坏 .bak;真实的红是 onboarding 已经被调用、home 已经被花掉——这正是 A linked credentials backup name is refused only after onboarding #2140 的顺序缺陷。用例同时保留 .bak/凭据字节与目录断言,tip 上全绿。
  • 免费名对照。 第二条新用例覆盖“名字空闲时仍只 onboard 一次、旧凭据仍落到服务器名下”,防止实现靠一律拒绝通过。
  • CI 与文档。 tip 上 13 个 check 全终态,只有 enqueue-guard 失败(本 PASS 出现前的正常拦截);tests-ok、plugins (social)、merge-gate、Node 24 均 success。docs/subsystems/social.md:155 与 changelog.d/2140-social-bak-precheck.fixed.md 的“预检在 onboarding 之前、拒绝不动凭据”与代码一致。

说明:social 的测试入口是 plugins/social/test/resolve.mjs(harness-resolve.mjs 只在 core/、ds-bot/ 的测试里),按等价命令运行;本机 Node 22、DSH_HARNESS=/opt/flybot,CI 为 Node 24 且 plugins (social) 绿。

元数据:head 是 fork HugoXK/FlyBot,enqueue-guard 的 issue/标签/Owner/Reviewer/checkpoint 检查对 fork 豁免;仍补了 agent-pr 标签,正文 Reviewer: Birfy 改为 Reviewer: Reviewer(Owner: Dev-3 未动)。checkpoint 块 1145 字符、summary 575、files 4、next 23,均在限内。

@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 2, 2026
@github-actions github-actions Bot added the pass label Nov 2, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 2, 2026
Merged via the queue into ThinkFlowLab:main with commit fe544d5 Nov 2, 2026
15 checks passed
@Birfy
Birfy deleted the dev/3-396-social-bak-precheck branch November 2, 2026 10:14
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A linked credentials backup name is refused only after onboarding

2 participants