Closes audit-2026-04-07 CRITICAL #3. Holding the `commands` capability used
to bind whatever names the guest module returned from `list_commands`, so an
admin enabling a plugin could not know which commands it would claim and a
plugin could widen its own command surface after review.
The manifest is now the authority. `plugin.json` gains a `commands` block
(`[{"name": "hello"}]`) and `RegisterCommand` refuses any undeclared name —
the single choke point both auto-registration and direct registration route
through, so no caller can bypass it. Declared names are validated to the
dispatcher's canonical lowercase form, deduplicated, and capped at 64.
The object shape matches docs/plans/slash-commands.md so the richer
per-command schema can land later without a manifest migration.
Also pins the two neighbouring CRITICALs that verification found already
closed, and adds the storage key cap host_storage.go's doc comment already
promised:
- #2 (storage key isolation): TestStorageKeysIsolatedPerPlugin — the KV
namespace is the caller's Instance.ID with no parameter to override it,
and plugin_kv PRIMARY KEY (plugin_id, key) makes the split structural.
- #4 (event rate limit): TestEventDeliveryHasNoGuestPath — EventSink.Dispatch
invokes no guest code and has no callers, so there is nothing to limit yet;
a SECURITY GATE comment requires the limiter in whatever change wires
delivery.
- #5 mitigation: TestEmptyAllowlistDeniesEveryHost — the shipped empty
http_allowlist must fail closed.
BREAKING CHANGE: a plugin declaring the `commands` capability must now list
its commands in the manifest's `commands` block; undeclared names no longer
bind. Only the in-repo `hello` example is affected and is updated here.
Deletes Server/store (SQLiteStore, MemStore, the composed Store
interface) and collapses to a single sqlc-backed db package, executing
the prior audit's P4 "single data layer" direction (finding #6).
SQLiteStore was a pure pass-through to *db.DB, so consumers now depend
on narrow interfaces that *db.DB satisfies directly:
- service.Store (service/datastore.go, renamed from store/store.go)
- ws.EventStore (ws/eventstore.go)
- plugin.PluginStore (plugin/pluginstore.go)
The event- and plugin-KV methods that lived in the store's SQLite
implementation move into the db package (db/event_queries.go,
db/plugin_queries.go), keeping their raw-SQL form.
Tests: the MemStore-based unit tests now run against a real in-memory
SQLite db opened per-test with migrations applied, via package-local
seed helpers. Fault-injection tests embed a real *db.DB and override the
single method under test, preserving error-path coverage. Full server
suite and sqlc-verify are green.
Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3
updated; architecture server.md / data-model.md diagrams and prose
updated to the api -> service -> db layering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
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>