From 7c7a1ad45d11b78c2ad406141cffb2890b596d83 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 18 Oct 2026 12:45:31 +0200 Subject: [PATCH] docs(19.1): create phase plan --- .planning/ROADMAP.md | 14 +- .../19.1-chat-satellites/19.1-01-PLAN.md | 312 ++++++++++++++++++ .../19.1-chat-satellites/19.1-02-PLAN.md | 272 +++++++++++++++ .../19.1-chat-satellites/19.1-03-PLAN.md | 226 +++++++++++++ .../19.1-chat-satellites/19.1-04-PLAN.md | 258 +++++++++++++++ .../19.1-chat-satellites/19.1-05-PLAN.md | 284 ++++++++++++++++ .../19.1-chat-satellites/19.1-06-PLAN.md | 239 ++++++++++++++ .../phases/19.1-chat-satellites/COVERAGE.md | 72 ++++ 8 files changed, 1675 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/19.1-chat-satellites/19.1-01-PLAN.md create mode 100644 .planning/phases/19.1-chat-satellites/19.1-02-PLAN.md create mode 100644 .planning/phases/19.1-chat-satellites/19.1-03-PLAN.md create mode 100644 .planning/phases/19.1-chat-satellites/19.1-04-PLAN.md create mode 100644 .planning/phases/19.1-chat-satellites/19.1-05-PLAN.md create mode 100644 .planning/phases/19.1-chat-satellites/19.1-06-PLAN.md create mode 100644 .planning/phases/19.1-chat-satellites/COVERAGE.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 5d68fd6b..ccf4aed3 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1235,10 +1235,20 @@ Plans: 3. Userfriends is checked against fonoteka first, so there is one shared copy. 4. The new code has unit tests, delivered in the phase's last plan. -**Plans:** 0 plans +**Plans:** 6 plans Plans: -- [ ] TBD (run $gsd-plan-phase 19.1 to break down) +**Wave 1** +- [ ] 19.1-01-PLAN.md — userfriends repo, baseline, friends HTTP tracer, group events, soft chat adapter, GDPR +- [ ] 19.1-03-PLAN.md — chatbots repo, bot_flags, slash-command dispatcher, /ping tracer (no AI-plugin import) +- [ ] 19.1-04-PLAN.md — webpush-go note, then herald subscriptions, PublicOnly send, rollback-sends-0 tracer + +**Wave 2** *(D-17 gate for forum; chatvideo after chat)* +- [ ] 19.1-02-PLAN.md — forum repo after D-17 Render/Purify, DeleteContext, GFM/XSS HTTP tracer +- [ ] 19.1-05-PLAN.md — LiveKit v2 note, then chatvideo sessions, authorizer, generic reconciler + +**Wave 3** *(blocked on Waves 1–2)* +- [ ] 19.1-06-PLAN.md — PHP method maps, goldens, scripts/check-phase19.1.sh ### Phase 19.2: Chat polish against WavePath clients (INSERTED) diff --git a/.planning/phases/19.1-chat-satellites/19.1-01-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-01-PLAN.md new file mode 100644 index 00000000..8c211393 --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-01-PLAN.md @@ -0,0 +1,312 @@ +--- +phase: 19.1-chat-satellites +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .planning/ROADMAP.md + - ../sm-userfriends-plugin/.gitignore + - ../sm-userfriends-plugin/go.mod + - ../sm-userfriends-plugin/go.sum + - ../sm-userfriends-plugin/go.work + - ../sm-userfriends-plugin/README.md + - ../sm-userfriends-plugin/plugin.go + - ../sm-userfriends-plugin/boot.go + - ../sm-userfriends-plugin/routes.go + - ../sm-userfriends-plugin/config/config.yaml + - ../sm-userfriends-plugin/lang/en/lang.yaml + - ../sm-userfriends-plugin/lang/pl/lang.yaml + - ../sm-userfriends-plugin/models/context.go + - ../sm-userfriends-plugin/models/friendship.go + - ../sm-userfriends-plugin/models/group.go + - ../sm-userfriends-plugin/models/group_member.go + - ../sm-userfriends-plugin/models/group_invite.go + - ../sm-userfriends-plugin/models/group_ban.go + - ../sm-userfriends-plugin/models/registry.go + - ../sm-userfriends-plugin/events/events.go + - ../sm-userfriends-plugin/classes/friendship_service.go + - ../sm-userfriends-plugin/classes/group_service.go + - ../sm-userfriends-plugin/classes/group_policy.go + - ../sm-userfriends-plugin/classes/group_visibility_gate.go + - ../sm-userfriends-plugin/classes/friends_context_adapter.go + - ../sm-userfriends-plugin/api/auth.go + - ../sm-userfriends-plugin/api/errors.go + - ../sm-userfriends-plugin/api/json.go + - ../sm-userfriends-plugin/api/friends.go + - ../sm-userfriends-plugin/api/groups.go + - ../sm-userfriends-plugin/updates/registry.go + - ../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go + - ../sm-userfriends-plugin/updates/baseline_test.go + - ../sm-userfriends-plugin/testmain_test.go + - ../sm-userfriends-plugin/harness_test.go + - ../sm-userfriends-plugin/fixture_plugin_test.go + - ../sm-userfriends-plugin/http_tracer_test.go +autonomous: false +requirements: [SC-19.1-1, SC-19.1-3, D-01, D-02, D-03, D-04, D-05, D-07, D-08] +estimate: + tokens: 200000 + raw_tokens: 200000 + tasks: 3 + confidence: low +must_haves: + truths: + - "D-01: module git.golem15.com/golem15/sm-userfriends-plugin, package userfriends, plugin ID golem15.userfriends, Requires golem15.user only; frozen PHP userfriends 182b514." + - "D-02 / D-04: one gormigrate ID 202610180001_create_golem15_userfriends_baseline uses CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS for golem15_userfriends_contexts, friendships, groups, group_members, group_invites, group_bans; demo-seed v1.0.1 ID is a comment and inserts no demo rows." + - "D-05: prefix /_userfriends/api/v1; JWT A request to B, accept, GET /friends returns 200; POST /friends/request uses bucket community-join at 10/min; actor id from the token." + - "D-08: events.GroupCreatedEventName userfriends.GroupCreated (and GroupDeleted, MemberJoined, MemberLeft); soft chat adapter via Lookup of chat Registry; Requires() never lists golem15.chat." + - "D-03: Boot Listens user.Deleting; heir is oldest admin else oldest member else hard-delete empty group; then member and ban rows go; order by role, not MySQL FIELD()." + - "SC-19.1-3: ROADMAP Phase 19.1 Repos names summercms.go; criterion 2 names webpush-go / LiveKit / bluemonday notes and no longer wires chatbots to the AI plugin; criterion 3 is marked already met." + artifacts: + - path: "../sm-userfriends-plugin/plugin.go" + provides: "golem15.userfriends plugin: Requires user only, config, lang, migrations, models, routes, buckets" + contains: "golem15.userfriends" + - path: "../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go" + provides: "idempotent userfriends v1.1.0 baseline" + contains: "IF NOT EXISTS" + - path: "../sm-userfriends-plugin/events/events.go" + provides: "group lifecycle festival events" + contains: "userfriends.GroupCreated" + - path: "../sm-userfriends-plugin/http_tracer_test.go" + provides: "JWT request/accept/list tracer and community-join bucket" + contains: "TestHTTPTracer" + - path: ".planning/ROADMAP.md" + provides: "Phase 19.1 Repos + SC2 + SC3 wording" + contains: "webpush-go" + key_links: + - from: "../sm-userfriends-plugin/boot.go" + to: "../sm-userfriends-plugin/events/events.go" + via: "Fire GroupCreated inside the write tx; Listen user.Deleting" + pattern: "GroupCreated" + - from: "../sm-userfriends-plugin/routes.go" + to: "../sm-userfriends-plugin/api/friends.go" + via: "jwt.auth plus throttle community-join on POST /friends/request" + pattern: "community-join" + - from: "../sm-userfriends-plugin/classes/friends_context_adapter.go" + to: "../sm-chat-plugin/contracts/registry.go" + via: "optional Lookup Registry; Add alias friendcontext" + pattern: "friendcontext" + prohibitions: + - statement: "Production sources, comments, lang and README never contain QuestStream, family, parent, child, or consuming-app names; plugin.name is Friends / Przyjaciele." + status: unresolved + verification: grep + - statement: "Requires() and go.mod never require sm-chat-plugin; sm-user-plugin is not edited." + status: unresolved + verification: grep + - statement: "Legacy /api/friends routes and Winter FriendsList / FriendRequests components are not registered (D-07)." + status: unresolved + verification: judgment +--- + +## Phase Goal + +**As a** application that mounts the satellites, **I want to** have Friends on SummerCMS with a generic `/_userfriends/api/v1`, **so that** a host can bind seams and call the published JSON API without PHP. + +This plan's slice (plan 1 of 6, wave 1): `golem15.userfriends` in a new repository, proven by JWT user A requesting B, accepting, and listing friends. + +## Spec-less probe fallback + +No SPEC.md and no REQUIREMENTS IDs exist for this phase: the orchestrator recorded "spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)". No probe predicates were generated. `must_haves` derive from ROADMAP success criteria SC-19.1-1..SC-19.1-4 and locked CONTEXT D-01..D-18. + +## Schema-gate recorded choice + +Schema-gate JS ORM push does not apply. This phase uses per-plugin gormigrate `CREATE TABLE IF NOT EXISTS` baselines (D-02). No prisma / drizzle / supabase / typeorm push task is added. + +## Decisions taken in this plan (Claude's discretion) + +- Package `userfriends`, PluginID `golem15.userfriends`, Winter directory layout. Friendship writes live in `classes.FriendshipService`; groups in `classes.GroupService`. +- Festival names `userfriends.GroupCreated`, `userfriends.GroupDeleted`, `userfriends.MemberJoined`, `userfriends.MemberLeft` (one constant per line). Payload fields GroupID, ActorID, MemberID, Tx. +- Soft chat adapter alias `friendcontext`, LegacyPHPClass `Golem15\UserFriends\Models\FriendContext`; optional group alias `friendgroup` / `Golem15\UserFriends\Models\FriendGroup` if the adapter registers groups. +- APIPrefix `/_userfriends/api/v1`, APIBucket `userfriends-api` 120/min, JoinBucket `community-join` 10/min on friend request and group join. +- Baseline id `202610180001_create_golem15_userfriends_baseline`. +- First execute commit patches ROADMAP Phase 19.1 Repos + criterion 2 + criterion 3 (CONTEXT roadmap corrections). + +## Artifacts this phase produces (this plan) + +- New local repo `../sm-userfriends-plugin` (module `git.golem15.com/golem15/sm-userfriends-plugin`). +- Types: `userfriends.Plugin`, `classes.FriendshipService`, `classes.GroupService`, `classes.GroupPolicy`, `classes.GroupVisibilityGate`, `events.GroupEvent`, models Context / Friendship / Group / GroupMember / GroupInvite / GroupBan. +- Funcs: `FriendshipService.Request`, `Accept`, `List`; `GroupService.Create`; `Delete` cascade on user.Deleting; `Plugin.Routes`, `Plugin.Buckets`. +- Config: none beyond plugin.name. Lang EN/PL plugin.name Friends / Przyjaciele. +- Routes: `GET /_userfriends/api/v1/friends`, `GET .../friends/requests`, `POST .../friends/request`, `POST .../friends/requests/{id}/accept`, plus expansion reject/cancel/unfriend and `/groups*`. +- Commands: none. + + +Create sm-userfriends-plugin with the frozen v1.1.0 schema (D-01, D-02), friendship and group services that fire festival group events (D-08), a soft chat context adapter (D-08), the GDPR listener (D-03), and the generic friends API tracer (D-05). + +Purpose: later hosts mount `/_userfriends/api/v1` without rewriting controllers; the tracer proves request/accept/list before the remaining group matrix. +Output: a compiling sibling module whose TestHTTPTracer sends a friend request as JWT A, accepts as JWT B, and lists 200. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md +@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md +@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md + +Concurrency: other sessions may edit summercms.go or STATE.md. Do not stage those. Do not edit modules/lighthouse, fetchguard, festival, conga, or ../fonoteka.go/plugins/golem15/user. Stage by explicit path. No co-author tags. Do not push unless the Task 1 reply says so. + + +Analog (copy shape, change names): +- ../sm-chat-plugin/plugin.go (PluginID, embed config/lang, Requires, init party.Register) +- ../sm-chat-plugin/boot.go (registerBootStep / runBootSteps, Listen user.Deleting) +- ../sm-chat-plugin/go.mod (sibling replace ../summercms.go) +- ../sm-chat-plugin/go.work (user replace ../fonoteka.go/plugins/golem15/user) +- ../sm-chat-plugin/routes.go (APIPrefix, Buckets, jwt.auth) +- ../sm-chat-plugin/api/auth.go (frontendUser; actor from token) +- ../sm-chat-plugin/api/errors.go (IDOR collapses to 404) +- ../sm-chat-plugin/http_tracer_test.go (bootWeb, frontendToken, call) +- ../sm-chat-plugin/events/events.go (one EventName per line, Tx field) +- ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go (IF NOT EXISTS) +- ../sm-chat-plugin/testmain_test.go (flag.Parse + testing.Short) +- ../sm-chatchannels-plugin/plugin.go (HasRoutes, no hard extra require beyond PHP) +- Frozen PHP at /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends @ 182b514 +- Host shapes at /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityFriendsController.php and CommunityGroupController.php (payloads only; do not port /_wave) + + + + + + + Task 1: Confirm D-01 names, D-02 baseline id and D-05 prefix before the new repo exists + .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md (D-01, D-02, D-05, D-08), .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (userfriends schema, Pitfall 11), .planning/notes/core-plugins-own-repos.md + Print a decision sheet, then stop. Do not create the directory, do not run tea, do not push. The sheet lists the locked identifiers this plan will write: module git.golem15.com/golem15/sm-userfriends-plugin, package userfriends, PluginID golem15.userfriends, tables golem15_userfriends_contexts / friendships / groups / group_members / group_invites / group_bans, migration id 202610180001_create_golem15_userfriends_baseline, import-compatible Postgres IF NOT EXISTS, published prefix /_userfriends/api/v1, community-join 10 per minute, Requires golem15.user only. Also list tea repos create --owner golem15 --name sm-userfriends-plugin (check tea repos create --help for flags) and that hold keeps work local. Confirm ROADMAP Phase 19.1 Repos still omits summercms.go and criterion 2 still names the AI plugin; Task 2 patches those two lines plus marks criterion 3 already met. + Lock D-01 plugin names, D-02 baseline id 202610180001_create_golem15_userfriends_baseline, and D-05 prefix /_userfriends/api/v1, then implement locally and optionally create the remote? + These names land in every adopting app's database and in go.mod require paths. Changing the migration id, table names, or published prefix after an app migrates needs a new migration and a client change. A pushed main is visible to every developer; no application mounts this plugin yet. + + + + + + Reply proceed-and-push, proceed-local, or abort. + + test -f .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md && grep -q "D-04" .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md && echo decision-sheet-ready + non-zero exit or "decision-sheet-ready" absent + + + - SUMMARY records the reply verbatim. + - After abort, ../sm-userfriends-plugin does not exist. + + The human confirmed the locked names and the remote choice before any repo file was written. + + + + Task 2: End-to-end friend request — repo, baseline, FriendshipService, JWT A requests B, accept, list 200 + D-02 migration id and table shapes and D-05 published /_userfriends/api/v1 become the shared contract; undoing them needs a new migration and a client change. + Task 1 reply is proceed-and-push or proceed-local; ../sm-userfriends-plugin does not already exist as a foreign module. + ../sm-userfriends-plugin/go.mod, ../sm-userfriends-plugin/go.work, ../sm-userfriends-plugin/go.sum, ../sm-userfriends-plugin/.gitignore, ../sm-userfriends-plugin/README.md, ../sm-userfriends-plugin/plugin.go, ../sm-userfriends-plugin/boot.go, ../sm-userfriends-plugin/routes.go, ../sm-userfriends-plugin/config/config.yaml, ../sm-userfriends-plugin/lang/en/lang.yaml, ../sm-userfriends-plugin/lang/pl/lang.yaml, ../sm-userfriends-plugin/models/context.go, ../sm-userfriends-plugin/models/friendship.go, ../sm-userfriends-plugin/models/registry.go, ../sm-userfriends-plugin/classes/friendship_service.go, ../sm-userfriends-plugin/api/auth.go, ../sm-userfriends-plugin/api/errors.go, ../sm-userfriends-plugin/api/json.go, ../sm-userfriends-plugin/api/friends.go, ../sm-userfriends-plugin/updates/registry.go, ../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go, ../sm-userfriends-plugin/updates/baseline_test.go, ../sm-userfriends-plugin/testmain_test.go, ../sm-userfriends-plugin/harness_test.go, ../sm-userfriends-plugin/fixture_plugin_test.go, ../sm-userfriends-plugin/http_tracer_test.go, .planning/ROADMAP.md + ../sm-chat-plugin/plugin.go, ../sm-chat-plugin/go.mod, ../sm-chat-plugin/go.work, ../sm-chat-plugin/routes.go, ../sm-chat-plugin/api/auth.go, ../sm-chat-plugin/api/errors.go, ../sm-chat-plugin/http_tracer_test.go, ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go, ../sm-chat-plugin/testmain_test.go, ../sm-chat-plugin/harness_test.go, ../sm-chat-plugin/fixture_plugin_test.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/updates/, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/models/Friendship.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/controllers/api/FriendsApiController.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityFriendsController.php, .planning/ROADMAP.md + Per D-01 and D-02, create ../sm-userfriends-plugin as a sibling of summercms.go. Copy the analog go.mod pin set (Go 1.27.0, gorm v1.31.2, gormigrate v2.1.7, pgx v5.10.0, testcontainers v0.44.0, summercms + sm-user-plugin require). replace summercms => ../summercms.go. go.work also replaces sm-user-plugin => ../fonoteka.go/plugins/golem15/user. Do not copy a deep relative replace. The production module list matches analog chat except it does not pull the chat module. No new third-party library. + +plugin.go: package userfriends, PluginID golem15.userfriends, Requires golem15.user only, embed config and lang, assert party.Plugin plus pact.HasConfig, HasMigrations, HasModels, HasLang, HasRoutes, surf.BucketProvider. README H1 Friends, one-sentence summary, import line, **Name:** mnemonic, standard sections; names only acme / blog; plugin display name Friends / Przyjaciele. + +In the same first execute commit, patch .planning/ROADMAP.md Phase 19.1 only: Repos add summercms.go for the Markdown module; criterion 2 drop the clause that wires chatbots to the AI plugin and name webpush-go / LiveKit / bluemonday notes; criterion 3 mark already met (fonoteka PHP tree equals Wavepath 182b514). Do not rewrite other phases. + +Baseline 202610180001_create_golem15_userfriends_baseline (D-02): CREATE TABLE IF NOT EXISTS for the six PHP v1.1.0 tables and their indexes from RESEARCH Frozen inventory. Types TIMESTAMPTZ. Status columns varchar + CHECK matching pending|accepted|declined and owner|admin|member and banned. Insert the FriendContext singleton id=1. Comment the demo-seed v1.0.1 version id; do not INSERT demo users (D-07). updates/registry.go copies analog Register / All / run. updates/baseline_test.go greps the six table names under -short. + +Models (leaf): Friendship.TableName golem15_userfriends_friendships; Context.TableName golem15_userfriends_contexts; models/registry.go Register / All. No festival imports in models/. + +classes.FriendshipService: Options{DB, Bus}, New refuses nil DB. This tracer implements Request(actorID, recipientID), Accept(actorID, friendshipID), List(actorID) matching CommunityFriendsController sendRequest / accept / index. Persist inside lagoon.Transaction. Sentinel errors prefixed golem15.userfriends:. Actor id is the argument, never a body field (D-05). + +Routes (D-05): APIPrefix /_userfriends/api/v1. Group jwt.auth + throttle:userfriends-api for GET /friends, GET /friends/requests, POST /friends/requests/{id}/accept. Separate group jwt.auth + throttle:community-join for POST /friends/request only. Buckets: userfriends-api Max 120 Decay time.Minute; community-join Max 10 Decay time.Minute; key frontend user id else IP, analog chat Buckets. api/auth.go copies frontendUser; missing JWT 401 Authentication required. api/errors.go: missing / unauthorized collapse to 404 Not found. + +Test host (D-04 smoke): testmain_test.go flag.Parse then testing.Short; harness boots golem15.user + golem15.userfriends with SUMMER_REALTIME__DRIVER=memory; fixturePluginID acme.teams; TestFixtureAbsentFromProduction greps production sources for the fixture id and the retired tokens. http_tracer_test.go TestHTTPTracer (Postgres, skip under -short): two frontend users, A POST /friends/request {user_id: B}, B POST accept, A GET /friends 200 with B. Also assert community-join Max is 10. TestBaseline runs under -short. + +Task 3 adds groups, festival events, the soft chat adapter, GDPR, and the remaining friend routes. Do not register legacy /api/friends paths or Winter components (D-07). + + go -C ../sm-userfriends-plugin vet ./... && go -C ../sm-userfriends-plugin test -count=1 -short ./... + non-zero exit, or "ok" lines absent, or a package reports FAIL + docker info >/dev/null 2>&1 && go -C ../sm-userfriends-plugin test -count=1 -run '^TestHTTPTracer$' -v . + non-zero exit: no Docker daemon (record NOT RUN in the SUMMARY), or TestHTTPTracer is missing, fails, or accept/list is not 200 + + + - grep -v '^#' ../sm-userfriends-plugin/plugin.go | grep -c 'golem15.userfriends' prints at least 1. + - grep -v '^#' ../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go | grep -c 'golem15_userfriends_friendships' prints at least 1. + - grep -n 'community-join' ../sm-userfriends-plugin/routes.go prints at least one line. + - grep -n 'Max: 10' ../sm-userfriends-plugin/routes.go prints at least one line. + - grep -n 'func (p \*Plugin) Requires' -A2 ../sm-userfriends-plugin/plugin.go | grep -c 'golem15.chat' prints 0. + - grep -n 'webpush-go' .planning/ROADMAP.md prints at least one line inside the Phase 19.1 section. + + JWT A can request B, B can accept, A can list friends 200; the baseline and ROADMAP corrections are committed. + + + + Task 3: Expand groups, festival events, soft chat adapter, GDPR heir, remaining friend routes + D-01 table and event names are copied into every adopting app; D-08 group events become the host wiring contract. + ../sm-userfriends-plugin/models/group.go, ../sm-userfriends-plugin/models/group_member.go, ../sm-userfriends-plugin/models/group_invite.go, ../sm-userfriends-plugin/models/group_ban.go, ../sm-userfriends-plugin/events/events.go, ../sm-userfriends-plugin/classes/group_service.go, ../sm-userfriends-plugin/classes/group_policy.go, ../sm-userfriends-plugin/classes/group_visibility_gate.go, ../sm-userfriends-plugin/classes/friends_context_adapter.go, ../sm-userfriends-plugin/api/friends.go, ../sm-userfriends-plugin/api/groups.go, ../sm-userfriends-plugin/routes.go, ../sm-userfriends-plugin/boot.go, ../sm-userfriends-plugin/plugin.go, ../sm-userfriends-plugin/README.md + ../sm-userfriends-plugin/classes/friendship_service.go, ../sm-chat-plugin/events/events.go, ../sm-chat-plugin/boot.go, ../sm-chat-plugin/contracts/registry.go, ../sm-chatchannels-plugin/classes/channel_service.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/GroupPolicy.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/GroupVisibilityGate.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/services/GroupService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/services/GroupMembershipService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/FriendsContextAdapter.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityFriendsController.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityGroupController.php + Per D-08, add events with GroupCreatedEventName userfriends.GroupCreated, GroupDeletedEventName userfriends.GroupDeleted, MemberJoinedEventName userfriends.MemberJoined, MemberLeftEventName userfriends.MemberLeft (one constant per line). GroupEvent fields GroupID, ActorID, MemberID, Tx. Fire inside lagoon.Transaction after the row is saved. + +Port GroupPolicy and GroupVisibilityGate 1:1 from PHP (matrix tests land in plan 06). GroupService.Create / Delete / Update and membership join / leave / kick / ban / unban / promote / demote / transfer fire the matching event. community-join 10/min also covers POST /groups/{id}/join when that route is mounted. + +Remaining friend routes matching CommunityFriendsController: GET /friends/sent, DELETE /friends/requests/{id} reject, DELETE /friends/sent/{id} cancel, DELETE /friends/{userId} unfriend. Group routes under /_userfriends/api/v1/groups matching D-05 (groups, members, invites, bans). Do not add webhook, emoji, chat-message, or image-upload host routes. + +Soft chat adapter (D-08): in Boot, Lookup the chat Registry type; if the lookup fails, skip. If it succeeds, Add PluginID with Registration Alias friendcontext and LegacyPHPClass the PHP FriendContext class string. Requires and go.mod stay limited to the user plugin; chat is optional at Boot only. + +GDPR (D-03): app.Events.Listen the user Deleting event; WithDB(ev.Tx); heir oldest admin else oldest member else hard-delete the empty group; then delete that user's member and ban rows. Order by role, not a MySQL FIELD() expression. + +If Task 1 replied proceed-and-push: after verify is green, tea repos create --owner golem15 --name sm-userfriends-plugin, add origin, git push -u origin main (never force). If proceed-local: no remote. Record the reply outcome in the SUMMARY. Do not edit the user plugin. + + go -C ../sm-userfriends-plugin vet ./... && go -C ../sm-userfriends-plugin test -count=1 -short ./... + non-zero exit or a package reports FAIL + + + - grep -n 'userfriends.GroupCreated' ../sm-userfriends-plugin/events/events.go prints one line. + - grep -n 'userfriends.MemberLeft' ../sm-userfriends-plugin/events/events.go prints one line. + - grep -n 'friendcontext' ../sm-userfriends-plugin/boot.go ../sm-userfriends-plugin/classes/friends_context_adapter.go prints at least one line. + - grep -n 'DeletingEvent' ../sm-userfriends-plugin/boot.go prints at least one line. + - grep -n 'func (p \*Plugin) Requires' -A3 ../sm-userfriends-plugin/plugin.go | grep -c 'golem15.chat' prints 0. + + Groups fire festival events, chat is optional, and deleting a user hands off or removes their groups. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| JWT actor → /_userfriends/api/v1 | untrusted body and path ids cross here | +| FriendshipService write → Postgres | actor ids become friendship and group rows | +| user.Deleting → GDPR cascade | a deleted user must not leave owned groups orphaned | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.1-01 | Information disclosure | api/friends.go | high | mitigate | Actor from token; stranger accept/list/unfriend collapse to 404; community-join 10/min on request | +| T-19.1-08 | Information disclosure | boot.go onUserDeleting | medium | mitigate | Heir or hard-delete then remove member and ban rows on user.Deleting | +| T-19.1-SC | Tampering | go.mod requires | high | mitigate | Analog pins only; no new registry packages | + + + + +go -C ../sm-userfriends-plugin vet ./... and go -C ../sm-userfriends-plugin test -count=1 -short ./... stay green. TestHTTPTracer on Docker proves request/accept/list. ROADMAP Phase 19.1 Repos, criterion 2 and criterion 3 match CONTEXT. + + + +sm-userfriends-plugin exists with the v1.1.0 baseline, a working friends HTTP tracer, group festival events, a soft chat adapter, and the GDPR listener. summercms.go modules are untouched aside from the ROADMAP wording patch. + + + +Create `.planning/phases/19.1-chat-satellites/19.1-01-SUMMARY.md` when done + diff --git a/.planning/phases/19.1-chat-satellites/19.1-02-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-02-PLAN.md new file mode 100644 index 00000000..62da8234 --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-02-PLAN.md @@ -0,0 +1,272 @@ +--- +phase: 19.1-chat-satellites +plan: 02 +type: execute +wave: 2 +depends_on: [] +files_modified: + - .planning/notes/2026-10-18-bluemonday-forum-policy.md + - ../sm-forum-plugin/.gitignore + - ../sm-forum-plugin/go.mod + - ../sm-forum-plugin/go.sum + - ../sm-forum-plugin/go.work + - ../sm-forum-plugin/README.md + - ../sm-forum-plugin/plugin.go + - ../sm-forum-plugin/boot.go + - ../sm-forum-plugin/routes.go + - ../sm-forum-plugin/config/config.yaml + - ../sm-forum-plugin/lang/en/lang.yaml + - ../sm-forum-plugin/lang/pl/lang.yaml + - ../sm-forum-plugin/contracts/access_resolver.go + - ../sm-forum-plugin/classes/deny_all.go + - ../sm-forum-plugin/classes/forum_service.go + - ../sm-forum-plugin/models/category.go + - ../sm-forum-plugin/models/thread.go + - ../sm-forum-plugin/models/post.go + - ../sm-forum-plugin/models/thread_read.go + - ../sm-forum-plugin/models/registry.go + - ../sm-forum-plugin/api/auth.go + - ../sm-forum-plugin/api/errors.go + - ../sm-forum-plugin/api/json.go + - ../sm-forum-plugin/api/categories.go + - ../sm-forum-plugin/api/threads.go + - ../sm-forum-plugin/api/posts.go + - ../sm-forum-plugin/console/rewrite_context_types.go + - ../sm-forum-plugin/updates/registry.go + - ../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go + - ../sm-forum-plugin/updates/baseline_test.go + - ../sm-forum-plugin/testmain_test.go + - ../sm-forum-plugin/harness_test.go + - ../sm-forum-plugin/fixture_plugin_test.go + - ../sm-forum-plugin/http_tracer_test.go +autonomous: true +requirements: [SC-19.1-1, D-01, D-02, D-03, D-04, D-05, D-08, D-16, D-17, D-18] +estimate: + tokens: 200000 + raw_tokens: 200000 + tasks: 3 + confidence: low +must_haves: + truths: + - "D-17: forum execution starts only after a summercms.go module exports Render(markdown) and Purify(html); sm-forum-plugin go.mod never requires bluemonday or goldmark." + - "D-16: the bluemonday forum-policy note exists (this plan writes it unless D-17 already did) and names v1.0.27, AllowElements of the PHP list, schemes http/https/mailto, RequireNoFollowOnLinks true, AddTargetBlankToFullyQualifiedLinks true, and is not the richeditor UGC policy." + - "D-02: baseline 202610180001_create_golem15_forum_baseline creates golem15_forum_categories (context_type NOT NULL, no FK), threads, posts, thread_reads with IF NOT EXISTS." + - "D-05 / D-18: /_forum/api/v1/contexts/{type}/{id}/categories tracer creates category+thread+post; content_html has a table for GFM and no script element; adopted content_html is served stored." + - "D-03 / D-08: ForumAccessResolver default DenyAll with Boot Warn and SetAccessResolver; DeleteContext(type, id) soft-deletes the morph pair; forum never imports userfriends; forum:rewrite-context-types owns golem15_forum_categories.context_type." + artifacts: + - path: "../sm-forum-plugin/plugin.go" + provides: "golem15.forum plugin, Requires golem15.user" + contains: "golem15.forum" + - path: "../sm-forum-plugin/classes/forum_service.go" + provides: "ForumService including DeleteContext" + contains: "DeleteContext" + - path: "../sm-forum-plugin/contracts/access_resolver.go" + provides: "ForumAccessResolver seam" + contains: "CanRead" + - path: "../sm-forum-plugin/console/rewrite_context_types.go" + provides: "forum:rewrite-context-types" + contains: "forum:rewrite-context-types" + - path: ".planning/notes/2026-10-18-bluemonday-forum-policy.md" + provides: "D-16 named forum policy note (skipped if D-17 already wrote one)" + contains: "bluemonday" + key_links: + - from: "../sm-forum-plugin/classes/forum_service.go" + to: "D-17 Render/Purify" + via: "write-path render of new and edited posts" + pattern: "Render\\(" + - from: "../sm-forum-plugin/boot.go" + to: "../sm-forum-plugin/classes/deny_all.go" + via: "Boot Warn while DenyAll is the resolver" + pattern: "deny-all" + - from: "../sm-forum-plugin/console/rewrite_context_types.go" + to: "../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go" + via: "rewrites golem15_forum_categories.context_type LegacyPHPClass to alias" + pattern: "context_type" + prohibitions: + - statement: "forum does not vendor goldmark or bluemonday and does not import the richeditor UGC policy or cabana.RenderMarkdown." + status: unresolved + verification: grep + - statement: "forum go.mod never requires sm-userfriends-plugin." + status: unresolved + verification: grep + - statement: "Adopted content_html is never re-rendered on read (D-18)." + status: unresolved + verification: judgment +--- + +## Phase Goal + +**As a** application that mounts the satellites, **I want to** have Forum on SummerCMS with a generic `/_forum/api/v1`, **so that** a host can bind ForumAccessResolver and call context-scoped categories without PHP. + +This plan's slice (plan 2 of 6, wave 2): `golem15.forum` after D-17 `Render` / `Purify` exists. + +## Decisions taken in this plan (Claude's discretion) + +- Package `forum`, PluginID `golem15.forum`, Requires `golem15.user` only. +- ForumAccessResolver methods CanRead / CanPost / CanManage / CanManageContext matching PHP DenyAllForumAccessResolver. +- DeleteContext(ctx, type, id) is a new Go API (Pitfall 12). Morph rewrite is forum-owned `forum:rewrite-context-types`. +- APIPrefix `/_forum/api/v1`, APIBucket `forum-api` 120/min. +- Baseline id `202610180001_create_golem15_forum_baseline`. +- Bluemonday note path `.planning/notes/2026-10-18-bluemonday-forum-policy.md` unless D-17 already wrote a named-policy note. + +## Artifacts this phase produces (this plan) + +- `../sm-forum-plugin` module `git.golem15.com/golem15/sm-forum-plugin`. +- Types: `forum.Plugin`, `contracts.AccessResolver`, `classes.DenyAll`, `classes.ForumService`. +- Funcs: `ForumService.CreateCategory`, `CreateThread`, `Reply`, `MarkThreadRead`, `UnreadThreadCount`, `DeleteContext`, `RewriteContextTypes`. +- Command: `summer forum:rewrite-context-types`. +- Routes under `/_forum/api/v1/contexts/{type}/{id}/...`. + + +Write the D-16 forum-policy note if missing, then create sm-forum-plugin that consumes D-17 Render/Purify (D-16, D-17, D-18), ships the v1.1.0 baseline (D-02), DenyAll ForumAccessResolver (D-03), DeleteContext (D-08), and the generic forum API tracer (D-05). + +Purpose: hosts bind the resolver; forum never imports userfriends. +Output: TestHTTPTracer creates a GFM table post whose stored content_html has a table and no script element. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md +@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md +@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md +@.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md +@.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md + + +Analog: +- ../sm-chatchannels-plugin/contracts/access_resolver.go +- ../sm-chatchannels-plugin/classes/deny_all.go +- ../sm-chatchannels-plugin/boot.go (deny-all Warn, SetAccessResolver) +- ../sm-chatchannels-plugin/routes.go (contexts/{type}/{id}) +- ../sm-chat-plugin/console/rewrite_context_types.go +- ../sm-chat-plugin/http_tracer_test.go +- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum @ ad10b54 +- Host CommunityForumController for payload keys only + + + + + + + Task 1: D-16 forum-policy note and D-17 Render/Purify gate + .planning/notes/2026-10-18-bluemonday-forum-policy.md + .planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md, .planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md, .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md (D-16, D-17), modules/seaglass/seaglass.go, modules/cabana/field_markdown.go, modules/cabana/field_markdown.go + Per D-16, if a note already exists under .planning/notes/ that names a dedicated forum policy (AllowElements of the PHP list, schemes http/https/mailto, nofollow on, target blank on) and is not the 2026-10-11 richeditor note, skip writing and record the path in the SUMMARY. Otherwise write .planning/notes/2026-10-18-bluemonday-forum-policy.md in the hashids-note shape: library pin github.com/microcosm-cc/bluemonday v1.0.27 already in framework go.mod; named forum policy (not the richeditor UGC policy); goldmark GFM already a framework dep; D-17 owns Render and Purify; forum must not vendor either library. + +Then assert D-17: a summercms.go package exports Render and Purify (grep modules/ for `func Render(` and `func Purify(` outside cabana.RenderMarkdown). If they are absent, STOP — do not create ../sm-forum-plugin and do not grow a local renderer. This is the D-17 gate (Pitfall 6). + + test -f .planning/notes/2026-10-18-bluemonday-forum-policy.md -o -n "$(grep -l 'RequireNoFollowOnLinks' .planning/notes/*bluemonday* 2>/dev/null | grep -v framework-html-sanitizer)" && echo forum-policy-note-ready + "forum-policy-note-ready" absent + rg -n 'func Render\(|func Purify\(' modules --glob '*.go' | grep -v 'RenderMarkdown' | grep -q 'func Render' && rg -n 'func Purify\(' modules --glob '*.go' | grep -q 'func Purify' && echo d17-ready + "d17-ready" absent — D-17 module missing; halt, do not start Task 2 + + + - A .planning/notes/ file names bluemonday v1.0.27 and the PHP allow-list. + - grep -n 'func Render(' modules --glob '*.go' | grep -v RenderMarkdown prints at least one line. + - grep -n 'func Purify(' modules --glob '*.go' prints at least one line. + + The forum-policy note exists and D-17 Render/Purify are importable. + + + + Task 2: End-to-end forum post — category, thread, GFM table, no script element + D-02 forum baseline id and D-05 /_forum/api/v1 become the shared contract. + D-17 exports Render and Purify (Task 1 printed d17-ready). ../sm-forum-plugin does not already exist as a foreign module. + ../sm-forum-plugin/go.mod, ../sm-forum-plugin/go.work, ../sm-forum-plugin/go.sum, ../sm-forum-plugin/.gitignore, ../sm-forum-plugin/README.md, ../sm-forum-plugin/plugin.go, ../sm-forum-plugin/boot.go, ../sm-forum-plugin/routes.go, ../sm-forum-plugin/config/config.yaml, ../sm-forum-plugin/lang/en/lang.yaml, ../sm-forum-plugin/lang/pl/lang.yaml, ../sm-forum-plugin/contracts/access_resolver.go, ../sm-forum-plugin/classes/deny_all.go, ../sm-forum-plugin/classes/forum_service.go, ../sm-forum-plugin/models/category.go, ../sm-forum-plugin/models/thread.go, ../sm-forum-plugin/models/post.go, ../sm-forum-plugin/models/thread_read.go, ../sm-forum-plugin/models/registry.go, ../sm-forum-plugin/api/auth.go, ../sm-forum-plugin/api/errors.go, ../sm-forum-plugin/api/json.go, ../sm-forum-plugin/api/categories.go, ../sm-forum-plugin/api/threads.go, ../sm-forum-plugin/api/posts.go, ../sm-forum-plugin/updates/registry.go, ../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go, ../sm-forum-plugin/updates/baseline_test.go, ../sm-forum-plugin/testmain_test.go, ../sm-forum-plugin/harness_test.go, ../sm-forum-plugin/fixture_plugin_test.go, ../sm-forum-plugin/http_tracer_test.go + ../sm-chatchannels-plugin/plugin.go, ../sm-chatchannels-plugin/contracts/access_resolver.go, ../sm-chatchannels-plugin/classes/deny_all.go, ../sm-chatchannels-plugin/boot.go, ../sm-chatchannels-plugin/routes.go, ../sm-chat-plugin/go.mod, ../sm-chat-plugin/http_tracer_test.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/ForumService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/DenyAllForumAccessResolver.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/updates/, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/tests/security/ForumMarkdownRenderTest.php + Per D-01 and D-02, create ../sm-forum-plugin as a sibling. Analog go.mod pins; replace summercms => ../summercms.go; go.work replaces sm-user-plugin. go.mod stays on analog pins plus the user plugin. Consume D-17 from summercms; do not add markdown libraries or the friends module. + +plugin.go: package forum, PluginID golem15.forum, Requires golem15.user, HasRoutes, BucketProvider, HasCommands (rewrite lands in Task 3). README H1 Forum; plugin.name Forum / Forum. SetAccessResolver analog chatchannels. + +Baseline 202610180001_create_golem15_forum_baseline: four PHP v1.1.0 tables from RESEARCH. categories.context_type NOT NULL, no FK; unique (context_type, context_id, slug); threads.user_id and posts.user_id nullable SET NULL; deletion_reason varchar check self|moderator or null. TestBaseline greps the four table names. + +contracts.AccessResolver: CanRead, CanPost, CanManage, CanManageContext. classes.DenyAll all false. ForumService.New installs DenyAll when Resolver is nil; SetAccessResolver(nil) becomes DenyAll. Boot Warn while the default is active (D-03). + +ForumService write path (D-17, D-18): CreateCategory, CreateThread, Reply persist inside lagoon.Transaction. New and edited posts call framework Render on markdown and store content_html; never call the richeditor UGC sanitizer or cabana.RenderMarkdown. GET returns the stored content_html and does not call Render again. + +Routes (D-05): APIPrefix /_forum/api/v1, jwt.auth + throttle:forum-api 120/min. Paths /contexts/{type}/{id}/categories (GET, POST), POST .../categories/{categoryId}/threads, POST .../threads/{threadId}/posts, POST .../threads/{threadId}/read, GET .../unread. Actor from token. + +Test host: golem15.user + golem15.forum + acme.teams. After Boot the harness installs an allow-all test resolver so the tracer can write. TestAccessResolverDefault (short) asserts DenyAll before that swap. TestHTTPTracer (Docker): create category+thread+post whose body is a GFM table plus a script tag; stored content_html contains a table element and does not contain a script element; mark read; unread count 0. TestNoUserfriendsImport (short) greps production sources for the friends module path. + +Remote policy follows plan 01 Task 1: tea-create only on proceed-and-push; otherwise stay local. Never force-push. + + go -C ../sm-forum-plugin vet ./... && go -C ../sm-forum-plugin test -count=1 -short ./... + non-zero exit, or "ok" lines absent, or a package reports FAIL + docker info >/dev/null 2>&1 && go -C ../sm-forum-plugin test -count=1 -run '^TestHTTPTracer$' -v . + non-zero exit: no Docker daemon (record NOT RUN), or TestHTTPTracer missing/fails, or content_html still contains a script element + + + - grep -v '^#' ../sm-forum-plugin/plugin.go | grep -c 'golem15.forum' prints at least 1. + - grep -v '^#' ../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go | grep -c 'golem15_forum_categories' prints at least 1. + - grep -n 'func (DenyAll)' ../sm-forum-plugin/classes/deny_all.go prints at least one line. + - grep -n 'sm-userfriends-plugin' ../sm-forum-plugin/go.mod prints 0 lines. + - grep -n 'bluemonday\|goldmark' ../sm-forum-plugin/go.mod prints 0 lines. + + An allow-all test host can create a sanitized GFM post through /_forum/api/v1; deny-all remains the production default. + + + + Task 3: DeleteContext, morph rewrite, remaining routes, GDPR null authorship + forum:rewrite-context-types changes stored context_type rows; PHP cannot resolve rewritten aliases. + ../sm-forum-plugin/classes/forum_service.go, ../sm-forum-plugin/console/rewrite_context_types.go, ../sm-forum-plugin/api/categories.go, ../sm-forum-plugin/api/threads.go, ../sm-forum-plugin/api/posts.go, ../sm-forum-plugin/routes.go, ../sm-forum-plugin/boot.go, ../sm-forum-plugin/plugin.go, ../sm-forum-plugin/README.md + ../sm-forum-plugin/classes/forum_service.go, ../sm-chat-plugin/console/rewrite_context_types.go, ../sm-chat-plugin/classes/writes.go, ../sm-chatchannels-plugin/boot.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/ForumService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityForumController.php + Per D-08, add ForumService.DeleteContext(ctx, type, id): soft-delete categories (and cascaded threads/posts via existing FKs / deleted_at) for that morph pair. Do not import userfriends. The host wires group-deleted to this method later. + +Per D-03 morph rewrite: command forum:rewrite-context-types walks registered LegacyPHPClass values and UPDATE golem15_forum_categories SET context_type = alias WHERE context_type = that class, idempotent. Do not extend chat's command. + +Remaining ForumService methods from PHP: seedDefaultCategory, renameCategory, reorderCategories, deleteCategory, editPost, deletePost, deleteThread, unreadThreadCount, markThreadRead. HTTP: PUT/DELETE categories, reorder, GET threads/posts, PUT/DELETE posts, DELETE threads, preview. 404 collapse for unknown type / unauthorized. + +GDPR (D-03): Listen user.Deleting; set threads.user_id and posts.user_id to null; keep bodies. + +README documents DeleteContext, the rewrite command, and that the host must SetAccessResolver. + + go -C ../sm-forum-plugin vet ./... && go -C ../sm-forum-plugin test -count=1 -short ./... + non-zero exit or a package reports FAIL + + + - grep -n 'func (s \*ForumService) DeleteContext' ../sm-forum-plugin/classes/forum_service.go prints one line. + - grep -n 'forum:rewrite-context-types' ../sm-forum-plugin/console/rewrite_context_types.go prints one line. + - grep -n 'DeletingEvent' ../sm-forum-plugin/boot.go prints at least one line. + - grep -n 'sm-userfriends-plugin' ../sm-forum-plugin/go.mod ../sm-forum-plugin/*.go ../sm-forum-plugin/classes/*.go prints 0 lines. + + Forum can cascade a generic context delete, rewrite adopted class strings, and null authorship on user delete. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| JWT actor → /_forum/api/v1 | untrusted markdown and context ids | +| ForumService write → content_html | sanitizer boundary (D-16/D-17) | +| ForumAccessResolver | host-supplied allow; default deny-all | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.1-04 | Tampering | classes/forum_service.go write path | high | mitigate | D-17 Render/Purify on write; XSS corpus in TestHTTPTracer; adopted HTML served stored (D-18) | +| T-19.1-10 | Elevation of privilege | classes/deny_all.go | high | mitigate | DenyAll default + Boot Warn + setter; IDOR → 404 | +| T-19.1-SC | Tampering | go.mod requires | high | mitigate | No new libraries in forum; consume D-17 | + + + + +Task 1 prints d17-ready before any forum file is written. go -C ../sm-forum-plugin test -short stays green. TestHTTPTracer proves GFM table and stripped script. + + + +sm-forum-plugin exists, consumes D-17, deny-all is the default, DeleteContext is generic, and the HTTP tracer stores sanitized content_html. + + + +Create `.planning/phases/19.1-chat-satellites/19.1-02-SUMMARY.md` when done + diff --git a/.planning/phases/19.1-chat-satellites/19.1-03-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-03-PLAN.md new file mode 100644 index 00000000..8208f6f7 --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-03-PLAN.md @@ -0,0 +1,226 @@ +--- +phase: 19.1-chat-satellites +plan: 03 +type: execute +wave: 1 +depends_on: [] +files_modified: + - ../sm-chatbots-plugin/.gitignore + - ../sm-chatbots-plugin/go.mod + - ../sm-chatbots-plugin/go.sum + - ../sm-chatbots-plugin/go.work + - ../sm-chatbots-plugin/README.md + - ../sm-chatbots-plugin/plugin.go + - ../sm-chatbots-plugin/boot.go + - ../sm-chatbots-plugin/routes.go + - ../sm-chatbots-plugin/config/config.yaml + - ../sm-chatbots-plugin/lang/en/lang.yaml + - ../sm-chatbots-plugin/lang/pl/lang.yaml + - ../sm-chatbots-plugin/contracts/slash_command.go + - ../sm-chatbots-plugin/contracts/bot_auth_resolver.go + - ../sm-chatbots-plugin/classes/deny_all.go + - ../sm-chatbots-plugin/classes/registry.go + - ../sm-chatbots-plugin/classes/dispatcher.go + - ../sm-chatbots-plugin/classes/help.go + - ../sm-chatbots-plugin/models/bot_flag.go + - ../sm-chatbots-plugin/models/registry.go + - ../sm-chatbots-plugin/api/auth.go + - ../sm-chatbots-plugin/api/errors.go + - ../sm-chatbots-plugin/api/json.go + - ../sm-chatbots-plugin/api/commands.go + - ../sm-chatbots-plugin/updates/registry.go + - ../sm-chatbots-plugin/updates/202610180001_create_golem15_chatbots_baseline.go + - ../sm-chatbots-plugin/updates/baseline_test.go + - ../sm-chatbots-plugin/testmain_test.go + - ../sm-chatbots-plugin/harness_test.go + - ../sm-chatbots-plugin/fixture_plugin_test.go + - ../sm-chatbots-plugin/http_tracer_test.go +autonomous: true +requirements: [SC-19.1-1, SC-19.1-2, D-01, D-02, D-03, D-04, D-05] +estimate: + tokens: 160000 + raw_tokens: 160000 + tasks: 2 + confidence: low +must_haves: + truths: + - "D-01 / SC-19.1-2: module git.golem15.com/golem15/sm-chatbots-plugin, package chatbots, PluginID golem15.chatbots, Requires golem15.chat and golem15.user; go.mod never requires the AI plugin." + - "D-02: baseline 202610180001_create_golem15_chatbots_baseline creates golem15_chatbots_bot_flags with unique user_id." + - "D-05 / CMD-06: POST /_chatbots/api/v1/dispatch body {input} uses the JWT actor; register /ping returns ephemeral and inserts 0 chat messages; GET /commands lists /help after isAvailable." + - "D-03: BotAuthResolver default DenyAll with setter and Boot Warn; user.Deleting deletes the bot_flags row." + artifacts: + - path: "../sm-chatbots-plugin/plugin.go" + provides: "golem15.chatbots plugin" + contains: "golem15.chatbots" + - path: "../sm-chatbots-plugin/classes/registry.go" + provides: "SlashCommandRegistry with reserved help and duplicate rejection" + contains: "registerBuiltin" + - path: "../sm-chatbots-plugin/classes/dispatcher.go" + provides: "CommandDispatcher" + contains: "Dispatch" + - path: "../sm-chatbots-plugin/http_tracer_test.go" + provides: "/ping ephemeral tracer" + contains: "TestHTTPTracer" + key_links: + - from: "../sm-chatbots-plugin/api/commands.go" + to: "../sm-chatbots-plugin/classes/dispatcher.go" + via: "actor from bouncer.User, never from the body" + pattern: "Dispatch" + - from: "../sm-chatbots-plugin/boot.go" + to: "../sm-chatbots-plugin/classes/deny_all.go" + via: "Boot Warn while BotAuthResolver is DenyAll" + pattern: "BotAuthResolver" + prohibitions: + - statement: "Production sources never import the AI plugin module and never take invoker id from the request body." + status: unresolved + verification: grep + - statement: "Reserved help cannot be register()d as a host command; registerBuiltin installs it." + status: unresolved + verification: judgment +--- + +## Phase Goal + +**As a** application that mounts the satellites, **I want to** have Chat bots on SummerCMS with a generic `/_chatbots/api/v1`, **so that** a host can register slash commands and dispatch them as the JWT actor. + +This plan's slice (plan 3 of 6, wave 1): `golem15.chatbots` with `/ping` ephemeral dispatch. + +## Decisions taken in this plan (Claude's discretion) + +- Package `chatbots`, PluginID `golem15.chatbots`. +- SlashCommand interface: Trigger, Description, IsAvailable, Handle. Modes ephemeral / announce. +- Reserved trigger `help`; register() rejects reserved + duplicates; registerBuiltin skips the reserved check. +- Unrecognized trigger returns nil (fall through). +- APIPrefix `/_chatbots/api/v1`, APIBucket `chatbots-api` 120/min. +- Baseline id `202610180001_create_golem15_chatbots_baseline`. +- Display name Chat bots / Boty czatu. + +## Artifacts this phase produces (this plan) + +- `../sm-chatbots-plugin`. +- Types: `contracts.SlashCommand`, `contracts.BotAuthResolver`, `classes.Registry`, `classes.Dispatcher`, `classes.CommandResult`, `models.BotFlag`. +- Funcs: `Registry.Register`, `RegisterBuiltin`, `Dispatcher.Dispatch`, `Plugin.SetBotAuthResolver`. +- Routes: `GET /_chatbots/api/v1/commands`, `POST /_chatbots/api/v1/dispatch`. + + +Create sm-chatbots-plugin with bot_flags (D-02), SlashCommandRegistry/dispatcher (D-05), deny-all BotAuthResolver (D-03), and the /ping HTTP tracer. No AI-plugin import (SC-19.1-2). + +Purpose: hosts register commands; the invoker is always the authenticated actor. +Output: TestHTTPTracer dispatches /ping as JWT actor and sees ephemeral with zero chat inserts. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md +@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md +@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md + + +Analog: +- ../sm-chatchannels-plugin/plugin.go (Requires chat+user) +- ../sm-chatchannels-plugin/classes/deny_all.go +- ../sm-chat-plugin/contracts/registry.go (duplicate rejection) +- ../sm-chat-plugin/api/auth.go +- ../sm-chat-plugin/http_tracer_test.go +- ../sm-chat-plugin/go.work (chat replace) +- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots @ 1ecf8c8 + + + + + + + Task 1: End-to-end /ping — registry, dispatcher, JWT actor, ephemeral, /help + D-02 bot_flags baseline and D-05 /_chatbots/api/v1 become the shared contract. + ../sm-chat-plugin exists (Phase 19) and ../sm-chatbots-plugin does not already exist as a foreign module. + ../sm-chatbots-plugin/go.mod, ../sm-chatbots-plugin/go.work, ../sm-chatbots-plugin/go.sum, ../sm-chatbots-plugin/.gitignore, ../sm-chatbots-plugin/README.md, ../sm-chatbots-plugin/plugin.go, ../sm-chatbots-plugin/boot.go, ../sm-chatbots-plugin/routes.go, ../sm-chatbots-plugin/config/config.yaml, ../sm-chatbots-plugin/lang/en/lang.yaml, ../sm-chatbots-plugin/lang/pl/lang.yaml, ../sm-chatbots-plugin/contracts/slash_command.go, ../sm-chatbots-plugin/contracts/bot_auth_resolver.go, ../sm-chatbots-plugin/classes/deny_all.go, ../sm-chatbots-plugin/classes/registry.go, ../sm-chatbots-plugin/classes/dispatcher.go, ../sm-chatbots-plugin/classes/help.go, ../sm-chatbots-plugin/models/bot_flag.go, ../sm-chatbots-plugin/models/registry.go, ../sm-chatbots-plugin/api/auth.go, ../sm-chatbots-plugin/api/errors.go, ../sm-chatbots-plugin/api/json.go, ../sm-chatbots-plugin/api/commands.go, ../sm-chatbots-plugin/updates/registry.go, ../sm-chatbots-plugin/updates/202610180001_create_golem15_chatbots_baseline.go, ../sm-chatbots-plugin/updates/baseline_test.go, ../sm-chatbots-plugin/testmain_test.go, ../sm-chatbots-plugin/harness_test.go, ../sm-chatbots-plugin/fixture_plugin_test.go, ../sm-chatbots-plugin/http_tracer_test.go + ../sm-chatchannels-plugin/plugin.go, ../sm-chatchannels-plugin/go.work, ../sm-chatchannels-plugin/classes/deny_all.go, ../sm-chat-plugin/contracts/registry.go, ../sm-chat-plugin/api/auth.go, ../sm-chat-plugin/http_tracer_test.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/SlashCommandRegistry.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/CommandDispatcher.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/CommandResult.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/commands/HelpCommand.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/contracts/SlashCommand.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/contracts/BotAuthResolver.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/updates/create_bot_flags_table.php + Per D-01 and D-02, create ../sm-chatbots-plugin. Analog pins; replace summercms => ../summercms.go; go.work replaces sm-user-plugin and sm-chat-plugin. Require sm-chat-plugin. Do not require the AI plugin module (SC-19.1-2). + +plugin.go: package chatbots, PluginID golem15.chatbots, Requires golem15.chat and golem15.user. README H1 Chat bots; plugin.name Chat bots. SetBotAuthResolver analog SetAccessResolver. + +Baseline 202610180001_create_golem15_chatbots_baseline: golem15_chatbots_bot_flags (id, user_id unique chatbots_bot_flags_user_unique, is_bot bool default true, timestamps, FK user_id cascade). TestBaseline greps that table. + +Registry (PHP SlashCommandRegistry): reserved trigger help; Register rejects reserved and duplicates; RegisterBuiltin skips the reserved check and installs HelpCommand. Dispatcher.Dispatch(input, actor, ctx) parses the trigger; unrecognized returns nil; never reads an invoker id from input (D-05 CMD-06). CommandResult modes ephemeral and announce. + +This tracer registers a builtin-style /ping in the test host that returns ephemeral payload pong. Production Boot registers only help via RegisterBuiltin. + +BotAuthResolver (D-03): DenyAll Resolve returns nil, IsElevated false. New installs DenyAll; setter nil becomes DenyAll; Boot Warn while default is active. + +Routes: GET /_chatbots/api/v1/commands (filter IsAvailable for the JWT actor), POST /_chatbots/api/v1/dispatch body {input}. jwt.auth + throttle:chatbots-api 120/min. Actor from bouncer.User. + +Test host: golem15.user + golem15.chat + golem15.chatbots. TestHTTPTracer: JWT actor POST {input:"/ping"} returns ephemeral pong; chat message count for that conversation stays 0; GET /commands includes help. TestNoGolem (short) greps production .go and go.mod for the AI plugin module path. TestAccessResolverDefault analog for BotAuthResolver DenyAll. + +Remote: stay local unless plan 01 already chose proceed-and-push and tea is available; never force-push. + + go -C ../sm-chatbots-plugin vet ./... && go -C ../sm-chatbots-plugin test -count=1 -short ./... + non-zero exit, or "ok" lines absent, or a package reports FAIL + docker info >/dev/null 2>&1 && go -C ../sm-chatbots-plugin test -count=1 -run '^TestHTTPTracer$' -v . + non-zero exit: no Docker daemon (record NOT RUN), or TestHTTPTracer missing/fails, or a chat message row was inserted for /ping + + + - grep -v '^#' ../sm-chatbots-plugin/plugin.go | grep -c 'golem15.chatbots' prints at least 1. + - grep -n 'golem15.chat' ../sm-chatbots-plugin/plugin.go prints at least one Requires line. + - grep -n 'sm-golem-plugin\|golem15.golem' ../sm-chatbots-plugin/go.mod ../sm-chatbots-plugin/*.go ../sm-chatbots-plugin/classes/*.go prints 0 lines. + - grep -n 'ReservedHelp\|"help"' ../sm-chatbots-plugin/classes/registry.go prints at least one line. + + A JWT actor can dispatch /ping ephemerally and see /help in the catalog; the AI plugin is not a dependency. + + + + Task 2: GDPR bot_flags delete and catalog/auth expansion + ../sm-chatbots-plugin/boot.go, ../sm-chatbots-plugin/classes/registry.go, ../sm-chatbots-plugin/api/commands.go, ../sm-chatbots-plugin/README.md + ../sm-chatbots-plugin/boot.go, ../sm-chat-plugin/boot.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityCommandController.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/BotRestController.php + Per D-03, Listen user.Deleting and delete golem15_chatbots_bot_flags for that user_id inside ev.Tx. + +Document BotAuthResolver: generic API stays jwt.auth for humans; host may later resolve a bot bearer through the setter (Wavepath BotRestReachTest behavior). Deny-all means no bot principal until the host installs one. Catalog continues to filter IsAvailable. + +README documents Register / RegisterBuiltin, reserved help, dispatch actor rule, and that the plugin requires Chat and User only. + + go -C ../sm-chatbots-plugin vet ./... && go -C ../sm-chatbots-plugin test -count=1 -short ./... + non-zero exit or a package reports FAIL + + + - grep -n 'DeletingEvent' ../sm-chatbots-plugin/boot.go prints at least one line. + - grep -n 'bot_flags' ../sm-chatbots-plugin/boot.go prints at least one line. + - grep -n 'sm-golem-plugin\|golem15.golem' ../sm-chatbots-plugin/go.mod ../sm-chatbots-plugin/*.go prints 0 lines. + + Deleting a user removes their bot_flags row; dispatch still uses the JWT actor. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| JWT actor → POST /dispatch | untrusted input string; invoker must not come from the body | +| BotAuthResolver | host-supplied bot bearer; default deny-all | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.1-07 | Elevation of privilege | api/commands.go | high | mitigate | Actor from bouncer.User only (CMD-06); TestHTTPTracer uses the token user | +| T-19.1-11 | Elevation of privilege | classes/deny_all.go | high | mitigate | BotAuthResolver DenyAll + Boot Warn + setter | +| T-19.1-SC | Tampering | go.mod requires | high | mitigate | Analog pins plus sm-chat-plugin; no AI plugin module | + + + + +go -C ../sm-chatbots-plugin test -short stays green. TestHTTPTracer proves /ping ephemeral and /help listed. TestNoGolem stays green. + + + +sm-chatbots-plugin exists with bot_flags, registry/dispatcher, deny-all bot auth, and a working /ping tracer. It requires Chat and User only. + + + +Create `.planning/phases/19.1-chat-satellites/19.1-03-SUMMARY.md` when done + diff --git a/.planning/phases/19.1-chat-satellites/19.1-04-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-04-PLAN.md new file mode 100644 index 00000000..37495983 --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-04-PLAN.md @@ -0,0 +1,258 @@ +--- +phase: 19.1-chat-satellites +plan: 04 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .planning/notes/2026-10-18-webpush-go.md + - ../sm-herald-plugin/.gitignore + - ../sm-herald-plugin/go.mod + - ../sm-herald-plugin/go.sum + - ../sm-herald-plugin/go.work + - ../sm-herald-plugin/README.md + - ../sm-herald-plugin/plugin.go + - ../sm-herald-plugin/boot.go + - ../sm-herald-plugin/routes.go + - ../sm-herald-plugin/config/config.yaml + - ../sm-herald-plugin/lang/en/lang.yaml + - ../sm-herald-plugin/lang/pl/lang.yaml + - ../sm-herald-plugin/contracts/herald_profile.go + - ../sm-herald-plugin/contracts/presence_gate.go + - ../sm-herald-plugin/classes/deny_all.go + - ../sm-herald-plugin/classes/neutral_profile.go + - ../sm-herald-plugin/classes/subscription_service.go + - ../sm-herald-plugin/classes/sender.go + - ../sm-herald-plugin/jobs/send.go + - ../sm-herald-plugin/console/vapid_generate.go + - ../sm-herald-plugin/models/subscription.go + - ../sm-herald-plugin/models/registry.go + - ../sm-herald-plugin/api/auth.go + - ../sm-herald-plugin/api/errors.go + - ../sm-herald-plugin/api/json.go + - ../sm-herald-plugin/api/push.go + - ../sm-herald-plugin/updates/registry.go + - ../sm-herald-plugin/updates/202610180001_create_golem15_herald_baseline.go + - ../sm-herald-plugin/updates/baseline_test.go + - ../sm-herald-plugin/testmain_test.go + - ../sm-herald-plugin/harness_test.go + - ../sm-herald-plugin/fixture_plugin_test.go + - ../sm-herald-plugin/http_tracer_test.go +autonomous: true +requirements: [SC-19.1-1, SC-19.1-2, D-04, D-05, D-06, D-09, D-10, D-11, D-12] +estimate: + tokens: 200000 + raw_tokens: 200000 + tasks: 3 + confidence: low +must_haves: + truths: + - "D-09 / SC-19.1-2: .planning/notes/2026-10-18-webpush-go.md exists and names github.com/SherClockHolmes/webpush-go v1.4.0, jwt/v5 share, Options.HTTPClient = *fetchguard.Client Do, and does not name the in-tree Web Push module as the sender." + - "D-10: subscribe rejects non-HTTPS and non-public hosts; send uses fetchguard PublicOnlyMode; there is no EndpointGuard type." + - "D-11: SendEvent and SendUnconditional Enqueue Kind golem15.herald.send on the caller tx; a rolled-back write sends 0; 410/404 prune the row." + - "D-12: config keys golem15.herald.vapid.{subject,public_key,private_key}; summer herald:vapid-generate prints VAPID_PUBLIC_KEY= and VAPID_PRIVATE_KEY= lines; the private key is never logged." + - "D-06 / D-05: POST/PATCH/DELETE /_herald/api/v1/push/subscription and GET /_herald/api/v1/push/vapid-public-key exist; PHP ships-no-routes rule is reversed in the README." + - "D-03: PresenceGate default DenyAll with Boot Warn; HeraldProfile default is Neutral (app name only), no Warn." + artifacts: + - path: ".planning/notes/2026-10-18-webpush-go.md" + provides: "webpush-go decision note before import" + contains: "SherClockHolmes/webpush-go" + - path: "../sm-herald-plugin/jobs/send.go" + provides: "conga send job" + contains: "golem15.herald.send" + - path: "../sm-herald-plugin/console/vapid_generate.go" + provides: "herald:vapid-generate" + contains: "herald:vapid-generate" + - path: "../sm-herald-plugin/http_tracer_test.go" + provides: "subscribe + SendUnconditional + 410 prune + rollback" + contains: "TestSendEventRollback" + key_links: + - from: "../sm-herald-plugin/classes/sender.go" + to: "modules/fetchguard/client.go" + via: "Options.HTTPClient = publicOnly *fetchguard.Client" + pattern: "HTTPClient" + - from: "../sm-herald-plugin/classes/sender.go" + to: "../sm-herald-plugin/jobs/send.go" + via: "Enqueue on caller tx" + pattern: "Enqueue" + prohibitions: + - statement: "herald never imports the in-tree Web Push module and never uses push.* config keys." + status: unresolved + verification: grep + - statement: "The private VAPID key is never written to slog, payloads, or README examples." + status: unresolved + verification: judgment + - statement: "Requires() is empty; herald does not import user models, only user ids." + status: unresolved + verification: grep +--- + +## Phase Goal + +**As a** application that mounts the satellites, **I want to** have Herald on SummerCMS with a generic `/_herald/api/v1` push API, **so that** a host can store subscriptions and send Web Push without PHP. + +This plan's slice (plan 4 of 6, wave 1): decision note first, then subscribe + SendUnconditional + 410 prune + rollback sends 0. + +## Decisions taken in this plan (Claude's discretion) + +- Package `herald`, PluginID `golem15.herald`, Requires empty. +- Job Kind `golem15.herald.send`, queue `push`, MaxAttempts 1. +- PresenceGate.ShouldSend(userID, surface, eventID) bool. +- NeutralHeraldProfile: Icon, Badge, BaseUrl, NotificationFor, TagFor (app-name-only). +- OBS-01 cooldown: process-local sync.Map 60s (PHP in-process fallback). +- APIPrefix `/_herald/api/v1`, APIBucket `herald-api` 120/min. +- Baseline id `202610180001_create_golem15_herald_baseline`. +- Display name Herald / Herald. + +## Artifacts this phase produces (this plan) + +- Decision note `.planning/notes/2026-10-18-webpush-go.md`. +- `../sm-herald-plugin`. +- Types: `contracts.HeraldProfile`, `contracts.PresenceGate`, `classes.SubscriptionService`, `jobs.SendArgs`. +- Funcs: `SendEvent`, `SendUnconditional`, `Subscribe`, `Patch`, `Unsubscribe`. +- Command: `summer herald:vapid-generate`. +- Config keys: `golem15.herald.vapid.subject`, `.public_key`, `.private_key`. + + +Write the webpush-go decision note (D-09), then create sm-herald-plugin with subscriptions, PublicOnly send (D-10), a conga job enqueued in the caller tx (D-11), PresenceGate deny-all (D-03), and the generic push API (D-05, D-06). + +Purpose: a rolled-back write must push nothing; 410 must prune. +Output: TestHTTPTracer POSTs an https subscription, SendUnconditional after commit hits the fake, 410 deletes the row, rollback sends 0. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md +@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md +@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md +@.planning/phases/19.1-chat-satellites/COVERAGE.md +@.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md + + +Analog: +- ../sm-chat-plugin/jobs/maintain_thread_summary.go +- ../sm-chat-plugin/classes/service.go Enqueue on tx +- ../sm-chatchannels-plugin/classes/deny_all.go +- modules/fetchguard/client.go Do +- modules/fetchguard/policy.go PublicOnlyMode +- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald @ a749f83 + + + + + + + Task 1: webpush-go decision note before any herald import + D-09 sits behind the plugin sender; swapping the library later is a local change. + .planning/notes/2026-10-18-webpush-go.md + .planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md, .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (D-09, Pitfall 7, Pitfall 8), .planning/phases/19.1-chat-satellites/COVERAGE.md, modules/fetchguard/client.go, modules/flare/commands.go + Per D-09 / SC-19.1-2, write .planning/notes/2026-10-18-webpush-go.md in the hashids-note shape BEFORE creating the herald repo or running go get. Confirm the pin with go list -m -versions github.com/SherClockHolmes/webpush-go (expect v1.4.0). The note must record: module path and v1.4.0; MIT; shares github.com/golang-jwt/jwt/v5 with the framework; Options.HTTPClient is the Do interface so *fetchguard.Client is passed directly (Pitfall 8 — no adapter type); TTL default 2419200 and urgency normal; prune 404/410; why the in-tree Web Push module is not used (different config keys and command name). Do not generate keys in this task. + + test -f .planning/notes/2026-10-18-webpush-go.md && grep -q 'SherClockHolmes/webpush-go' .planning/notes/2026-10-18-webpush-go.md && grep -q 'v1.4.0' .planning/notes/2026-10-18-webpush-go.md && echo webpush-note-ready + "webpush-note-ready" absent or the note lacks the module path or v1.4.0 + + + - The note names Options.HTTPClient and fetchguard.Client. + - The note names jwt/v5 as a shared transitive. + - grep -n 'flare\|websockets:generate-vapid-keys' .planning/notes/2026-10-18-webpush-go.md may mention them only as the rejected alternative. + + The decision note exists and no herald go.mod yet requires webpush-go. + + + + Task 2: End-to-end push — subscribe https, SendUnconditional after commit, 410 prune, rollback sends 0 + D-02 herald baseline and D-05 /_herald/api/v1 become the shared contract; D-06 publishes routes PHP did not ship. + Task 1 printed webpush-note-ready. ../sm-herald-plugin does not already exist as a foreign module. + ../sm-herald-plugin/go.mod, ../sm-herald-plugin/go.work, ../sm-herald-plugin/go.sum, ../sm-herald-plugin/.gitignore, ../sm-herald-plugin/README.md, ../sm-herald-plugin/plugin.go, ../sm-herald-plugin/boot.go, ../sm-herald-plugin/routes.go, ../sm-herald-plugin/config/config.yaml, ../sm-herald-plugin/lang/en/lang.yaml, ../sm-herald-plugin/lang/pl/lang.yaml, ../sm-herald-plugin/classes/subscription_service.go, ../sm-herald-plugin/classes/sender.go, ../sm-herald-plugin/jobs/send.go, ../sm-herald-plugin/models/subscription.go, ../sm-herald-plugin/models/registry.go, ../sm-herald-plugin/api/auth.go, ../sm-herald-plugin/api/errors.go, ../sm-herald-plugin/api/json.go, ../sm-herald-plugin/api/push.go, ../sm-herald-plugin/updates/registry.go, ../sm-herald-plugin/updates/202610180001_create_golem15_herald_baseline.go, ../sm-herald-plugin/updates/baseline_test.go, ../sm-herald-plugin/testmain_test.go, ../sm-herald-plugin/harness_test.go, ../sm-herald-plugin/fixture_plugin_test.go, ../sm-herald-plugin/http_tracer_test.go + ../sm-chat-plugin/plugin.go, ../sm-chat-plugin/jobs/maintain_thread_summary.go, ../sm-chat-plugin/classes/service.go, ../sm-chat-plugin/http_tracer_test.go, modules/fetchguard/client.go, modules/fetchguard/policy.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/PushSender.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/services/SubscriptionService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/updates/create_push_subscriptions_table.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/config/herald.php, .planning/notes/2026-10-18-webpush-go.md + Only after the note exists, create ../sm-herald-plugin and go get github.com/SherClockHolmes/webpush-go@v1.4.0. Analog pins; replace summercms => ../summercms.go. Requires() empty. Do not import user models. Do not import the in-tree Web Push module. + +plugin.go: package herald, PluginID golem15.herald, HasJobs, HasRoutes, HasCommands (command in Task 3), BucketProvider. README H1 Herald; rewrite the PHP reuse contract around the generic API (D-06). + +Baseline 202610180001_create_golem15_herald_baseline: golem15_herald_push_subscriptions (endpoint varchar 500, keys text JSON p256dh/auth, unique (user_id, endpoint) herald_push_user_endpoint_unique, FK user_id cascade because the test host has users). TestBaseline greps the table. + +SubscriptionService.Subscribe(userID, endpoint, keys, ua): reject non-HTTPS and non-public hosts with the same PublicOnly policy (D-10, fail-closed). Unique (user_id, endpoint). + +Sender: fetchguard.NewClient PublicOnlyMode; webpush Options.HTTPClient = that client; TTL 2419200; Urgency normal. SendUnconditional Enqueue jobs.SendArgs on the caller tx (D-11), Kind golem15.herald.send. The worker sends to every subscription and prunes 404 and 410. Tests inject fetchguard.WithTransport — no extra Doer type. + +Routes (D-05): POST /_herald/api/v1/push/subscription jwt.auth + throttle:herald-api. Actor from token. + +Test host: golem15.user (JWT only) + golem15.herald. TestSubscribeEndpoint (short): http:// and a private-IP host are rejected. TestHTTPTracer (Docker): POST https subscription against a fake transport; after commit SendUnconditional hits the fake; a 410 response deletes the row. TestSendEventRollback (Docker): Enqueue inside a tx that rolls back; fake send count stays 0. + +Remote: stay local unless plan 01 chose proceed-and-push; never force-push. + + go -C ../sm-herald-plugin vet ./... && go -C ../sm-herald-plugin test -count=1 -short ./... + non-zero exit, or "ok" lines absent, or a package reports FAIL + docker info >/dev/null 2>&1 && go -C ../sm-herald-plugin test -count=1 -run '^(TestHTTPTracer|TestSendEventRollback)$' -v . + non-zero exit: no Docker daemon (record NOT RUN), or either test is missing/fails, or rollback still invoked the fake send + + + - grep -n 'SherClockHolmes/webpush-go' ../sm-herald-plugin/go.mod prints one line. + - grep -n 'golem15.herald.send' ../sm-herald-plugin/jobs/send.go prints at least one line. + - grep -n 'PublicOnlyMode' ../sm-herald-plugin/classes/*.go prints at least one line. + - grep -n 'modules/flare' ../sm-herald-plugin/go.mod ../sm-herald-plugin/*.go ../sm-herald-plugin/classes/*.go ../sm-herald-plugin/jobs/*.go prints 0 lines. + + An https subscription can be stored; a committed send hits the fake and 410 prunes; a rolled-back send hits nothing. + + + + Task 3: PresenceGate, NeutralHeraldProfile, vapid-generate, remaining push routes + ../sm-herald-plugin/contracts/herald_profile.go, ../sm-herald-plugin/contracts/presence_gate.go, ../sm-herald-plugin/classes/deny_all.go, ../sm-herald-plugin/classes/neutral_profile.go, ../sm-herald-plugin/classes/sender.go, ../sm-herald-plugin/console/vapid_generate.go, ../sm-herald-plugin/api/push.go, ../sm-herald-plugin/routes.go, ../sm-herald-plugin/boot.go, ../sm-herald-plugin/plugin.go, ../sm-herald-plugin/config/config.yaml, ../sm-herald-plugin/README.md + ../sm-herald-plugin/classes/sender.go, ../sm-chatchannels-plugin/classes/deny_all.go, ../sm-chat-plugin/console/rewrite_context_types.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/DenyAllPresenceGate.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/NeutralHeraldProfile.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/console/GenerateVapidKeys.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/config/herald.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/PushSubscriptionController.php + Per D-03, PresenceGate default DenyAll ShouldSend always false; SetPresenceGate; Boot Warn while DenyAll is active. HeraldProfile default NeutralHeraldProfile (app name only) — no Warn for the profile. + +SendEvent (D-11): Enqueue on caller tx; the worker calls PresenceGate.ShouldSend and skips when false; SendUnconditional does not consult the gate. PayloadBuilder uses HeraldProfile. OBS-01: process-local sync.Map 60s for other delivery failures. + +D-12: config keys golem15.herald.vapid.subject (default mailto:admin@example.com), public_key, private_key; map PHP env names VAPID_SUBJECT / VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY. Boot Warn when keys are missing; never auto-generate. Command herald:vapid-generate calls webpush GenerateVAPIDKeys and prints VAPID_PUBLIC_KEY= and VAPID_PRIVATE_KEY= lines. Do not slog the private key. Do not copy the in-tree generate-vapid command name. + +Remaining routes: PATCH and DELETE /_herald/api/v1/push/subscription, GET /_herald/api/v1/push/vapid-public-key (public key only). README records D-06: this plugin ships the generic API. + + go -C ../sm-herald-plugin vet ./... && go -C ../sm-herald-plugin test -count=1 -short ./... + non-zero exit or a package reports FAIL + + + - grep -n 'herald:vapid-generate' ../sm-herald-plugin/console/vapid_generate.go prints one line. + - grep -n 'golem15.herald.vapid' ../sm-herald-plugin/config/config.yaml ../sm-herald-plugin/*.go prints at least one line. + - grep -n 'ShouldSend' ../sm-herald-plugin/contracts/presence_gate.go prints one line. + - grep -n 'modules/flare\|websockets:generate-vapid-keys' ../sm-herald-plugin/*.go ../sm-herald-plugin/console/*.go prints 0 lines. + + PresenceGate is fail-closed, vapid-generate prints env lines, and PATCH/DELETE/GET vapid-public-key exist. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Client → POST subscription | user-supplied endpoint URL (SSRF) | +| Send job → push service | outbound HTTPS via PublicOnly | +| Operator → vapid-generate | private key on stdout only | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.1-02 | Elevation of privilege | classes/subscription_service.go, sender.go | high | mitigate | PublicOnly at subscribe and send; TestSubscribeEndpoint; no EndpointGuard seam | +| T-19.1-06 | Information disclosure | console/vapid_generate.go | high | mitigate | Print env lines only; never slog the private key; GET vapid-public-key returns the public key | +| T-19.1-SC | Tampering | go.mod webpush-go | high | mitigate | Note-first pin v1.4.0; package-legitimacy crates check does not apply | + + + + +The note exists before go get. go -C ../sm-herald-plugin test -short stays green. Docker TestHTTPTracer and TestSendEventRollback prove send-after-commit, 410 prune, and rollback silence. + + + +Herald ships the generic push API, PublicOnly send, transactional enqueue, and vapid-generate. The in-tree Web Push module is unused. + + + +Create `.planning/phases/19.1-chat-satellites/19.1-04-SUMMARY.md` when done + diff --git a/.planning/phases/19.1-chat-satellites/19.1-05-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-05-PLAN.md new file mode 100644 index 00000000..62a8fe97 --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-05-PLAN.md @@ -0,0 +1,284 @@ +--- +phase: 19.1-chat-satellites +plan: 05 +type: execute +wave: 2 +depends_on: [] +files_modified: + - .planning/notes/2026-10-18-livekit-server-sdk-go.md + - ../sm-chatvideo-plugin/.gitignore + - ../sm-chatvideo-plugin/go.mod + - ../sm-chatvideo-plugin/go.sum + - ../sm-chatvideo-plugin/go.work + - ../sm-chatvideo-plugin/README.md + - ../sm-chatvideo-plugin/plugin.go + - ../sm-chatvideo-plugin/boot.go + - ../sm-chatvideo-plugin/routes.go + - ../sm-chatvideo-plugin/schedule.go + - ../sm-chatvideo-plugin/config/config.yaml + - ../sm-chatvideo-plugin/lang/en/lang.yaml + - ../sm-chatvideo-plugin/lang/pl/lang.yaml + - ../sm-chatvideo-plugin/classes/livekit.go + - ../sm-chatvideo-plugin/classes/authorizer.go + - ../sm-chatvideo-plugin/classes/video_service.go + - ../sm-chatvideo-plugin/classes/notifier.go + - ../sm-chatvideo-plugin/jobs/reconcile.go + - ../sm-chatvideo-plugin/console/reconcile.go + - ../sm-chatvideo-plugin/models/session.go + - ../sm-chatvideo-plugin/models/participant.go + - ../sm-chatvideo-plugin/models/registry.go + - ../sm-chatvideo-plugin/api/auth.go + - ../sm-chatvideo-plugin/api/errors.go + - ../sm-chatvideo-plugin/api/json.go + - ../sm-chatvideo-plugin/api/video.go + - ../sm-chatvideo-plugin/api/call_links.go + - ../sm-chatvideo-plugin/updates/registry.go + - ../sm-chatvideo-plugin/updates/202610180001_create_golem15_chatvideo_baseline.go + - ../sm-chatvideo-plugin/updates/baseline_test.go + - ../sm-chatvideo-plugin/testmain_test.go + - ../sm-chatvideo-plugin/harness_test.go + - ../sm-chatvideo-plugin/fixture_plugin_test.go + - ../sm-chatvideo-plugin/http_tracer_test.go +autonomous: true +requirements: [SC-19.1-1, SC-19.1-2, D-04, D-05, D-07, D-13, D-14, D-15] +estimate: + tokens: 200000 + raw_tokens: 200000 + tasks: 3 + confidence: low +must_haves: + truths: + - "D-13 / SC-19.1-2: .planning/notes/2026-10-18-livekit-server-sdk-go.md names github.com/livekit/server-sdk-go/v2 v2.19.0, flags pion/media-sdk/buildkit, RoomAPI interface, TrustedMode for the operator URL, and jwt/v5 MapClaims if SDK grant JSON differs." + - "D-02: baseline 202610180001_create_golem15_chatvideo_baseline creates golem15_chatvideo_sessions (unique active_conversation_id, join_code, no updated_at, CREATED_AT=started_at) and participants; demo-seed v1.0.1 is a comment." + - "D-05 / D-15: paths /_chatvideo/api/v1/conversations/{id}/video/{start,join,leave,end,status} and /call-links/{code}; video-initiate 5/min on start only; authorizer splits segment 2 of video:session:{id} and never uses the segment-1 helper; capabilities prs." + - "D-14: summer chatvideo:reconcile-sessions plus conga Kind golem15.chatvideo.reconcile_sessions MaxAttempts 1 every 5 minutes; generic only (empty/gone room or max_duration); no GuestCall, call cards, or CallNotifier." + - "D-13 token golden: HS256, iss API key, sub user id string, name, iat/nbf/exp token_ttl 900, video grant room/roomJoin/canPublish/canSubscribe, optional metadata JSON avatar." + artifacts: + - path: ".planning/notes/2026-10-18-livekit-server-sdk-go.md" + provides: "LiveKit v2 decision note before import" + contains: "server-sdk-go/v2" + - path: "../sm-chatvideo-plugin/classes/livekit.go" + provides: "RoomAPI wrapper" + contains: "type RoomAPI interface" + - path: "../sm-chatvideo-plugin/classes/authorizer.go" + provides: "video namespace authorizer" + contains: "parseVideoSessionChannel" + - path: "../sm-chatvideo-plugin/jobs/reconcile.go" + provides: "generic reconciler job" + contains: "golem15.chatvideo.reconcile_sessions" + - path: "../sm-chatvideo-plugin/console/reconcile.go" + provides: "chatvideo:reconcile-sessions" + contains: "chatvideo:reconcile-sessions" + key_links: + - from: "../sm-chatvideo-plugin/boot.go" + to: "../sm-chatvideo-plugin/classes/authorizer.go" + via: "Registry.Register video" + pattern: "Register\\(\"video\"" + - from: "../sm-chatvideo-plugin/classes/livekit.go" + to: "modules/fetchguard/policy.go" + via: "TrustedMode HTTP to livekit_admin_url" + pattern: "TrustedMode" + - from: "../sm-chatvideo-plugin/routes.go" + to: "../sm-chatvideo-plugin/api/video.go" + via: "video-initiate 5/min on start only" + pattern: "video-initiate" + prohibitions: + - statement: "Production and tests never construct lksdk.Room and tests never import pion." + status: unresolved + verification: grep + - statement: "The authorizer never calls the lighthouse segment-1 helper for video:session ids." + status: unresolved + verification: grep + - statement: "Reconciler does not import GuestCall, call cards, CallNotifier, or start_grace_seconds." + status: unresolved + verification: grep + - statement: "Legacy /api/video paths and bundled JS/CSS are not registered (D-07)." + status: unresolved + verification: judgment +user_setup: + - service: livekit + why: "Operator URL and keys for a real SFU. Tests use a RoomAPI fake; no dashboard is required to execute." + env_vars: + - name: LIVEKIT_URL + source: "chatvideo config livekit_url; default ws://localhost:7880" + - name: LIVEKIT_API_KEY + source: "operator LiveKit project" + - name: LIVEKIT_API_SECRET + source: "operator LiveKit project" +--- + +## Phase Goal + +**As a** application that mounts the satellites, **I want to** have Chat video on SummerCMS with a generic `/_chatvideo/api/v1`, **so that** a host can start conversation-scoped LiveKit sessions without PHP. + +This plan's slice (plan 5 of 6, wave 2): LiveKit note first, then start session + token golden + authorizer + empty-room reconcile. + +## Decisions taken in this plan (Claude's discretion) + +- Package `chatvideo`, PluginID `golem15.chatvideo`, Requires `golem15.chat`. +- RoomAPI: CreateRoom, DeleteRoom (missing-room no-op), RemoveParticipant, ParticipantCount (missing-room 0), ActiveRoomNames. +- Conversation id is the primary address; optional context lookup through chat Registry. +- Job Kind `golem15.chatvideo.reconcile_sessions`, MaxAttempts 1, pact.Every 5 minutes. +- join_code alphabet `abcdefghijklmnopqrstuvwxyz0123456789` length 10. +- Display name Chat video / Czat wideo. +- After this plan's APIs exist, execute should run the security-review agent (CONTEXT discretion). This planner does not spawn it. + +## Artifacts this phase produces (this plan) + +- Decision note `.planning/notes/2026-10-18-livekit-server-sdk-go.md`. +- `../sm-chatvideo-plugin`. +- Types: `classes.RoomAPI`, `models.Session`, `models.Participant`. +- Funcs: `VideoService.Start`, `Join`, `Leave`, `End`, `Status`, `Reconcile`, `AuthorizeVideo`. +- Command: `summer chatvideo:reconcile-sessions`. +- Config keys: livekit_url, livekit_api_key, livekit_api_secret, max_duration, token_ttl, livekit_admin_url, max_participants, screen_share. + + +Write the LiveKit v2 decision note (D-13), then create sm-chatvideo-plugin with sessions, RoomAPI + TrustedMode HTTP, a video authorizer that splits segment 2 (D-15), the generic video API (D-05), and a generic reconciler (D-14). + +Purpose: one active session per conversation; strangers cannot subscribe video:session:{id}. +Output: TestHTTPTracer starts a session, TestLiveKitTokenClaims matches PHP, TestVideoAuthorizer denies a stranger, reconcile of an empty room ends the session. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md +@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md +@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md +@.planning/phases/19.1-chat-satellites/COVERAGE.md +@.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md + + +Analog: +- ../sm-chat-plugin/classes/authorizer.go (parse three segments; do not use the segment-1 helper) +- ../sm-chat-plugin/boot.go Registry.Register +- ../sm-paymentgateway-plugin/schedule.go pact.Every +- modules/lighthouse/channel.go (segment-1 helper — anti-analog) +- modules/fetchguard/policy.go TrustedMode +- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo @ 25135b4 +- Wavepath ReconcileVideoSessions.php generic part only + + + + + + + Task 1: LiveKit v2 decision note before any chatvideo import + D-13 sits behind RoomAPI; swapping the SDK later is a local wrapper change. + .planning/notes/2026-10-18-livekit-server-sdk-go.md + .planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md, .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (D-13, Pitfall 9), .planning/phases/19.1-chat-satellites/COVERAGE.md, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/LiveKitService.php + Per D-13 / SC-19.1-2, write .planning/notes/2026-10-18-livekit-server-sdk-go.md in the hashids-note shape BEFORE creating the chatvideo repo or running go get. Confirm the pin with go list -m -versions github.com/livekit/server-sdk-go/v2 (expect v2.19.0). The official module path is github.com/livekit/server-sdk-go/v2 — not the v1 path. Record: v2.19.0; Apache-2.0; transitive livekit/protocol, twirp, protobuf; flag pion, media-sdk, buildkit (Pitfall 9); production talks to RoomAPI only; tests ship a fake and must not construct lksdk.Room; HTTP to the operator URL uses fetchguard TrustedMode; token claims golden vs PHP LiveKitService.php 80-99; if protocol/auth ToJWT differs, mint with jwt/v5 MapClaims inside the same wrapper. Do not reopen the library choice. + + test -f .planning/notes/2026-10-18-livekit-server-sdk-go.md && grep -q 'livekit/server-sdk-go/v2' .planning/notes/2026-10-18-livekit-server-sdk-go.md && grep -q 'v2.19.0' .planning/notes/2026-10-18-livekit-server-sdk-go.md && echo livekit-note-ready + "livekit-note-ready" absent or the note lacks /v2 or v2.19.0 + + + - The note flags pion or media-sdk. + - The note names RoomAPI and TrustedMode. + - The note names jwt/v5 as the golden fallback. + + The LiveKit note exists and no chatvideo go.mod yet requires the SDK. + + + + Task 2: End-to-end video start — unique active session, token golden, authorizer stranger deny + D-02 chatvideo baseline and D-05 /_chatvideo/api/v1 become the shared contract. + Task 1 printed livekit-note-ready. ../sm-chat-plugin exists. ../sm-chatvideo-plugin does not already exist as a foreign module. + ../sm-chatvideo-plugin/go.mod, ../sm-chatvideo-plugin/go.work, ../sm-chatvideo-plugin/go.sum, ../sm-chatvideo-plugin/.gitignore, ../sm-chatvideo-plugin/README.md, ../sm-chatvideo-plugin/plugin.go, ../sm-chatvideo-plugin/boot.go, ../sm-chatvideo-plugin/routes.go, ../sm-chatvideo-plugin/config/config.yaml, ../sm-chatvideo-plugin/lang/en/lang.yaml, ../sm-chatvideo-plugin/lang/pl/lang.yaml, ../sm-chatvideo-plugin/classes/livekit.go, ../sm-chatvideo-plugin/classes/authorizer.go, ../sm-chatvideo-plugin/classes/video_service.go, ../sm-chatvideo-plugin/models/session.go, ../sm-chatvideo-plugin/models/participant.go, ../sm-chatvideo-plugin/models/registry.go, ../sm-chatvideo-plugin/api/auth.go, ../sm-chatvideo-plugin/api/errors.go, ../sm-chatvideo-plugin/api/json.go, ../sm-chatvideo-plugin/api/video.go, ../sm-chatvideo-plugin/updates/registry.go, ../sm-chatvideo-plugin/updates/202610180001_create_golem15_chatvideo_baseline.go, ../sm-chatvideo-plugin/updates/baseline_test.go, ../sm-chatvideo-plugin/testmain_test.go, ../sm-chatvideo-plugin/harness_test.go, ../sm-chatvideo-plugin/fixture_plugin_test.go, ../sm-chatvideo-plugin/http_tracer_test.go + ../sm-chat-plugin/classes/authorizer.go, ../sm-chat-plugin/boot.go, ../sm-chat-plugin/routes.go, ../sm-chat-plugin/http_tracer_test.go, modules/lighthouse/channel.go, modules/fetchguard/policy.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/LiveKitService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/VideoChannelAuthorizer.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/models/VideoSession.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/updates/, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/config/config.php, .planning/notes/2026-10-18-livekit-server-sdk-go.md + Only after the note exists, create ../sm-chatvideo-plugin and go get github.com/livekit/server-sdk-go/v2@v2.19.0. Analog pins; replace summercms and sm-chat-plugin and sm-user-plugin. Requires golem15.chat. + +plugin.go: package chatvideo, PluginID golem15.chatvideo, HasRoutes, HasJobs, HasCommands, HasSchedule (schedule in Task 3), BucketProvider. README H1 Chat video. + +Baseline 202610180001_create_golem15_chatvideo_baseline: sessions and participants from RESEARCH. unique active_conversation_id uniq_chatvideo_active_conversation; join_code varchar 12 unique; status pending|active|ended; no updated_at; comment demo-seed v1.0.1. Model CREATED_AT=started_at. TestBaseline greps both tables. + +RoomAPI interface as RESEARCH. Production client: NewRoomServiceClient with HTTP through fetchguard TrustedMode (operator URL, may be loopback). Admin URL: livekit_admin_url else rewrite wss/ws to https/http. DeleteRoom swallows missing-room (D-13). ParticipantCount missing-room returns 0. Tests: in-memory fake; do not import the WebRTC stack the note flagged; do not construct the realtime room client. + +Token: HS256 claims matching PHP (iss API key, sub user id string, name, iat/nbf/exp, nested video grant, optional metadata). Prefer jwt/v5 MapClaims if SDK ToJWT fails the golden. TestLiveKitTokenClaims (short). + +Authorizer (D-15): parseVideoSessionChannel copies parseChatConversationChannel — three segments video, session, digits. Do not call the lighthouse segment-1 helper (Pitfall 1: it would read session and yield 0). Allow only participants of the session's conversation; capabilities prs. Boot Registry.Register("video", ...). Do not re-register the user namespace. TestVideoAuthorizer (short): participant allowed, stranger denied. + +VideoService.Start: create LiveKit room, persist session with unique active_conversation_id, invite participants, return token. video-initiate bucket 5/min on POST .../video/start only. Paths /_chatvideo/api/v1/conversations/{id}/video/start and /join (join in this tracer enough to mint a token). jwt.auth + general chatvideo-api 120/min on the rest. + +TestHTTPTracer (Docker): two conversation members; start; second start on the same conversation fails the unique active index or returns the existing session; fake RoomAPI saw CreateRoom. + +Remote: stay local unless plan 01 chose proceed-and-push; never force-push. + + go -C ../sm-chatvideo-plugin vet ./... && go -C ../sm-chatvideo-plugin test -count=1 -short ./... + non-zero exit, or "ok" lines absent, or a package reports FAIL + docker info >/dev/null 2>&1 && go -C ../sm-chatvideo-plugin test -count=1 -run '^TestHTTPTracer$' -v . + non-zero exit: no Docker daemon (record NOT RUN), or TestHTTPTracer missing/fails, or two actives exist for one conversation + + + - grep -n 'livekit/server-sdk-go/v2' ../sm-chatvideo-plugin/go.mod prints one line. + - grep -n 'parseVideoSessionChannel' ../sm-chatvideo-plugin/classes/authorizer.go prints one line. + - grep -n 'video-initiate' ../sm-chatvideo-plugin/routes.go prints at least one line. + - grep -n 'ChannelID(' ../sm-chatvideo-plugin/classes/authorizer.go prints 0 lines. + - grep -n 'pion' ../sm-chatvideo-plugin/*_test.go ../sm-chatvideo-plugin/classes/*_test.go prints 0 lines. + + A member can start one active session; the token golden matches PHP; a stranger is denied video:session:{id}. + + + + Task 3: leave/end/status/call-links, generic reconciler, realtime events + ../sm-chatvideo-plugin/classes/video_service.go, ../sm-chatvideo-plugin/classes/notifier.go, ../sm-chatvideo-plugin/jobs/reconcile.go, ../sm-chatvideo-plugin/console/reconcile.go, ../sm-chatvideo-plugin/schedule.go, ../sm-chatvideo-plugin/api/video.go, ../sm-chatvideo-plugin/api/call_links.go, ../sm-chatvideo-plugin/routes.go, ../sm-chatvideo-plugin/plugin.go, ../sm-chatvideo-plugin/boot.go, ../sm-chatvideo-plugin/README.md + ../sm-chatvideo-plugin/classes/video_service.go, ../sm-chat-plugin/classes/emit.go, ../sm-chat-plugin/jobs/maintain_thread_summary.go, ../sm-paymentgateway-plugin/schedule.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/VideoCallNotifier.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/console/ReconcileVideoSessions.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityVideoController.php + Per D-05, add leave, end, status on /_chatvideo/api/v1/conversations/{id}/video/{leave,end,status} and GET /_chatvideo/api/v1/call-links/{code} with bucket call-link 10/min. join_code alphabet abcdefghijklmnopqrstuvwxyz0123456789 length 10. Uniform 404 for unknown codes. + +Per D-15, notifier publishes video:call:incoming to user:{id}; accepted/declined/ended and participant joined/left to video:session:{id}. Payload keys from RESEARCH. Publish failure is logged and not raised. Do not port host close-push extras. + +Per D-14, Reconcile: if the LiveKit room is missing or empty, or started_at + max_duration elapsed, end the session, DeleteRoom (swallowed), emit video:call:ended. Command chatvideo:reconcile-sessions. Job Kind golem15.chatvideo.reconcile_sessions, MaxAttempts 1. Plugin.Schedule pact.Every(5 * time.Minute) on that command (5 minutes divides 24h). Keep only empty-or-gone room and max_duration. Drop host-only extras (guest rooms, chat cards, grace seconds) and the host command name. + +After APIs exist, note in the SUMMARY that execute should run the security-review agent (CONTEXT). Do not spawn it from this task. + + go -C ../sm-chatvideo-plugin vet ./... && go -C ../sm-chatvideo-plugin test -count=1 -short ./... + non-zero exit or a package reports FAIL + + + - grep -n 'chatvideo:reconcile-sessions' ../sm-chatvideo-plugin/console/reconcile.go prints one line. + - grep -n 'golem15.chatvideo.reconcile_sessions' ../sm-chatvideo-plugin/jobs/reconcile.go prints one line. + - grep -n 'Every(5' ../sm-chatvideo-plugin/schedule.go prints one line. + - grep -n 'GuestCall\|CallNotifier\|start_grace_seconds' ../sm-chatvideo-plugin/*.go ../sm-chatvideo-plugin/classes/*.go ../sm-chatvideo-plugin/jobs/*.go prints 0 lines. + - grep -n 'call-links' ../sm-chatvideo-plugin/routes.go prints at least one line. + + Leave/end/status and call-links work; the 5-minute reconciler ends empty or expired rooms only. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| JWT actor → /_chatvideo/api/v1 | conversation id and join_code | +| Token mint → client | LiveKit HS256 join token is a capability grant | +| Authorizer → subscribe | client-chosen video:session:{id} | +| Operator URL → LiveKit | TrustedMode only (not user-supplied) | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.1-03 | Elevation of privilege | classes/livekit.go token mint | high | mitigate | sub = authenticated user id string; secret server-side; TestLiveKitTokenClaims | +| T-19.1-05 | Elevation of privilege | classes/authorizer.go | high | mitigate | Split segment 2; participants only; prs; TestVideoAuthorizer | +| T-19.1-09 | Information disclosure | api/call_links.go | medium | mitigate | call-link 10/min; uniform 404 | +| T-19.1-SC | Tampering | go.mod livekit SDK | high | mitigate | Note-first /v2 v2.19.0; tests fake RoomAPI | + + + + +The note exists before go get. go -C ../sm-chatvideo-plugin test -short stays green including TestLiveKitTokenClaims and TestVideoAuthorizer. TestHTTPTracer proves one active session. + + + +sm-chatvideo-plugin exists with RoomAPI, a correct video authorizer, generic start/join/leave/end/status/call-links, and a generic 5-minute reconciler. Host-only GuestCall and call cards are absent. + + + +Create `.planning/phases/19.1-chat-satellites/19.1-05-SUMMARY.md` when done + diff --git a/.planning/phases/19.1-chat-satellites/19.1-06-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-06-PLAN.md new file mode 100644 index 00000000..70acc0cb --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/19.1-06-PLAN.md @@ -0,0 +1,239 @@ +--- +phase: 19.1-chat-satellites +plan: 06 +type: execute +wave: 3 +depends_on: [19.1-01, 19.1-02, 19.1-03, 19.1-04, 19.1-05] +files_modified: + - ../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md + - ../sm-userfriends-plugin/testdata/php_methods.txt + - ../sm-userfriends-plugin/php_method_map_test.go + - ../sm-userfriends-plugin/golden_test.go + - ../sm-userfriends-plugin/testdata/goldens/ + - ../sm-forum-plugin/testdata/PHP_METHOD_MAP.md + - ../sm-forum-plugin/testdata/php_methods.txt + - ../sm-forum-plugin/php_method_map_test.go + - ../sm-forum-plugin/golden_test.go + - ../sm-forum-plugin/testdata/goldens/ + - ../sm-chatbots-plugin/testdata/PHP_METHOD_MAP.md + - ../sm-chatbots-plugin/testdata/php_methods.txt + - ../sm-chatbots-plugin/php_method_map_test.go + - ../sm-chatbots-plugin/golden_test.go + - ../sm-chatbots-plugin/testdata/goldens/ + - ../sm-herald-plugin/testdata/PHP_METHOD_MAP.md + - ../sm-herald-plugin/testdata/php_methods.txt + - ../sm-herald-plugin/php_method_map_test.go + - ../sm-herald-plugin/golden_test.go + - ../sm-herald-plugin/testdata/goldens/ + - ../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md + - ../sm-chatvideo-plugin/testdata/php_methods.txt + - ../sm-chatvideo-plugin/php_method_map_test.go + - ../sm-chatvideo-plugin/golden_test.go + - ../sm-chatvideo-plugin/testdata/goldens/ + - scripts/check-phase19.1.sh +autonomous: false +requirements: [SC-19.1-1, SC-19.1-4, D-04] +estimate: + tokens: 180000 + raw_tokens: 180000 + tasks: 3 + confidence: low +must_haves: + truths: + - "SC-19.1-4 / D-04: each plugin has testdata/PHP_METHOD_MAP.md plus TestPHPMethodMap; counts are chatbots 22, forum 25, herald 14, userfriends 31, chatvideo 0 in-plugin (Wavepath host methods mapped to generic-API Go or explicit N/A)." + - "Herald EndpointGuard pin/reject rows are not-applicable (D-10) with a pointer to fetchguard tests. Components and demo seeders are not-applicable (D-07)." + - "Goldens cover serializers, lighthouse MemoryDriver publications, LiveKit JWT claims, and forum semantic HTML (normalized DOM, D-18)." + - "scripts/check-phase19.1.sh fail-closed named tests across five sibling roots plus the three decision-note paths; --self-test refuses planted skip/miss/zero output." + artifacts: + - path: "scripts/check-phase19.1.sh" + provides: "phase gate analog to check-phase19.sh" + contains: "USERFRIENDS" + - path: "../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md" + provides: "31 PHP methods mapped" + contains: "test_" + - path: "../sm-chatbots-plugin/testdata/PHP_METHOD_MAP.md" + provides: "22 PHP methods mapped" + contains: "test_" + - path: "../sm-forum-plugin/testdata/PHP_METHOD_MAP.md" + provides: "25 PHP methods mapped" + contains: "test_" + - path: "../sm-herald-plugin/testdata/PHP_METHOD_MAP.md" + provides: "14 PHP methods mapped" + contains: "test_" + - path: "../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md" + provides: "0 in-plugin plus Wavepath generic-or-N/A rows" + contains: "N/A" + key_links: + - from: "scripts/check-phase19.1.sh" + to: "../sm-userfriends-plugin/http_tracer_test.go" + via: "named TestHTTPTracer required in postgres stage" + pattern: "TestHTTPTracer" + - from: "../sm-herald-plugin/testdata/PHP_METHOD_MAP.md" + to: "modules/fetchguard" + via: "EndpointGuard rows not-applicable D-10" + pattern: "not-applicable" + prohibitions: + - statement: "This plan does not re-implement D-17, vue-community-layer, or /_wave host routes." + status: unresolved + verification: judgment + - statement: "Goldens never contain a VAPID private key or LiveKit API secret." + status: unresolved + verification: grep +--- + +## Phase Goal + +**As a** application that mounts the satellites, **I want to** have a fail-closed phase gate and a complete PHP-method map, **so that** every frozen test is ported or explicitly not applicable. + +This plan's slice (plan 6 of 6, wave 3): unit tests last — maps, goldens, `scripts/check-phase19.1.sh`. + +## Decisions taken in this plan (Claude's discretion) + +- Gate env overrides: USERFRIENDS, FORUM, CHATBOTS, HERALD, CHATVIDEO (sibling checkouts). +- Named short tests from RESEARCH/VALIDATION: TestBaseline ×5, TestHTTPTracer ×5, TestAccessResolverDefault, TestLiveKitTokenClaims, TestVideoAuthorizer, TestSubscribeEndpoint, TestNoGolem, TestNoUserfriendsImport, TestPHPMethodMap, TestFixtureAbsentFromProduction. +- Postgres stage: tracers + TestSendEventRollback + GDPR cascades. +- Decision-note paths grepped for locked module path + version. +- Execute should run the security-review agent after this plan (APIs + push + tokens + HTML + GDPR all exist). This planner does not spawn it. +- Visible recorded choice: spec-less probe fallback was skipped in plan 01; no probe predicates here either. + +## Artifacts this phase produces (this plan) + +- Five `testdata/PHP_METHOD_MAP.md` + `php_method_map_test.go`. +- Five `golden_test.go` + `testdata/goldens/`. +- `scripts/check-phase19.1.sh`. + + +Complete D-04 / SC-19.1-4: PHP method maps (chatbots 22, forum 25, herald 14, userfriends 31, chatvideo 0 in-plugin plus Wavepath generic-or-N/A), goldens from Wavepath, and scripts/check-phase19.1.sh. + +Purpose: a skipped or missing named test fails the phase gate. +Output: bash scripts/check-phase19.1.sh --self-test and the named-test stages are green when Docker is present. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md +@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md +@.planning/phases/19.1-chat-satellites/19.1-VALIDATION.md +@.planning/phases/19.1-chat-satellites/COVERAGE.md +@scripts/check-phase19.sh + + +Analog: +- ../sm-chat-plugin/php_method_map_test.go +- ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md +- ../sm-chat-plugin/golden_test.go +- scripts/check-phase19.sh (stages, sibling env, fail-closed detectors) +- Frozen PHP tests under /media/nvme/dev/jz/wavepath.org/plugins/golem15/{chatbots,forum,herald,userfriends}/tests +- Wavepath host tests for chatvideo generic behaviors + + + + + + + Task 1: End-to-end phase gate — check-phase19.1.sh self-test plus one named map + Plans 01-05 have created the five sibling plugin roots with TestHTTPTracer and TestBaseline names. + scripts/check-phase19.1.sh, ../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md, ../sm-userfriends-plugin/testdata/php_methods.txt, ../sm-userfriends-plugin/php_method_map_test.go + scripts/check-phase19.sh, ../sm-chat-plugin/php_method_map_test.go, ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md, .planning/phases/19.1-chat-satellites/19.1-VALIDATION.md + Copy scripts/check-phase19.sh shape into scripts/check-phase19.1.sh. Stages: self-test, plugins, docs, postgres. Sibling env USERFRIENDS / FORUM / CHATBOTS / HERALD / CHATVIDEO defaulting to $ROOT/../sm-<name>-plugin. Fail-closed: a skip, miss, red, or "no tests to run" fails. --self-test plants JSON the detectors must refuse. Named short tests from VALIDATION (TestBaseline, TestHTTPTracer, TestNoGolem, TestNoUserfriendsImport, TestSubscribeEndpoint, TestLiveKitTokenClaims, TestVideoAuthorizer, TestAccessResolverDefault, TestPHPMethodMap, TestFixtureAbsentFromProduction). Postgres: the five tracers plus TestSendEventRollback and GDPR cascade names. Docs stage greps the three note paths for SherClockHolmes/webpush-go, livekit/server-sdk-go/v2, and the forum-policy bluemonday pin. + +Also write userfriends testdata/PHP_METHOD_MAP.md for all 31 PHP test methods (GroupPolicy, GroupVisibilityGate, GroupModels). Components and demo seeders are not-applicable (D-07). php_method_map_test.go copies the chat analog. This tracer proves the gate can require TestPHPMethodMap on one repo. + + bash -n scripts/check-phase19.1.sh && bash scripts/check-phase19.1.sh --self-test + non-zero exit, or --self-test does not refuse planted skip/miss/zero output + go -C ../sm-userfriends-plugin test -count=1 -short -run '^TestPHPMethodMap$' . + non-zero exit, or TestPHPMethodMap missing, or "no tests to run" + + + - grep -n 'USERFRIENDS' scripts/check-phase19.1.sh prints at least one line. + - grep -n 'TestPHPMethodMap' scripts/check-phase19.1.sh prints at least one line. + - wc -l ../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md is greater than 31. + + The phase gate self-test refuses planted failures, and userfriends TestPHPMethodMap is green. + + + + Task 2: Remaining PHP method maps (chatbots 22, forum 25, herald 14, chatvideo 0+Wavepath) + ../sm-chatbots-plugin/testdata/PHP_METHOD_MAP.md, ../sm-chatbots-plugin/testdata/php_methods.txt, ../sm-chatbots-plugin/php_method_map_test.go, ../sm-forum-plugin/testdata/PHP_METHOD_MAP.md, ../sm-forum-plugin/testdata/php_methods.txt, ../sm-forum-plugin/php_method_map_test.go, ../sm-herald-plugin/testdata/PHP_METHOD_MAP.md, ../sm-herald-plugin/testdata/php_methods.txt, ../sm-herald-plugin/php_method_map_test.go, ../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md, ../sm-chatvideo-plugin/testdata/php_methods.txt, ../sm-chatvideo-plugin/php_method_map_test.go + ../sm-userfriends-plugin/php_method_map_test.go, ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/tests, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/tests, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/tests, .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (Last-plan PHP-test-method mapping) + Per D-04, write one map per remaining plugin. Extract `function test` names from the frozen PHP trees. Status ported or not-applicable with the D-id. + +chatbots 22: all port (registry, dispatcher, help, deny-all, RODO, boundary, additivity). +forum 25: all port; markdown goldens consume D-17. +herald 14: port except EndpointGuard pin/reject → not-applicable (D-10) plus a pointer to fetchguard tests. +chatvideo 0 in-plugin: map Wavepath CommunityChannelVideoTest, CommunityCallLinkTest, CommunityVideoSeverTest, CallNotificationLifecycleTest, GuestCallLinkTest, GuestLiveKitGatewayTest (and any other video host tests found) to generic-API Go or explicit N/A for guest rooms, call cards, and host close-push. + +Each repo gets php_method_map_test.go analog chat. Do not edit sm-user-plugin. + + go -C ../sm-chatbots-plugin test -count=1 -short -run '^TestPHPMethodMap$' . && go -C ../sm-forum-plugin test -count=1 -short -run '^TestPHPMethodMap$' . && go -C ../sm-herald-plugin test -count=1 -short -run '^TestPHPMethodMap$' . && go -C ../sm-chatvideo-plugin test -count=1 -short -run '^TestPHPMethodMap$' . + non-zero exit, any TestPHPMethodMap missing, or "no tests to run" + + + - Each testdata/PHP_METHOD_MAP.md exists. + - grep -c 'not-applicable (D-10)' ../sm-herald-plugin/testdata/PHP_METHOD_MAP.md prints at least 1. + - grep -c 'N/A' ../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md prints at least 1. + + Every frozen PHP test method has a Go case or an explicit not-applicable row. + + + + Task 3: Goldens and human capture of Wavepath payloads + ../sm-userfriends-plugin/golden_test.go, ../sm-userfriends-plugin/testdata/goldens/, ../sm-forum-plugin/golden_test.go, ../sm-forum-plugin/testdata/goldens/, ../sm-chatbots-plugin/golden_test.go, ../sm-chatbots-plugin/testdata/goldens/, ../sm-herald-plugin/golden_test.go, ../sm-herald-plugin/testdata/goldens/, ../sm-chatvideo-plugin/golden_test.go, ../sm-chatvideo-plugin/testdata/goldens/, scripts/check-phase19.1.sh + ../sm-chat-plugin/golden_test.go, ../sm-chat-plugin/testdata/goldens/, .planning/phases/19.1-chat-satellites/19.1-VALIDATION.md, .planning/phases/19.1-chat-satellites/COVERAGE.md + Per D-04 and D-18, add golden_test.go analog chat in each plugin. Capture serializer payloads, lighthouse MemoryDriver.Publications() where the plugin emits, LiveKit JWT claims (chatvideo), and forum semantic HTML (normalized DOM — same tags, attributes, text; not byte-identical). Forum corpus covers script, javascript: scheme, iframe, onerror, GFM tables, code blocks, links, autolinks. Adopted content_html is served stored. + +If a live PHP Wavepath backend is reachable, record payloads into testdata/goldens/. If not, derive fixtures from the PHP tests and mark the SUMMARY "goldens from PHP fixtures; live capture NOT RUN". Never store VAPID private material or LiveKit secrets in goldens. + +Add any missing named tests the gate requires. Record in the SUMMARY that execute should run the security-review agent now that the five APIs, push sender, token issuer, sanitizer boundary, and GDPR cascades exist. Do not spawn that agent here. + + go -C ../sm-userfriends-plugin test -count=1 -short ./... && go -C ../sm-forum-plugin test -count=1 -short ./... && go -C ../sm-chatbots-plugin test -count=1 -short ./... && go -C ../sm-herald-plugin test -count=1 -short ./... && go -C ../sm-chatvideo-plugin test -count=1 -short ./... + non-zero exit or a package reports FAIL + bash scripts/check-phase19.1.sh plugins + non-zero exit, or a required named short test is skipped, missing, or red + Approve goldens against Wavepath if the PHP backend was available; otherwise confirm the SUMMARY records NOT RUN and PHP-fixture provenance. + + + - Each plugin has testdata/goldens/ with at least one json or html file. + - grep -n 'private_key\|API_SECRET' ../sm-*/testdata/goldens/* prints 0 lines (or those files do not exist). + - scripts/check-phase19.1.sh plugins exits 0 on a Docker-free host. + + Goldens and the phase gate cover all five plugins; secrets are absent from fixtures. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Golden fixtures → repo | captured payloads must not include secrets | +| Phase gate → CI | a skip must not count as green | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.1-12 | Information disclosure | testdata/goldens/ | medium | mitigate | No VAPID private key or LiveKit secret in fixtures; grep gate | +| T-19.1-SC | Tampering | scripts/check-phase19.1.sh | high | mitigate | Fail-closed detectors; --self-test plants skip/miss/zero | + +After this plan, execute runs the security-review agent on the five generic APIs, the push sender, the LiveKit token issuer, the HTML sanitizer boundary, and the GDPR cascades (CONTEXT). This planner does not spawn it. + + + + +bash scripts/check-phase19.1.sh --self-test and plugins stay green. Five TestPHPMethodMap names pass. Goldens exist without secrets. + + + +SC-19.1-4 is true: unit tests, PHP maps, goldens, and the phase gate exist. Every D-04 PHP method is ported or explicitly not applicable. + + + +Create `.planning/phases/19.1-chat-satellites/19.1-06-SUMMARY.md` when done + diff --git a/.planning/phases/19.1-chat-satellites/COVERAGE.md b/.planning/phases/19.1-chat-satellites/COVERAGE.md new file mode 100644 index 00000000..95a9c6df --- /dev/null +++ b/.planning/phases/19.1-chat-satellites/COVERAGE.md @@ -0,0 +1,72 @@ +# Phase 19.1 — Third-party API coverage + +Full-coverage-by-default for the two surfaces this phase integrates. Every OPT-OUT has a one-line reason. Guest call links, CallNotifier, and Wavepath `/_wave` host routes are out of scope (CONTEXT deferred; D-07 / D-14). + +**Libraries:** `github.com/SherClockHolmes/webpush-go` v1.4.0 (D-09); `github.com/livekit/server-sdk-go/v2` v2.19.0 RoomService (D-13). + +**Status:** COVER = production or test code in this phase must call or wrap it. OPT-OUT = explicitly not used. + +## webpush-go v1.4.0 + +| Symbol | Status | Plan | Reason | +|--------|--------|------|--------| +| `HTTPClient` (`Do(*http.Request)`) | COVER | 04 | `Options.HTTPClient` is `*fetchguard.Client` (D-10); no wrapper type | +| `Options` | COVER | 04 | TTL 2419200, urgency `normal`, VAPID keys, Subscriber, HTTPClient | +| `Options.HTTPClient` | COVER | 04 | PublicOnly send path | +| `Options.RecordSize` | OPT-OUT | — | PHP PushSender never set a record-size cap | +| `Options.Subscriber` | COVER | 04 | `golem15.herald.vapid.subject` (default `mailto:admin@example.com`) | +| `Options.Topic` | OPT-OUT | — | PHP never set Topic / collapse | +| `Options.TTL` | COVER | 04 | PHP default 2419200, overridable per send (D-11) | +| `Options.Urgency` | COVER | 04 | PHP default `normal`, overridable per send | +| `Options.VAPIDPublicKey` | COVER | 04 | config `golem15.herald.vapid.public_key` | +| `Options.VAPIDPrivateKey` | COVER | 04 | config `golem15.herald.vapid.private_key`; never logged | +| `Options.VapidExpiration` | OPT-OUT | — | library default (now+12h) matches PHP VAPID JWT window | +| `Keys` | COVER | 04 | stored `{p256dh, auth}` on `golem15_herald_push_subscriptions.keys` | +| `Subscription` | COVER | 04 | endpoint + Keys from the subscription row | +| `SendNotification` | OPT-OUT | — | unused; send always has a request context | +| `SendNotificationWithContext` | COVER | 04 | the only send entry; prune on 404/410 | +| `GenerateVAPIDKeys` | COVER | 04 | `summer herald:vapid-generate` prints env lines | +| Urgency constants (`very-low`/`low`/`normal`/`high`) | COVER | 04 | default `normal`; others accepted when the caller overrides | +| `flare` / `push.*` / `websockets:generate-vapid-keys` | OPT-OUT | — | D-09 locked webpush-go; different config and command names | + +## LiveKit RoomService (`server-sdk-go/v2` v2.19.0 `RoomServiceClient`) + +Production talks to the plugin `RoomAPI` interface only. The official client implements it. Tests ship a Twirp-free fake and must not import pion or construct `lksdk.Room`. + +| Symbol | Status | Plan | Reason | +|--------|--------|------|--------| +| `NewRoomServiceClient` | COVER | 05 | production constructor; HTTP via fetchguard TrustedMode | +| `CreateRoom` | COVER | 05 | start session; room name `chat-conversation-{id}-{16 hex}` | +| `ListRooms` | COVER | 05 | `RoomAPI.ActiveRoomNames` for the reconciler | +| `DeleteRoom` | COVER | 05 | end/reconcile; missing-room is a no-op (D-13) | +| `ListParticipants` | COVER | 05 | `RoomAPI.ParticipantCount` (missing-room → 0) | +| `GetParticipant` | OPT-OUT | — | PHP LiveKitService never fetched one participant | +| `RemoveParticipant` | COVER | 05 | leave path; PHP LiveKitService::removeParticipant | +| `ForwardParticipant` | OPT-OUT | — | not in PHP LiveKitService | +| `MoveParticipant` | OPT-OUT | — | not in PHP LiveKitService | +| `MutePublishedTrack` | OPT-OUT | — | PHP never muted tracks from the plugin | +| `UpdateParticipant` | OPT-OUT | — | not in PHP LiveKitService | +| `UpdateSubscriptions` | OPT-OUT | — | not in PHP LiveKitService | +| `UpdateRoomMetadata` | OPT-OUT | — | not in PHP LiveKitService | +| `SendData` | OPT-OUT | — | realtime data goes through lighthouse, not LiveKit data messages | +| `CreateToken` / `auth.NewAccessToken` / `ToJWT` | COVER | 05 | join JWT; golden vs PHP claims; jwt/v5 MapClaims if SDK grant JSON differs | +| `NewLiveKitAPI` / `LiveKitAPI.Room` | OPT-OUT | — | `NewRoomServiceClient` is enough; extra facade unused | +| `LiveKitAPI.Egress` | OPT-OUT | — | recording/streaming is not in the PHP plugin | +| `LiveKitAPI.Ingress` | OPT-OUT | — | not in the PHP plugin | +| `LiveKitAPI.SIP` | OPT-OUT | — | not in the PHP plugin | +| `LiveKitAPI.AgentDispatch` | OPT-OUT | — | not in the PHP plugin | +| `LiveKitAPI.Connector` | OPT-OUT | — | not in the PHP plugin | +| `ConnectToRoom` / `lksdk.Room` | OPT-OUT | — | realtime participant client; plugin is RoomService-only | +| pion / media-sdk / buildkit imports in tests | OPT-OUT | — | flag in the decision note; tests use `RoomAPI` fake | +| Guest call links / `GuestCall` | OPT-OUT | — | Wavepath host; D-14 generic reconciler only | +| CallNotifier / call cards / `start_grace_seconds` | OPT-OUT | — | Wavepath host; not in chatvideo config | +| Wavepath `/_wave` video routes | OPT-OUT | — | deferred to the Wavepath app port | + +## Host surfaces explicitly out of scope + +| Surface | Reason | +|---------|--------| +| Wavepath `/_wave/api/v1/community/*` and `/_wave/api/v1/push/*` | Deferred Ideas; generic `/_x/api/v1` is the published contract (D-05) | +| Host seams (`WaveForumAccessResolver`, `WaveBotAuthResolver`, `WaveHeraldProfile`, `WavePresenceGate`, `CommunityVideoService`) | App wiring, not satellite code | +| `vue-community-layer` | Deferred Ideas | +| D-17 `Render` / `Purify` implementation | Separate `/msd-quick`; forum consumes, does not vendor |