|
|
@@ -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)`.
|
|
|
|
|
|
-**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)"`.
|
|
|
|
|
|
@@ -58,7 +69,7 @@ std::string generateApiKey(); // 48 lowercase hex (OpenSSL RAND_byte
|
|
|
// key_store.hpp
|
|
|
class KeyStore {
|
|
|
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
|
|
|
std::optional<ApiKey> lookup(const std::string& key) 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)
|
|
|
class SettingsStore {
|
|
|
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
|
|
|
std::shared_ptr<const Settings> snapshot() const;
|
|
|
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 {
|
|
|
public:
|
|
|
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&);
|
|
|
std::optional<CollectionMeta> get(const std::string& project, const std::string& name);
|
|
|
std::vector<CollectionMeta> list(const std::string& project);
|
|
|
@@ -402,7 +413,7 @@ using namespace svapi;
|
|
|
|
|
|
TEST(KeyStore, BootstrapCreatesAdminKeyOnce) {
|
|
|
SVAPI_REQUIRE_DB(db);
|
|
|
- const std::string coll = "_svapitest_keys_a";
|
|
|
+ const std::string coll = "svapitest_keys_a";
|
|
|
db->client().dropCollection(coll);
|
|
|
|
|
|
KeyStore ks(*db, coll);
|
|
|
@@ -426,7 +437,7 @@ TEST(KeyStore, BootstrapCreatesAdminKeyOnce) {
|
|
|
|
|
|
TEST(KeyStore, EnvSeedsAdminKeyValue) {
|
|
|
SVAPI_REQUIRE_DB(db);
|
|
|
- const std::string coll = "_svapitest_keys_b";
|
|
|
+ const std::string coll = "svapitest_keys_b";
|
|
|
db->client().dropCollection(coll);
|
|
|
KeyStore ks(*db, coll);
|
|
|
ks.bootstrap("seed-admin-123");
|
|
|
@@ -437,7 +448,7 @@ TEST(KeyStore, EnvSeedsAdminKeyValue) {
|
|
|
|
|
|
TEST(KeyStore, CreateUpdateRemove) {
|
|
|
SVAPI_REQUIRE_DB(db);
|
|
|
- const std::string coll = "_svapitest_keys_c";
|
|
|
+ const std::string coll = "svapitest_keys_c";
|
|
|
db->client().dropCollection(coll);
|
|
|
KeyStore ks(*db, coll); ks.bootstrap("");
|
|
|
|
|
|
@@ -457,7 +468,7 @@ TEST(KeyStore, CreateUpdateRemove) {
|
|
|
|
|
|
TEST(SettingsStore, BootstrapAndSave) {
|
|
|
SVAPI_REQUIRE_DB(db);
|
|
|
- const std::string coll = "_svapitest_settings";
|
|
|
+ const std::string coll = "svapitest_settings";
|
|
|
db->client().dropCollection(coll);
|
|
|
SettingsStore ss(*db, coll, "current");
|
|
|
ss.bootstrap(/*envOpenAiKey*/ "sk-env");
|
|
|
@@ -649,7 +660,7 @@ Add `test_registry` target.
|
|
|
|
|
|
- [ ] **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`**
|
|
|
|
|
|
@@ -1142,8 +1153,8 @@ protected:
|
|
|
httplib::Server mockOpenAi_;
|
|
|
std::thread mockThread_;
|
|
|
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 {
|
|
|
db_ = connectOrNull();
|
|
|
@@ -1466,7 +1477,8 @@ void registerCollectionRoutes(ApiServer& s) {
|
|
|
auto body = bodyJson(req);
|
|
|
std::string name = body.value("name", ""), kind = body.value("kind", "json");
|
|
|
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'");
|
|
|
uint32_t dim = 0; std::string model;
|
|
|
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`.
|
|
|
- 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.
|
|
|
-- 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
|
|
|
|