Procházet zdrojové kódy

docs: drop '_' prefix on service collections (DB v2.3.1 routing bug); reserve 'vectorapi_' prefix

Fszontagh před 2 měsíci
rodič
revize
85725186da

+ 25 - 13
docs/superpowers/plans/2026-05-31-smartbotic-vectorapi-backend-projects.md

@@ -6,7 +6,18 @@
 
 
 **Why this exists:** After Tasks 1–4 the DB shipped v2.3.0 (project namespaces). See spec **Addendum A** in `docs/superpowers/specs/2026-05-31-smartbotic-vectorapi-design.md`. The client (`libsmartbotic-db-client` 2.3.0) addresses projects via `Config::project` or per-call `<project>:<collection>` qualified names (qualified wins), and exposes `listProjects()`/`createProject(name)`/`dropProject(name)`.
 **Why this exists:** After Tasks 1–4 the DB shipped v2.3.0 (project namespaces). See spec **Addendum A** in `docs/superpowers/specs/2026-05-31-smartbotic-vectorapi-design.md`. The client (`libsmartbotic-db-client` 2.3.0) addresses projects via `Config::project` or per-call `<project>:<collection>` qualified names (qualified wins), and exposes `listProjects()`/`createProject(name)`/`dropProject(name)`.
 
 
-**Architecture:** Multi-tenancy is threaded at the gateway boundary: handlers work in terms of `(project, collection)` and a single `qualify()` prepends the namespace before the gRPC call. Authorization (key→projects grants) is entirely our layer. Global service collections (`_vectorapi_keys`, `_vectorapi_settings`) are stored unqualified (DB `default` project); each project gets its own registry `P:_vectorapi_collections`.
+**Architecture:** Multi-tenancy is threaded at the gateway boundary: handlers work in terms of `(project, collection)` and a single `qualify()` prepends the namespace before the gRPC call. Authorization (key→projects grants) is entirely our layer. Global service collections (`vectorapi_keys`, `vectorapi_settings`) are stored unqualified (DB `default` project); each project gets its own registry `P:vectorapi_collections`.
+
+**⚠️ DESIGN UPDATE (DB v2.3.1 collection-name constraint, found during Task 7):** Service
+collections use names **without a leading underscore** — `vectorapi_keys`,
+`vectorapi_settings`, `vectorapi_collections` — and the `vectorapi_` prefix is
+**reserved** (collection-create must reject it, alongside `_`). Reason: DB v2.3.1
+misroutes reads of `_`-prefixed collections when project-qualified (writes go to
+MemoryStore via the mirror's `_`-skip, but `default:_foo` reads route to LMDB →
+null). Test collection names likewise avoid a leading `_` (use `svapitest_*`).
+Service collections are never added to any project's registry, so the registry-gated
+collection/document handlers won't expose them. (This is a suspected DB bug, reported
+upstream; the no-underscore naming is the agreed workaround regardless.)
 
 
 **Tech stack / conventions:** unchanged from the base plan — C++20, `vectorapi_core` static lib, GoogleTest, flat `src/` headers with quoted includes, namespace `svapi`, integration tests skip cleanly when the DB is down (`SVAPI_REQUIRE_DB`). The DB **is currently running** (v2.3.0 on `localhost:9004`), so integration tests should actually run. Build: `cmake -B build -G Ninja -DBUILD_WEBUI=OFF -S . && cmake --build build -j"$(nproc)"`.
 **Tech stack / conventions:** unchanged from the base plan — C++20, `vectorapi_core` static lib, GoogleTest, flat `src/` headers with quoted includes, namespace `svapi`, integration tests skip cleanly when the DB is down (`SVAPI_REQUIRE_DB`). The DB **is currently running** (v2.3.0 on `localhost:9004`), so integration tests should actually run. Build: `cmake -B build -G Ninja -DBUILD_WEBUI=OFF -S . && cmake --build build -j"$(nproc)"`.
 
 
@@ -58,7 +69,7 @@ std::string generateApiKey();             // 48 lowercase hex (OpenSSL RAND_byte
 // key_store.hpp
 // key_store.hpp
 class KeyStore {
 class KeyStore {
 public:
 public:
-    KeyStore(DbGateway& db, std::string collection = "_vectorapi_keys");
+    KeyStore(DbGateway& db, std::string collection = "vectorapi_keys");
     void bootstrap(const std::string& envAdminKey); // ensure (encrypted) coll; if no keys, create one admin key (log once); env seeds value
     void bootstrap(const std::string& envAdminKey); // ensure (encrypted) coll; if no keys, create one admin key (log once); env seeds value
     std::optional<ApiKey> lookup(const std::string& key) const;  // from snapshot
     std::optional<ApiKey> lookup(const std::string& key) const;  // from snapshot
     std::vector<ApiKey> list() const;                            // from snapshot
     std::vector<ApiKey> list() const;                            // from snapshot
@@ -74,7 +85,7 @@ private:
 // settings_store.hpp  (global settings; same shape as base plan Task 7 but Settings has no api_key)
 // settings_store.hpp  (global settings; same shape as base plan Task 7 but Settings has no api_key)
 class SettingsStore {
 class SettingsStore {
 public:
 public:
-    SettingsStore(DbGateway& db, std::string collection = "_vectorapi_settings", std::string docId = "current");
+    SettingsStore(DbGateway& db, std::string collection = "vectorapi_settings", std::string docId = "current");
     void bootstrap(const std::string& envOpenAiKey);   // ensure (encrypted) coll; seed openai key from env if empty; publish snapshot
     void bootstrap(const std::string& envOpenAiKey);   // ensure (encrypted) coll; seed openai key from env if empty; publish snapshot
     std::shared_ptr<const Settings> snapshot() const;
     std::shared_ptr<const Settings> snapshot() const;
     void reloadFromDb(); bool save(const Settings&); void startWatch();
     void reloadFromDb(); bool save(const Settings&); void startWatch();
@@ -86,7 +97,7 @@ struct CollectionMeta { std::string name, kind; uint32_t vectorDimension=0; std:
 class CollectionRegistry {
 class CollectionRegistry {
 public:
 public:
     explicit CollectionRegistry(DbGateway& db);
     explicit CollectionRegistry(DbGateway& db);
-    void ensure(const std::string& project);                                  // ensure qualify(project,"_vectorapi_collections")
+    void ensure(const std::string& project);                                  // ensure qualify(project,"vectorapi_collections")
     bool add(const std::string& project, const CollectionMeta&);
     bool add(const std::string& project, const CollectionMeta&);
     std::optional<CollectionMeta> get(const std::string& project, const std::string& name);
     std::optional<CollectionMeta> get(const std::string& project, const std::string& name);
     std::vector<CollectionMeta> list(const std::string& project);
     std::vector<CollectionMeta> list(const std::string& project);
@@ -402,7 +413,7 @@ using namespace svapi;
 
 
 TEST(KeyStore, BootstrapCreatesAdminKeyOnce) {
 TEST(KeyStore, BootstrapCreatesAdminKeyOnce) {
     SVAPI_REQUIRE_DB(db);
     SVAPI_REQUIRE_DB(db);
-    const std::string coll = "_svapitest_keys_a";
+    const std::string coll = "svapitest_keys_a";
     db->client().dropCollection(coll);
     db->client().dropCollection(coll);
 
 
     KeyStore ks(*db, coll);
     KeyStore ks(*db, coll);
@@ -426,7 +437,7 @@ TEST(KeyStore, BootstrapCreatesAdminKeyOnce) {
 
 
 TEST(KeyStore, EnvSeedsAdminKeyValue) {
 TEST(KeyStore, EnvSeedsAdminKeyValue) {
     SVAPI_REQUIRE_DB(db);
     SVAPI_REQUIRE_DB(db);
-    const std::string coll = "_svapitest_keys_b";
+    const std::string coll = "svapitest_keys_b";
     db->client().dropCollection(coll);
     db->client().dropCollection(coll);
     KeyStore ks(*db, coll);
     KeyStore ks(*db, coll);
     ks.bootstrap("seed-admin-123");
     ks.bootstrap("seed-admin-123");
@@ -437,7 +448,7 @@ TEST(KeyStore, EnvSeedsAdminKeyValue) {
 
 
 TEST(KeyStore, CreateUpdateRemove) {
 TEST(KeyStore, CreateUpdateRemove) {
     SVAPI_REQUIRE_DB(db);
     SVAPI_REQUIRE_DB(db);
-    const std::string coll = "_svapitest_keys_c";
+    const std::string coll = "svapitest_keys_c";
     db->client().dropCollection(coll);
     db->client().dropCollection(coll);
     KeyStore ks(*db, coll); ks.bootstrap("");
     KeyStore ks(*db, coll); ks.bootstrap("");
 
 
@@ -457,7 +468,7 @@ TEST(KeyStore, CreateUpdateRemove) {
 
 
 TEST(SettingsStore, BootstrapAndSave) {
 TEST(SettingsStore, BootstrapAndSave) {
     SVAPI_REQUIRE_DB(db);
     SVAPI_REQUIRE_DB(db);
-    const std::string coll = "_svapitest_settings";
+    const std::string coll = "svapitest_settings";
     db->client().dropCollection(coll);
     db->client().dropCollection(coll);
     SettingsStore ss(*db, coll, "current");
     SettingsStore ss(*db, coll, "current");
     ss.bootstrap(/*envOpenAiKey*/ "sk-env");
     ss.bootstrap(/*envOpenAiKey*/ "sk-env");
@@ -649,7 +660,7 @@ Add `test_registry` target.
 
 
 - [ ] **Step 2:** Build → fails.
 - [ ] **Step 2:** Build → fails.
 
 
-- [ ] **Step 3: `src/collection_registry.hpp`** — the interface from Canonical interfaces; private `std::string coll(const std::string& project) const { return qualify(project, "_vectorapi_collections"); }` and `DbGateway& db_;`. Include `db_gateway.hpp` for `qualify`.
+- [ ] **Step 3: `src/collection_registry.hpp`** — the interface from Canonical interfaces; private `std::string coll(const std::string& project) const { return qualify(project, "vectorapi_collections"); }` and `DbGateway& db_;`. Include `db_gateway.hpp` for `qualify`.
 
 
 - [ ] **Step 4: `src/collection_registry.cpp`**
 - [ ] **Step 4: `src/collection_registry.cpp`**
 
 
@@ -1142,8 +1153,8 @@ protected:
     httplib::Server mockOpenAi_;
     httplib::Server mockOpenAi_;
     std::thread mockThread_;
     std::thread mockThread_;
     int mockPort_ = 0, mockDim_ = 4;
     int mockPort_ = 0, mockDim_ = 4;
-    std::string settingsColl_ = "_svapitest_api_settings";
-    std::string keysColl_     = "_svapitest_api_keys";
+    std::string settingsColl_ = "svapitest_api_settings";
+    std::string keysColl_     = "svapitest_api_keys";
 
 
     void SetUp() override {
     void SetUp() override {
         db_ = connectOrNull();
         db_ = connectOrNull();
@@ -1466,7 +1477,8 @@ void registerCollectionRoutes(ApiServer& s) {
         auto body = bodyJson(req);
         auto body = bodyJson(req);
         std::string name = body.value("name", ""), kind = body.value("kind", "json");
         std::string name = body.value("name", ""), kind = body.value("kind", "json");
         if (name.empty()) throw ApiError(ErrCode::Unprocessable, "validation", "name is required");
         if (name.empty()) throw ApiError(ErrCode::Unprocessable, "validation", "name is required");
-        if (name.rfind('_', 0) == 0) throw ApiError(ErrCode::Unprocessable, "validation", "name may not start with '_'");
+        if (name.rfind('_', 0) == 0 || name.rfind("vectorapi_", 0) == 0)
+            throw ApiError(ErrCode::Unprocessable, "validation", "name may not start with '_' or the reserved 'vectorapi_' prefix");
         if (kind != "json" && kind != "vector") throw ApiError(ErrCode::Unprocessable, "validation", "kind must be 'json' or 'vector'");
         if (kind != "json" && kind != "vector") throw ApiError(ErrCode::Unprocessable, "validation", "kind must be 'json' or 'vector'");
         uint32_t dim = 0; std::string model;
         uint32_t dim = 0; std::string model;
         if (kind == "vector") {
         if (kind == "vector") {
@@ -1963,7 +1975,7 @@ TEST_F(ApiFixture, OpenApiDescribesProjectRoutes) {
 - Binary serves `/healthz`, `/readyz`, `/openapi.json`, `/llms.txt`, `/ui/login`, the project-scoped `/api/v1/projects/{project}/…` surface, and admin `/api/v1/projects` + `/api/v1/keys`.
 - Binary serves `/healthz`, `/readyz`, `/openapi.json`, `/llms.txt`, `/ui/login`, the project-scoped `/api/v1/projects/{project}/…` surface, and admin `/api/v1/projects` + `/api/v1/keys`.
 - First run with no keys logs a generated **admin** key once; `SMARTBOTIC_VECTORAPI_KEY` seeds it; `OPENAI_API_KEY` seeds embeddings.
 - First run with no keys logs a generated **admin** key once; `SMARTBOTIC_VECTORAPI_KEY` seeds it; `OPENAI_API_KEY` seeds embeddings.
 - A non-admin key reaches only its granted projects (403 otherwise) and cannot manage projects/keys (403); revoking a key makes its requests 401.
 - A non-admin key reaches only its granted projects (403 otherwise) and cannot manage projects/keys (403); revoking a key makes its requests 401.
-- Editing `_vectorapi_settings` / `_vectorapi_keys` in the DB hot-reloads without restart.
+- Editing `vectorapi_settings` / `vectorapi_keys` in the DB hot-reloads without restart.
 
 
 ## Self-Review
 ## Self-Review
 
 

+ 11 - 4
docs/superpowers/specs/2026-05-31-smartbotic-vectorapi-design.md

@@ -356,12 +356,19 @@ seeds this initial admin key's value when set. The key is never logged again.
 ## A.4 Data model & scoping (replaces §5)
 ## A.4 Data model & scoping (replaces §5)
 
 
 - **Global** (stored unqualified, i.e. in the DB `default` project):
 - **Global** (stored unqualified, i.e. in the DB `default` project):
-  - `_vectorapi_keys` — one doc per key (doc id = the key string); encrypted.
-  - `_vectorapi_settings` — global settings doc `current`; encrypted. **No `api_key`
-    field anymore** — keys live in `_vectorapi_keys`.
+  - `vectorapi_keys` — one doc per key (doc id = the key string); encrypted.
+  - `vectorapi_settings` — global settings doc `current`; encrypted. **No `api_key`
+    field anymore** — keys live in `vectorapi_keys`.
 - **Per project `P`** (qualified `P:<name>`):
 - **Per project `P`** (qualified `P:<name>`):
-  - `P:_vectorapi_collections` — the collection registry for that project.
+  - `P:vectorapi_collections` — the collection registry for that project.
   - `P:<userCollection>` — user JSON / vector collections.
   - `P:<userCollection>` — user JSON / vector collections.
+
+> **Naming note (DB v2.3.1):** service collections deliberately do **not** use a
+> leading underscore — DB v2.3.1 misroutes reads of `_`-prefixed project-qualified
+> collections (write→MemoryStore, read→LMDB → null). The `vectorapi_` prefix is
+> therefore **reserved**: collection-create rejects names starting with `_` or
+> `vectorapi_`. Service collections are never registered, so the registry-gated
+> handlers never expose them. (Suspected DB bug, reported upstream.)
 - The gateway exposes `qualify(project, collection)`; global service collections are
 - The gateway exposes `qualify(project, collection)`; global service collections are
   used unqualified. Projects referenced by a request are `createProject`-ensured
   used unqualified. Projects referenced by a request are `createProject`-ensured
   (idempotent) on first granted use.
   (idempotent) on first granted use.