diff --git a/link/PLAN.md b/link/PLAN.md index e1c6c65..e886427 100644 --- a/link/PLAN.md +++ b/link/PLAN.md @@ -308,6 +308,7 @@ Counts in `hello` are a live snapshot taken on the Core thread, not a cached val 3. ~~**Sweeps.**~~ **Done.** `BridgeSweeps`: vitals / decay-on-transition / economy, all Core-thread timers, re-armable. Evidence in §13. 4. ~~**Request/response.**~~ **Done.** `BridgeProfile` + `BridgeRequests`: `char.profile` (by account+slot or serial), `account.roster`, `vendor.snapshot`, `bridge.error`. Evidence in §14. Sidecar should cache profiles and rate-limit requests. 5. ~~**`[link` account linking.**~~ **Done.** `BridgeAccountLink`: `[link` → one-time code → `link.confirm` → `WebsiteUserId` tag, persisted to `accounts.xml`. `mob.login` carries `webId`. Evidence in §15. +6. ~~**Town-crier inbound.**~~ **Done.** `BridgeTownCrier`: `towncrier.add` / `remove` into `GlobalTownCrierEntryList`, with abuse caps. Evidence in §16. 5. **`[link` account linking.** `CommandSystem.Register("link", AccessLevel.Player, …)`, one-time short-TTL codes in a main-thread dict, `Account.SetTag("WebsiteUserId", id)` — persists to `accounts.xml` for free. Loopback-only is the trust boundary; add a shared secret if the sidecar is ever exposed. 6. **Town-crier inbound.** `GlobalTownCrierEntryList.Instance.AddEntry(lines, duration)` (`Scripts/Mobiles/NPCs/TownCrier.cs:96`), marshaled to the Core thread. Cap line count/length and active entries. 7. **Core edit: `PlayerVendorSale`** (§6). Then the cheat-detection feed. @@ -349,6 +350,27 @@ Two defects were found this way and fixed: --- +## 16. Phase 6 acceptance + +`BridgeTownCrier.cs` handles inbound `towncrier.add` / `towncrier.remove`, pushing website news into `GlobalTownCrierEntryList` on the Core thread. Caps (line count, line length, active-entry count, duration) are enforced before touching the shared list — defense in depth on top of the loopback trust boundary. + +Verified with a sending stub and a probe that logs the actual crier list. Replies and game state agree: + +| Sent | Reply | Crier list | +|------|-------|------------| +| `add n1` (2 lines) | `towncrier.ok` | entry appears with the exact lines | +| `add n2` (8 lines, cap 6) | `towncrier.error "too many lines"` | never enters the list | +| `remove n1` | `towncrier.ok` | entry gone | +| `remove does-not-exist` | `towncrier.error "unknown id"` | no change | + +The probe showed the list at 1 entry after the add and 0 after the remove, with the over-cap add never appearing — so the caps and the add/remove both take real effect, not just acknowledged. + +Harness note: the first run's PowerShell stub missed the replies because it checked `NetworkStream.DataAvailable`, which does not see lines already buffered inside `StreamReader`. Switching to a blocking `ReadLine` with a read timeout captured them. The shard behaved correctly in both runs; only the test reader was wrong. `tools/stub_sidecar_request.ps1` uses the same `DataAvailable` pattern and got lucky on timing — prefer the blocking-read pattern for new stubs. + +No core changes; this closes the pure-plugin inbound work. + +--- + ## 15. Phase 5 acceptance `BridgeAccountLink.cs` implements `[link` and the inbound `link.confirm`. A player runs `[link`; the shard mints a one-time, expiring code (5 min TTL, unambiguous alphabet — no O/0/I/1), holds it in a Core-thread dict keyed to the account, and emits `link.request`. The player enters the code on the website; the sidecar sends `link.confirm`; the shard validates, writes the `WebsiteUserId` account tag, and replies `link.ok`.