The ubuntu-22.04-arm runner image does not preinstall xdg-utils, so
AppImage bundling fails with 'xdg-open binary not found'. Add it to
every Linux system-dependency list in ci.yml and release.yml (the
release ARM job shares the same list and would have failed identically
at tag time).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Network' is not a tauri-bundler AppCategory, so every bundling run
failed with 'invalid category' after compilation succeeded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bump crossbeam-epoch 0.9.20, quinn-proto 0.11.16, rustls-webpki
0.103.13 in Cargo.lock (semver-compatible; RUSTSEC-2026-0204, -0185,
-0098, -0099, -0104 and the second quick-xml instance resolved).
quick-xml 0.37 remains pinned by tauri-winrt-notification 0.7 (via
tauri-plugin-notification) with no compatible route to the fixed 0.41;
it only parses toast XML the library itself builds, never
attacker-controlled input, so RUSTSEC-2026-0194/-0195 are ignored in
.cargo/audit.toml with removal criteria documented inline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
readPump teardown always ran handleVoiceLeave, trusting the joined_at
guard in LeaveVoiceChannelIfMatch to protect replacement sessions. On
reconnect the voice session TRANSFERS to the replacement client with
the same joined_at, so the guard cannot tell the two apart: whenever
teardown snapshotted voiceChID before the transfer zeroed it, the old
connection deleted the replacement's voice_state row (flaked on the
Windows CI runner as TestServeWS_Reconnect_PreservesVoiceState).
Gate voice cleanup on !replaced — the same condition the
presence-offline broadcast four lines down already uses. A genuinely
final disconnect behaves exactly as before, and a stale row from a
crashed replacement is still swept by the fresh-connect cleanup.
Also deflake TestHub_ConcurrentRegisterUnregister: poll for quiescence
with a deadline instead of a fixed 50ms sleep that loses to the -race
scheduler on slow runners.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- cfg-gate open_devtools entirely behind the devtools feature (its
registration in lib.rs already was), removing the dead-code warning
in non-devtools builds
- drop let-bindings of unit-returning store.set in the three cert
rollback paths (let_unit_value)
- remove a needless borrow on the startup-error dialog description
(only compiled on non-Linux, hence Windows-only finding)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
golangci-lint had been failing invisibly behind the earlier CI gate
failures. Default-build lint is now clean:
- Delete the unused pre-topic-limiter rate-limit constants, the unused
bluemonday sanitizer, and the dead broadcast variants superseded by
their Low/High counterparts (broadcastExclude,
broadcastToDMParticipants(+Exclude), sendSequencedToUsers,
PubSub.debugDump). Test references were comments only; updated to
name the live variants.
- Separate 'Phase X Step Y' file headers from the package clause with a
blank line so staticcheck ST1000 no longer reads them as malformed
package comments (proper package docs exist in hub.go/manifest.go).
- Add .gitattributes normalizing line endings to LF on checkout —
the Windows CI runner materialized CRLF, which made every
prettier-formatted file fail the format gate.
Known remainder (pre-existing, out of P0 scope): golangci-lint with
-tags wazero reports 3 gosec + 2 staticcheck and -tags otel 1+1; CI
lints the default build. Tracked for the P1 plugin pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanical prettier --write; 297 files had drifted while the CI
format gate was dead. No functional changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove the unused 'type Event' named import from the committed
generated events.ts and teach the CI patch step to strip it on future
regenerations — the previous line-anchored patch deliberately skipped
imports, so ESLint failed on every run.
- Split vitest into its own client-tests job so the known-red suite
(P2 triage pending) is exactly one visible failing check instead of
masking the audit/lint/typecheck/prettier gates, which are now green.
- Drop the three stale roadmap files (PHASE_BC_LOCAL_TODO.md,
phase-b-acceleration.md, phase-c-differentiation.md) — referenced
nowhere since the CHANGELOG cleanup; already deleted on the
security-hardening branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
db/queries/sqlite/events.sql was changed (typed CAST for
GetMaxEventSeq) without re-running generation; commit the regenerated
dbgen (interface{} -> int64, no callers depend on the old signature).
Scope sqlc-verify's diff to db/dbgen: regeneration strips the
hand-added //go:build postgres tags in db/pgdbgen, so verifying that
tree can never pass; pgdbgen is scheduled for removal with the
Postgres scaffolding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
newEmitTestHub and NewHubForTest built raw Hub literals without the
topic rate limiter, so deliverBroadcast panicked on a nil receiver and
TestEmitEvents_ChannelEvent_CallsBroadcastToChannel could never pass.
CI never surfaced it because the pipeline died at govulncheck first.
Wire the limiter exactly as NewHub does; no production nil-guard, since
a nil limiter in production would silently disable rate limiting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server: golang.org/x/net 0.52.0->0.55.0 (GO-2026-5026, reachable via
autocert.HostWhitelist) and go-jose/v3 3.0.4->3.0.5 (GO-2026-4945,
reachable via the LiveKit SDK). govulncheck now passes clean; full
test suite and the otel/wazero build-tag matrix verified locally.
Client: npm audit fix (semver-compatible only, 44 packages) — npm audit
--audit-level=high now exits clean. vitest moved 3.2.4->3.2.7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- gitignore .serena/ and Client/tauri-client/.env; untrack the .env
(the KLIPY key it held is treated as burned; rotation + server-side
proxy tracked for P3)
- CI: verify generated sqlc output (make sqlc-verify) on the ubuntu leg
- CHANGELOG: honest reset narrative (v1.1.0-alpha series), remove
references to deleted roadmap files
- delete stale docs/phase-a-status.md; fix dangling ref in
docs/plans/slash-commands.md
- add root SECURITY.md (GitHub-surfaced policy; reporting works while
the source repo is private)
- docs/audit-2026-04-07.md: add maintained finding-closure table
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publish every release to the public OwnCord-releases repo with a full
source snapshot (AGPL section 6 for binary recipients), and make the
updater's repo coordinates configurable (github.owner/github.repo),
defaulting to OwnCord-releases. Both the server self-update and the
/client-update chain follow the new default, so deployed servers keep
updating after the source repo goes private.
The publish step fails closed: a private source repo with no
RELEASES_REPO_TOKEN aborts the release instead of silently shipping
binaries with no public source or update feed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TypeScript reports TS5101 for `baseUrl`, which is deprecated and will
stop functioning in TS 7.0. The `paths` entries already use relative
`./src/*` patterns, so `baseUrl` is unnecessary and can be dropped
without changing module resolution.
https://claude.ai/code/session_01RrgSn7AUsGYVPRthnudVMr
The Client Typecheck & Test CI job was failing on the prettier format
check. Two real root causes, fixed properly:
1. prettier endOfLine was set to 'crlf' but the repo stores files with
LF (no .gitattributes forcing eol), so 'prettier --check' failed on
292 files on the Linux CI runner. Set endOfLine to 'lf' to match the
on-disk reality. Also reformat the 2 files (pluginBridge.ts,
solidAdapter.ts) that had genuine style issues.
2. 15 'eslint-disable-next-line' comments targeted oxlint-only rules
(no-await-in-loop, no-unassigned-vars) that ESLint does not enable,
so ESLint reported them as 'Unused eslint-disable directive'
warnings. Switched the directive prefix to 'oxlint-disable-next-line'
— oxlint still honors them (its native syntax), and ESLint no longer
parses them as eslint directives, so the warnings are gone without
suppressing the safety check or removing the directives that oxlint
actually relies on.
Verified locally: oxlint, tsc --noEmit, eslint, prettier --check, and
npm audit --audit-level=high all exit 0.
contextcheck: add ctx context.Context as first param to ListVisibleChannels,
BlockUser, CreateDM, CreateInvite, UpdateProfile, SendMessage; pass r.Context()
from HTTP handlers and ctx from WS handler; replace context.Background() in
telemetry spans with the propagated ctx.
errcheck: suppress justified Close() errors — defer func(){ _ = rows.Close() }()
in sqlite_events.go (idiomatic; rows.Err() checked), _ = resp.Body.Close() in
host_http.go (body fully consumed), _ = f.Close() in host_ui.go (read-only fd).
gocritic/rangeValCopy: rewrite for _, ch := range all (line 60) to indexed loop
in ChannelService.ListVisibleChannels to avoid 144-byte per-iteration copy.
Resolves the parallel Phase B/C work that landed on the sister branch
while this branch was in review. Both branches independently implemented
real OTel + Wazero runtimes; this merge keeps the best of each.
Conflict resolution
- Server/plugin/sandbox_wazero.go: rewritten as a hybrid. Keeps the
HEAD lifecycle (eager `platformInit` with WASI preview-1, explicit
`platformDeactivate` per-instance, runtime closed via Registry.Close)
AND adopts the sister branch's richer artefacts:
* `WithMemoryLimitPages` actually enforces `cfg.MaxMemoryMB`,
* the JSON-over-linear-memory ABI
(`allocate` / `command_dispatch(ptr,len) → (ptr,len)` /
`deallocate`),
* `listExportedCommands` auto-binds commands the plugin exports
via `list_commands` at activation time (capability-gated).
- Server/plugin/registry.go: kept the HEAD `activate()` snapshot
pattern (read `runtimePlatform` under RLock, pass into
`activateWithRuntime` as a parameter) so a concurrent Close can't
race the wazero call. Sister branch's LoadAll stale-staging cleanup
and UninstallPlugin on-disk dir removal came in via auto-merge.
- Server/telemetry/telemetry_otel.go: kept the HEAD implementation
(race-fixed AppMetrics rebind, uint64 overflow guard, idempotent
shutdown, trace-provider cleanup on prom failure) and wired in the
sister branch's `OTLPInsecure` config field for plaintext gRPC opt-in.
- Server/go.mod: accepted sister branch's `BurntSushi/toml v1.6.0`
for the new TOML manifest support.
- Client/tauri-client/vitest.config.ts: union of both globs
(`tests/**/*.test.ts`, `src/**/*.test.ts`, `src/**/*.test.tsx`).
- PHASE_BC_LOCAL_TODO.md: combined the two checkbox histories;
TOML manifest, hello.wasm fixture, and OTLPInsecure are all marked
done now.
Sister branch additions accepted via auto-merge
- Server/plugin/examples/hello/{hello.wasm,main.go}: precompiled 925
KiB TinyGo plugin with the full ABI (allocate, deallocate,
list_commands, command_dispatch, on_event).
- Server/plugin/manifest_{toml,nottoml}.go: TOML manifest parser
behind the wazero build tag, JSON fallback elsewhere.
- Server/plugin/loader.go: prefers `plugin.toml`, falls back to
`plugin.json`.
- Server/api/plugins_handler.go: structured error responses + slog.
- Server/main.go, Server/config/config.go: OTLPInsecure plumbing,
defaults polish.
- docs/{contributing.md,server-configuration.md}: documentation
updates.
Test status
- `go build` passes on default, -tags otel, -tags wazero, and
-tags otel,wazero.
- `go vet` passes on every tag combination.
- `go test ./...` passes on default and on -tags otel,wazero.
- Client: `npx tsc --noEmit` clean; vitest 3188/3188 across 112 files.
https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
- sandbox_wazero.go: explicitly discard plugin stdin via WithStdin to
prevent WASM modules from reading the server process's stdin fd
- channel_handler.go: replace ErrInternal message pass-through with
generic 'an internal error occurred' — full error stays server-side
in slog.Error only
- invite_handler_test.go: update assertions to expect generic message
- service/user.go: fix ChangePassword docstring (no old-password verification)
- service/user.go: RevokeSession now maps db.ErrNotFound→ErrNotFound and
all other store errors→ErrInternal, preventing internal failures from
masquerading as 404s
- plugin/loader.go: update scanPluginDirectory comment to reflect fail-fast
behavior; fix Lstat comment wording
- db/queries/sqlite/events.sql: CAST COALESCE result to INTEGER so sqlc
generates int64 instead of interface{}
- api/plugins_handler.go: log install error server-side and return sanitized
structured JSON response instead of raw err.Error()
- .github/workflows/ci.yml: remove continue-on-error from tag build steps
so tag boundary drift fails CI"
Agent-Logs-Url: https://github.com/J3vb/OwnCord/sessions/6635420c-af26-4cc0-9397-5e5b37887437
Co-authored-by: J3vb <192430104+J3vb@users.noreply.github.com>
Phase B Step 8 (OpenTelemetry) and Phase C Step 9 (Wazero plugin runtime)
were structurally scaffolded but the tagged builds were placeholders that
errored at runtime. This commit lands the real implementations behind the
existing build tags, plus three review passes worth of fixes across the
plugin admin handler, plugin registry, telemetry adapter, and Solid client.
Telemetry (Phase B Step 8)
- Add real go.opentelemetry.io/otel{,/sdk,/exporters/{prometheus,otlp...}}
modules to go.mod plus contrib/instrumentation/net/http/otelhttp.
- Replace the telemetry_otel.go skeleton with a working Provider that
wires Prometheus + OTLP/gRPC exporters, otelhttp middleware, span and
meter adapters, and an idempotent Shutdown.
- AppMetrics cache is now reset *before* SetGlobal to close a race where
a concurrent NewAppMetrics() could observe a swapped provider but read
stale no-op instruments.
- Init releases the trace provider on a later prometheus exporter
failure so Init never leaks gRPC connections.
- convertAttrs handles int32/uint/uint32/uint64/float32 explicitly;
uint64 values that exceed math.MaxInt64 fall back to a STRING attr
rather than wrapping into a negative int64 and corrupting metrics.
- Tests under -tags otel cover the prometheus scrape, span lifecycle,
histogram recording, shutdown idempotency, AppMetrics rebind, and
the uint64 overflow fallback.
Plugin runtime (Phase C Step 9)
- Add github.com/tetratelabs/wazero v1.11.0 to go.mod.
- platformInit creates a shared wazero.Runtime with WASI preview1
pre-instantiated; activateWithRuntime compiles + instantiates each
plugin module under that runtime; platformDeactivate closes per-
plugin modules without tearing down the runtime.
- DisablePlugin now calls platformDeactivate so the wazero module is
freed immediately instead of leaking until registry Close.
- activate() captures runtimePlatform under r.mu.RLock and passes it as
a parameter to activateWithRuntime; the call no longer re-reads the
field, closing a race with concurrent Close.
- invokeCommand calls the plugin's command_dispatch export when
present; missing/broken exports return a user-facing diagnostic
instead of crashing the dispatcher.
- Tests under -tags wazero cover registry creation, module compilation,
re-enable after disable (verifies the leak fix), close-twice safety,
invalid wasm rejection, and DispatchCommand with a missing export.
Fixture is a 41-byte embedded add.wasm; no external asset required.
Plugin admin handler hardening
- /api/v1/admin/plugins/install now rejects uploads whose multipart
Content-Type is not application/zip|x-zip-compressed|octet-stream
(415) and uploads whose body lacks the PK\\x03\\x04 / PK\\x05\\x06
zip magic (400). The 16 MiB cap and registry-side zip-slip / symlink
/ size-bomb defences are still applied as before.
- New plugins_handler_test.go covers list-empty, install-503-when-nil,
content-type rejection, magic rejection, happy path, lifecycle 503,
invalid id, and isZipContentType / hasZipMagic helpers.
Solid client (Phase B Step 6) cleanup
- vitest.config.ts now wires vite-plugin-solid and broadens the test
glob to include src/**/*.test.tsx so Badge.test.tsx is actually
discovered (it was silently skipped).
- pluginBridge.ts targets postMessage at window.location.origin
instead of "*", and exposes a destroy() that detaches the message
listener and clears mounted frames.
- solidMount.ts imports the JSX type from "solid-js" instead of
"solid-js/web" (the latter does not re-export it), unblocking
npx tsc --noEmit.
Build/test status
- go build succeeds on default, -tags otel, -tags wazero, and
-tags otel,wazero.
- go test passes on every tag combination across telemetry, plugin,
api, ws, service, store, and the rest of the tree.
- Client: npx tsc --noEmit clean; vitest 3188/3188 across 112 files.
PHASE_BC_LOCAL_TODO.md is updated to mark the OTel modules + real Init,
the wazero module + real platformInit, and the test coverage that
landed in this commit as completed.
https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
- main.go: TinyGo WASI plugin implementing the OwnCord plugin ABI
(allocate/deallocate, list_commands, command_dispatch, on_event)
- hello.wasm: precompiled binary (TinyGo 0.40.1, Go 1.25.3, wasm-opt 129)
Responds to /hello [name] with a greeting; proof-of-life for wazero runtime
Plugin TOML support (-tags wazero):
- manifest_toml.go: tryLoadPluginTOML using BurntSushi/toml v1.6.0
- manifest_nottoml.go: no-op stub for default build
- loader.go: prefers plugin.toml, falls back to plugin.json
Solid vitest preset:
- vitest.config.ts: add vite-plugin-solid, expand include to
src/components/solid/**/*.test.tsx — Badge.test.tsx now runs
automatically as part of npm test (112 files / 3188 tests)
Clean sweep of every actionable item from the two Copilot review passes
on head 59ae4d8. Grouped by severity:
─── Crash / security (must-fix) ─────────────────────────────────────
1. main.go:140 — telemetryShutdown nil panic.
telemetry.Init can return (nil, err) on the -tags otel skeleton
path; the deferred closure would then call a nil function. Normalise
to a no-op shutdown when Init errors so the defer is always safe.
2. api/upload_handler.go — permSvc nil deref.
MountUploadRoutes + handleServeFile dereference permSvc on every
authenticated file request. Add a fail-fast panic at mount time so
the misconfiguration surfaces at wiring, not on the first 500.
Update upload_handler_test.go to pass a real PermissionService built
on the test DB (the existing tests were missing the argument entirely,
which meant the package wouldn't compile — this fixes the real bug
Copilot flagged).
3. ws/event_persister.go — NewEventPersister nil EventStore panic.
run() dereferences p.store on every flush. Panic at constructor
time instead so the crash happens once at startup rather than
minutes later in a background goroutine.
4. plugin/host_ui.go — serve-time symlink check.
rejectSymlinksUnder only runs at install time, so a symlink created
post-install (accidental or malicious) would be followed by
http.ServeFile and leak host files. Add an os.Lstat + ModeSymlink
check + IsRegular check to AssetHandler on every request. Cheap
relative to the file read and closes the TOCTOU window.
─── Correctness / observability (should-fix) ───────────────────────
5. ws/deps.go:77 — requirePerm hides misconfig as FORBIDDEN.
Previously, nil database, nil perms, or a GetRoleForUser error all
returned ErrCodeForbidden with the same message, making operator
failures indistinguishable from legitimate permission denials.
Split the branches: misconfig + DB error now return ErrCodeInternal
with a server-side slog.Error so operators see the real problem;
FORBIDDEN is reserved for the actual permission-bit check.
6. telemetry/metrics.go — ServiceCallDurationMs renamed to Sec.
Field name said "Ms" but the instrument name was
`service_call_duration_seconds` with unit "s". Renamed the field
and updated all 8 service-layer callers so the struct field and
metric semantics match.
7. ws/event_persister.go — flushEvy typo → flushEvery.
Renamed the field and the one call site in run().
─── Comments out of sync with code ──────────────────────────────────
8. plugin/loader.go — Stat vs Lstat comment.
The comment claimed "Stat (not Lstat)" but the code correctly uses
os.Lstat to detect symlinks. Updated the comment to match the code;
the code was already right.
9. telemetry/telemetry_otel.go — compile claim wrong.
Comment said the file would fail to compile without the upstream
OTel modules, but the skeleton deliberately avoids importing them
and Init returns a runtime error instead. Updated the comment to
reflect actual CI behaviour (the -tags otel build step passes
today but doesn't exercise real telemetry).
─── Nit / polish ────────────────────────────────────────────────────
10. ws/event_pruner.go — startup delay magic constant.
Hard-coded time.Minute made the "run shortly after startup"
behaviour untestable (a test with a 100ms interval would still
wait a full minute). Cap the startup delay by the interval:
min(interval, time.Minute). Documented via a new `maxStartupDelay`
constant.
11. ws/event_pruner_test.go — new file.
Unit coverage for runPrune cutoff correctness, error swallowing,
StartEventPruner nil-store short-circuit, ctx cancellation, and
the interval-bounded startup delay from fix#10. Uses a fakeEventStore
stub that records every prune call and signals the first one so
tests don't sleep.
─── Verification ────────────────────────────────────────────────────
gofmt -l clean. No network access in sandbox so `go vet` and `go test`
could not run; the changes are local and surgical and every touched
file compiles in isolation against the existing signatures.
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
First of two phase-D parity plans. Grounded in the existing V2 command
dispatcher (Server/ws/command.go) and the dormant
plugin.Registry.DispatchCommand + host_commands.go — this is not a
green-field design, it's a wiring plan for code that already exists.
Covers:
- Wire format: command_invoke, command_autocomplete, command_reply,
command_autocomplete_result
- Manifest extension: commands[] with option types, default_member_permissions,
contexts, autocomplete flag
- Schema: migrations/016_plugin_commands.sql with a unique name index
so two plugins can't both own /ban
- Code surface: enumerated file-by-file touch list
- Permission model: server-enforces default_member_permissions BEFORE
the plugin is invoked, plugins never get to gate their own commands
- Built-in commands: /me + /shrug ship in-tree as reference handlers
- Concurrency: 3s deadline via context.WithTimeout passed to DispatchCommand
- Failure modes & UX: 6-row table from "unknown command" through panic
auto-disable
- Testing: unit + integration + contract round trip
- Telemetry: 3 new counters + OTel span
- 4-stage rollout, each step independently shippable
- Open questions: bot identity for broadcasts, component v2 reservation,
cross-plugin imports, DM-context handling
Plan #8 (E2EE DMs + DAVE voice) and PHASE_D_PARITY_TODO.md items 2-7
to follow in a subsequent commit.
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Final in-sandbox completeness pass. Five focused pieces; the remaining
items in PHASE_BC_LOCAL_TODO.md after this commit are all genuinely
local-only (toolchain, network, native deps).
Test coverage (the biggest gap from prior reviews)
- Server/plugin/manifest_test.go — pluginNameRegexp accept/reject table,
validateRelativePath table, oversized version, unknown permission.
- Server/plugin/host_http_test.go — hostAllowed dot-boundary suffix,
empty-entry rejection, case insensitivity, FQDN trailing dot. ipAllowed
table over loopback, RFC1918, RFC4193 (ULA), RFC6598 (CGN), link-local,
multicast, unspecified — both v4 and v6 — plus public-IP accept cases.
- Server/plugin/loader_test.go — rejectSymlinksUnder catches direct and
nested symlinks; scanPluginDirectory rejects a plugin whose entrypoint
is a symlink. Skipped on Windows where symlink creation needs elevation.
- Server/plugin/host_ui_test.go — AssetHandler serves declared files,
rejects undeclared files (404), rejects path traversal, supports nested
asset paths.
- Server/ws/hub_seedseq_test.go — SeedSeq monotonic, never-backwards,
concurrent CAS safety, integration with nextSeq.
- Server/ws/extract_event_type_test.go — table covering happy paths,
control char rejection, escaped quote rejection, length cap (64),
empty/missing/non-JSON inputs.
Plugin install endpoint (closes a real feature gap)
- Server/plugin/registry.go — InstallFromZip extracts a plugin .zip into
a staging directory under cfg.Directory, validates it zip-slip safe
(cleaned-path Rel check), refuses non-regular entries, refuses
symlinks, caps compressed at 16 MiB and uncompressed total at 64 MiB
(each file gated by io.CopyN against the remaining budget). Manifest
is parsed at the staged root, then atomically renamed into the
canonical plugin directory and registered via the existing
installFromDisk path.
- Server/api/plugins_handler.go — POST /install accepts multipart with
one "plugin" file part, http.MaxBytesReader caps the request body,
io.LimitReader caps the in-memory buffer, calls Registry.InstallFromZip,
returns 201 with the new plugin name. The endpoint inherits the Pass 2
admin auth + IP gate (mounted under r.Use(admin.RequireAdminAuth)).
Protocol surface
- Server/ws/serve.go — buildAuthOK now takes replaySource and includes
it in the auth_ok payload as "replay_source": "none" | "buffer" | "db".
Two call sites updated: reconnect path passes the existing local,
fresh-connect path passes "none". Test export updated to pass "none".
CI build-tag matrix
- .github/workflows/ci.yml — three new steps inside server-build-test
build the server with -tags otel, -tags wazero, and -tags otel,wazero.
All three are continue-on-error: true until the upstream OTel and
wazero modules land in go.mod (tracked in PHASE_BC_LOCAL_TODO.md).
Once they do, dropping continue-on-error converts the steps into
hard CI gates against tag-boundary drift.
Documentation
- CHANGELOG.md — new root-level file with curated entries for Phase B,
Phase C, security, and behavioural changes operators must know about
(notably event_persistence.enabled = true by default).
- PHASE_BC_LOCAL_TODO.md — ticks off the install endpoint, the
replay_source field, and the existing event_persistence defaultYAML
entry. The remaining items are toolchain-bound.
After this pass, the in-sandbox completeness ceiling is reached.
Everything still pending requires Go 1.25 toolchain, npm install,
real OTel SDK + wazero modules, sqlc, postgres backend impl, or
tinygo.
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Eight focused follow-ups from the medium-severity review bucket. All
in-sandbox tractable; no module changes, no new dependencies.
Performance
- Drop the defensive memcpy in EventPersister.Enqueue. The hub already
passes a fresh slice from wrapWithSeq and the copy was happening under
seqMu, serializing broadcast throughput. Documented the no-mutate
contract on the call site.
Observability
- AppMetrics gains WSEventsPersistErrors counter; the persister run loop
bumps both it and the existing WSEventsPersisted counter via cached
metrics handle.
- Hub.persistEvent now extracts the real event type ("chat_message",
"voice_join", ...) from the wrapped JSON envelope via a small
no-allocation byte scan instead of recording the generic
"broadcast"/"channel_broadcast" label.
- Added OTel spans + ServiceCallDurationMs histogram entries on one
public method per remaining service: DMService.CreateDM,
VoiceService.JoinChannel, InviteService.CreateInvite,
ModerationService.BanUser, BlockService.BlockUser,
UserService.UpdateProfile. Mirrors the existing pattern from
MessageService.SendMessage.
Hardening
- plugin/loader now Lstat-walks each plugin directory and rejects any
symlink, plus refuses an entrypoint that is itself a symlink. The
asset handler's prefix check stays as defense in depth.
- ipAllowed (plugin HTTP capability) now rejects RFC6598 carrier-grade
NAT (100.64.0.0/10), closing a gap in net.IP.IsPrivate which only
covers RFC1918 + RFC4193.
- Registry.activateAll syncs Instance.Enabled := true after a successful
activate so callers reading the in-memory flag see the live state.
Documentation
- defaultYAML now documents the new event_persistence, telemetry, and
plugins config blocks with their defaults and one-line descriptions.
- PHASE_BC_LOCAL_TODO.md ticks off five items (defaultYAML docs ×2,
remaining service spans, registry wiring already-fixed in Pass 2).
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Phase B + C review pass: critical security and correctness fixes.
Security
- S1: plugin admin endpoints now require admin.RequireAdminAuth in addition
to AdminIPRestrict. Previously a LAN attacker on the allowed CIDR could
list/enable/disable/uninstall plugins without a session.
- S2: rewrite plugin HTTPDo allowlist with proper net/url parsing. Empty
entries are ignored, suffix matches require a dot boundary, and a custom
Dialer rejects loopback / RFC1918 / link-local addresses to close the
DNS-rebinding TOCTOU window. Redirects re-validated.
- S3 + #9: manifest Name pinned to ^[a-z0-9][a-z0-9_-]{0,63}$, Entrypoint
and UI tab assets validated against absolute / "..", NUL byte, backslash
and non-canonical paths. Asset handler hardened with filepath.Rel check
for symlink and prefix-without-separator escapes.
- S5: pluginBridge postMessage handler ignores the pluginId in the message
body and uses an e.source -> contentWindow lookup instead, defeating
spoofed messages from same-origin scripts.
- S8: HTTPDo body capped at 5 MiB via io.LimitReader, redirects bounded
to 5 hops.
Correctness
- Critical seq alignment: PersistEvent now takes the hub-assigned seq as a
required parameter so the events table row seq always matches the wrapped
payload seq. Hub seeds its in-memory atomic counter from MAX(events.seq)
on startup. Drops in the persister queue no longer mis-align row vs
payload seq.
- #1: live plugin.Registry constructed in main.go BEFORE NewRouter and
threaded through; admin handler is no longer wired with nil.
- #3: EventPersister.Stop is now safe to call without a prior Start by
tracking a started flag — previously deadlocked waiting on done.
Wiring
- NewRouter signature gains *plugin.Registry; two test callers updated.
- admin.RequireAdminAuth exported as a thin wrapper over the existing
package-private adminAuthMiddleware.
- sqlc query templates updated for the new PersistEvent + GetMaxEventSeq
contracts (sqlite + postgres).
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
Resolves the modify/delete conflict with dev (which removed
phase-a-foundation.md in a1e8970). The Implementation Status and
Actionable TODOs sections are preserved under docs/ alongside the
other project docs, matching the existing docs/*.md convention.
The original phase-a-foundation.md design brief is gone per dev's
intent; only the post-implementation status and follow-up checklist
survive.