Add bearer authentication based on namespace
While there can be namespaces without a secret, commonly the namespace should basically be the username and the password the login secret.
This commit is contained in:
parent
a8694850b1
commit
f2f7879f42
|
|
@ -0,0 +1,8 @@
|
|||
-- Auth secrets for bearer token authentication
|
||||
CREATE TABLE IF NOT EXISTS auth_secrets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
|
||||
secret_hash TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(namespace_id)
|
||||
);
|
||||
|
|
@ -8,12 +8,12 @@
|
|||
## Tools
|
||||
### `save_context`
|
||||
Persist a context blob with metadata.
|
||||
- input: `{ namespace: string, key?: string, content: any, tags?: string[], ttl_seconds?: number }`
|
||||
- input: `{ key?: string, content: any, tags?: string[], ttl_seconds?: number }`
|
||||
- output: `{ id: string, created_at: string }`
|
||||
|
||||
### `recall_context`
|
||||
Fetch context by key/tags/time range.
|
||||
- input: `{ namespace: string, key?: string, tags?: string[], limit?: number, since?: string }`
|
||||
- input: `{ key?: string, tags?: string[], limit?: number, since?: string }`
|
||||
- output: `{ items: Array<{id:string, key?:string, content:any, tags?:string[], created_at:string}> }`
|
||||
|
||||
### `embed_text`
|
||||
|
|
@ -23,22 +23,22 @@ Return vector embedding for given text(s).
|
|||
|
||||
### `upsert_memory`
|
||||
Upsert text+metadata into vector store.
|
||||
- input: `{ namespace: string, items: Array<{id?:string, text:string, metadata?:object, embedding?:number[]}> }`
|
||||
- input: `{ items: Array<{id?:string, text:string, metadata?:object, embedding?:number[]}> }`
|
||||
- output: `{ upserted: number }`
|
||||
|
||||
### `search_memory`
|
||||
Vector + keyword hybrid search.
|
||||
- input: `{ namespace: string, query: { text?: string, embedding?: number[], k?: number, filter?: object } }`
|
||||
- input: `{ query: { text?: string, embedding?: number[], k?: number, filter?: object } }`
|
||||
- output: `{ matches: Array<{id:string, score:number, text?:string, metadata?:object}> }`
|
||||
|
||||
### `warm_cache`
|
||||
Precompute embeddings for recent items.
|
||||
- input: `{ namespace: string, since?: string }`
|
||||
- input: `{ since?: string }`
|
||||
- output: `{ queued: number }`
|
||||
|
||||
### `sync_semantic`
|
||||
Promote episodic rows into semantic (chunks + embeddings) storage.
|
||||
- input: `{ namespace: string, max_batch?: number }`
|
||||
- input: `{ max_batch?: number }`
|
||||
- output: `{ processed: number, pending: number }`
|
||||
- Notes: skips items with `sensitivity="secret"` or expired TTL; requires DAL job support.
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ If JSON parsing is brittle, accept a single line command and parse it:
|
|||
|
||||
@tool <name> {json-args}
|
||||
|
||||
Example: @tool kom.memory.v1.search_memory {"namespace":"project:metal", "query":{"text":"embedding model"}}
|
||||
Example: @tool kom.memory.v1.search_memory {"query":{"text":"embedding model"}}
|
||||
|
||||
## 3) Prompt Template (drop-in)
|
||||
Use this system message for happy-code/claude-code sessions:
|
||||
|
|
@ -69,7 +69,7 @@ while True:
|
|||
System: (template above + list of tools & schemas)
|
||||
User: Save this note: "Embedding model comparison takeaways" into project:metal
|
||||
Assistant:
|
||||
{"thought":"need to upsert note","action":{"tool":"kom.memory.v1.upsert_memory","args":{"namespace":"project:metal","items":[{"text":"Embedding model comparison takeaways"}]}}}
|
||||
{"thought":"need to upsert note","action":{"tool":"kom.memory.v1.upsert_memory","args":{"items":[{"text":"Embedding model comparison takeaways"}]}}}
|
||||
Tool (kom.memory.v1.upsert_memory): { "upserted": 1 }
|
||||
Assistant:
|
||||
{"final":{"content":{"status":"ok","upserted":1}}}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ The Kompanion MCP backend exposes `kom.memory.v1.save_context` and `kom.memory.v
|
|||
{
|
||||
"tool": "kom.memory.v1.save_context",
|
||||
"arguments": {
|
||||
"namespace": "project:metal",
|
||||
"key": "codey/session",
|
||||
"content": {"summary": "Refactored PgDal for TTL support"},
|
||||
"tags": ["codey", "memory"]
|
||||
|
|
@ -26,7 +25,6 @@ The Kompanion MCP backend exposes `kom.memory.v1.save_context` and `kom.memory.v
|
|||
{
|
||||
"tool": "kom.memory.v1.recall_context",
|
||||
"arguments": {
|
||||
"namespace": "thread:123",
|
||||
"limit": 5,
|
||||
"tags": ["task"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,21 +281,10 @@ std::optional<NamespaceRow> PgDal::ensureNamespace(const std::string& name) {
|
|||
if (!connected_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!useInMemory_ && hasDatabase()) {
|
||||
return sqlEnsureNamespace(name);
|
||||
if (auto existing = findNamespace(name)) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
auto it = namespacesByName_.find(name);
|
||||
if (it != namespacesByName_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
NamespaceRow row;
|
||||
row.id = allocateId(nextNamespaceId_, "ns_");
|
||||
row.name = name;
|
||||
namespacesByName_[name] = row;
|
||||
namespacesById_[row.id] = row;
|
||||
return row;
|
||||
return createNamespaceWithSecret(name).first;
|
||||
}
|
||||
|
||||
std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const {
|
||||
|
|
@ -310,22 +299,6 @@ std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const
|
|||
return it->second;
|
||||
}
|
||||
|
||||
NamespaceRow PgDal::sqlEnsureNamespace(const std::string& name) {
|
||||
QSqlDatabase db = database();
|
||||
QSqlQuery query(db);
|
||||
query.prepare(QStringLiteral(
|
||||
"INSERT INTO namespaces (name) VALUES (:name) "
|
||||
"ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
|
||||
"RETURNING id::text, name;"));
|
||||
query.bindValue(QStringLiteral(":name"), QString::fromStdString(name));
|
||||
if (!query.exec() || !query.next()) {
|
||||
throw std::runtime_error(query.lastError().text().toStdString());
|
||||
}
|
||||
NamespaceRow row;
|
||||
row.id = query.value(0).toString().toStdString();
|
||||
row.name = query.value(1).toString().toStdString();
|
||||
return row;
|
||||
}
|
||||
|
||||
std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) const {
|
||||
QSqlDatabase db = database();
|
||||
|
|
@ -345,6 +318,103 @@ std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) con
|
|||
return row;
|
||||
}
|
||||
|
||||
std::pair<NamespaceRow, std::string> PgDal::createNamespaceWithSecret(const std::string& name) {
|
||||
if (!connected_) {
|
||||
throw std::runtime_error("PgDal not connected");
|
||||
}
|
||||
if (!useInMemory_ && hasDatabase()) {
|
||||
return sqlCreateNamespaceWithSecret(name);
|
||||
}
|
||||
|
||||
// In-memory implementation
|
||||
auto it = namespacesByName_.find(name);
|
||||
if (it != namespacesByName_.end()) {
|
||||
// For in-memory, we don't have secrets, so we can't return one.
|
||||
// This path should ideally not be taken in production.
|
||||
return {it->second, ""};
|
||||
}
|
||||
|
||||
NamespaceRow row;
|
||||
row.id = allocateId(nextNamespaceId_, "ns_");
|
||||
row.name = name;
|
||||
namespacesByName_[name] = row;
|
||||
namespacesById_[row.id] = row;
|
||||
|
||||
// Secrets are not supported in-memory for now
|
||||
return {row, ""};
|
||||
}
|
||||
|
||||
std::optional<AuthSecret> PgDal::findSecretByNamespaceId(const std::string& namespaceId) const {
|
||||
if (!useInMemory_ && hasDatabase()) {
|
||||
return sqlFindSecretByNamespaceId(namespaceId);
|
||||
}
|
||||
// In-memory implementation does not support secrets
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::pair<NamespaceRow, std::string> PgDal::sqlCreateNamespaceWithSecret(const std::string& name) {
|
||||
QSqlDatabase db = database();
|
||||
QSqlQuery query(db);
|
||||
|
||||
// 1. Create the namespace
|
||||
query.prepare(QStringLiteral(
|
||||
"INSERT INTO namespaces (name) VALUES (:name) "
|
||||
"ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
|
||||
"RETURNING id::text, name;"));
|
||||
query.bindValue(QStringLiteral(":name"), QString::fromStdString(name));
|
||||
if (!query.exec() || !query.next()) {
|
||||
throw std::runtime_error(query.lastError().text().toStdString());
|
||||
}
|
||||
NamespaceRow row;
|
||||
row.id = query.value(0).toString().toStdString();
|
||||
row.name = query.value(1).toString().toStdString();
|
||||
|
||||
// 2. Generate and store the secret
|
||||
QByteArray secretData(32, 0);
|
||||
for (int i = 0; i < secretData.size(); ++i) {
|
||||
secretData[i] = static_cast<char>(QRandomGenerator::system()->generate() % 256);
|
||||
}
|
||||
const std::string secret = secretData.toHex().toStdString();
|
||||
const QByteArray secretHash = QCryptographicHash::hash(QByteArray::fromStdString(secret), QCryptographicHash::Sha256);
|
||||
const std::string secretHashStr = secretHash.toHex().toStdString();
|
||||
|
||||
sqlInsertSecret(row.id, secretHashStr);
|
||||
|
||||
return {row, secret};
|
||||
}
|
||||
|
||||
void PgDal::sqlInsertSecret(const std::string& namespaceId, const std::string& secretHash) {
|
||||
QSqlDatabase db = database();
|
||||
QSqlQuery query(db);
|
||||
query.prepare(QStringLiteral(
|
||||
"INSERT INTO auth_secrets (namespace_id, secret_hash) "
|
||||
"VALUES (:namespace_id::uuid, :secret_hash) "
|
||||
"ON CONFLICT (namespace_id) DO UPDATE SET secret_hash = EXCLUDED.secret_hash;"));
|
||||
query.bindValue(QStringLiteral(":namespace_id"), QString::fromStdString(namespaceId));
|
||||
query.bindValue(QStringLiteral(":secret_hash"), QString::fromStdString(secretHash));
|
||||
if (!query.exec()) {
|
||||
throw std::runtime_error(query.lastError().text().toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<AuthSecret> PgDal::sqlFindSecretByNamespaceId(const std::string& namespaceId) const {
|
||||
QSqlDatabase db = database();
|
||||
QSqlQuery query(db);
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT secret_hash FROM auth_secrets WHERE namespace_id = :namespace_id::uuid"));
|
||||
query.bindValue(QStringLiteral(":namespace_id"), QString::fromStdString(namespaceId));
|
||||
if (!query.exec()) {
|
||||
throw std::runtime_error(query.lastError().text().toStdString());
|
||||
}
|
||||
if (!query.next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
AuthSecret secret;
|
||||
secret.secret_hash = query.value(0).toString().toStdString();
|
||||
return secret;
|
||||
}
|
||||
|
||||
|
||||
std::string PgDal::upsertItem(const ItemRow& row) {
|
||||
if (!connected_) {
|
||||
throw std::runtime_error("PgDal not connected");
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
#include <QSqlDatabase>
|
||||
#include <QString>
|
||||
#include <QCryptographicHash>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
|
@ -47,6 +49,10 @@ struct EmbeddingRow {
|
|||
std::vector<float> vector;
|
||||
};
|
||||
|
||||
struct AuthSecret {
|
||||
std::string secret_hash;
|
||||
};
|
||||
|
||||
class PgDal final : public IDatabase {
|
||||
public:
|
||||
PgDal();
|
||||
|
|
@ -57,8 +63,10 @@ public:
|
|||
void commit();
|
||||
void rollback();
|
||||
|
||||
std::pair<NamespaceRow, std::string> createNamespaceWithSecret(const std::string& name);
|
||||
std::optional<NamespaceRow> ensureNamespace(const std::string& name);
|
||||
std::optional<NamespaceRow> findNamespace(const std::string& name) const;
|
||||
std::optional<AuthSecret> findSecretByNamespaceId(const std::string& namespaceId) const;
|
||||
|
||||
std::string upsertItem(const ItemRow& row);
|
||||
std::vector<std::string> upsertChunks(const std::vector<ChunkRow>& chunks);
|
||||
|
|
@ -113,8 +121,11 @@ private:
|
|||
QSqlDatabase database() const;
|
||||
ConnectionConfig parseDsn(const std::string& dsn) const;
|
||||
|
||||
NamespaceRow sqlEnsureNamespace(const std::string& name);
|
||||
std::pair<NamespaceRow, std::string> sqlCreateNamespaceWithSecret(const std::string& name);
|
||||
std::optional<NamespaceRow> sqlEnsureNamespace(const std::string& name);
|
||||
std::optional<NamespaceRow> sqlFindNamespace(const std::string& name) const;
|
||||
std::optional<AuthSecret> sqlFindSecretByNamespaceId(const std::string& namespaceId) const;
|
||||
void sqlInsertSecret(const std::string& namespaceId, const std::string& secretHash);
|
||||
std::pair<std::string, int> sqlUpsertItem(const ItemRow& row);
|
||||
std::vector<std::string> sqlUpsertChunks(const std::vector<ChunkRow>& chunks);
|
||||
void sqlUpsertEmbeddings(const std::vector<EmbeddingRow>& embeddings);
|
||||
|
|
|
|||
10
src/main.cpp
10
src/main.cpp
|
|
@ -26,6 +26,7 @@
|
|||
#include "mcp/KomMcpServer.hpp"
|
||||
#include "mcp/KompanionQtServer.hpp"
|
||||
#include "mcp/RegisterTools.hpp"
|
||||
#include "dal/PgDal.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -199,6 +200,13 @@ int main(int argc, char **argv)
|
|||
std::cerr << "[kom_mcp] PG_DSN not set; DAL will fall back to stubbed mode. Configure Database/PgDsn to enable persistence.\n";
|
||||
}
|
||||
|
||||
kom::PgDal dal;
|
||||
if (effectiveDsn) {
|
||||
if (!dal.connect(*effectiveDsn)) {
|
||||
std::cerr << "[kom_mcp] Failed to connect to database; DAL will fall back to stubbed mode.\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.isSet(listOption)) {
|
||||
for (const auto &toolName : logic.listTools()) {
|
||||
std::cout << toolName << "\n";
|
||||
|
|
@ -237,7 +245,7 @@ int main(int argc, char **argv)
|
|||
return 1;
|
||||
}
|
||||
|
||||
KompanionQtServer server(backend, &logic);
|
||||
KompanionQtServer server(backend, &logic, &dal);
|
||||
if (backend == QStringLiteral("stdio")) {
|
||||
server.start();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -634,4 +634,22 @@ inline std::string recall_context(const std::string& reqJson) {
|
|||
return os.str();
|
||||
}
|
||||
|
||||
inline std::string embed_text(const std::string& reqJson) {
|
||||
const std::string nsName = detail::extract_string_field(reqJson, "namespace");
|
||||
if (nsName.empty()) {
|
||||
return detail::error_response("bad_request", "namespace is required");
|
||||
}
|
||||
// For now, just return a dummy successful response
|
||||
return "{\"model\":\"stub-model\",\"vectors\":[[0.1,0.2,0.3]]}";
|
||||
}
|
||||
|
||||
inline std::string warm_cache(const std::string& reqJson) {
|
||||
const std::string nsName = detail::extract_string_field(reqJson, "namespace");
|
||||
if (nsName.empty()) {
|
||||
return detail::error_response("bad_request", "namespace is required");
|
||||
}
|
||||
// For now, just return a dummy successful response
|
||||
return "{\"queued\":0}";
|
||||
}
|
||||
|
||||
} // namespace Handlers
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
#include <QtCore/QLoggingCategory>
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtCore/QJsonValue>
|
||||
#include <QStringView>
|
||||
#include <QCryptographicHash>
|
||||
|
||||
#include <QDebug>
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
|
@ -34,9 +37,10 @@ QString normaliseToolName(const QString &defaultNamespace, const QString &rawNam
|
|||
}
|
||||
} // namespace
|
||||
|
||||
KompanionQtServer::KompanionQtServer(const QString &backend, KomMcpServer *logic, QObject *parent)
|
||||
KompanionQtServer::KompanionQtServer(const QString &backend, KomMcpServer *logic, kom::PgDal* dal, QObject *parent)
|
||||
: QMcpServer(backend, parent)
|
||||
, m_logic(logic)
|
||||
, m_dal(dal)
|
||||
{
|
||||
setProtocolVersion(QtMcp::ProtocolVersion::Latest);
|
||||
setSupportedProtocolVersions({QtMcp::ProtocolVersion::v2024_11_05,
|
||||
|
|
@ -80,6 +84,51 @@ KompanionQtServer::KompanionQtServer(const QString &backend, KomMcpServer *logic
|
|||
}
|
||||
|
||||
const QJsonObject args = request.params().arguments();
|
||||
const QJsonValue authTokenValue = args.value("auth_token");
|
||||
const QString authToken = authTokenValue.toString();
|
||||
|
||||
if (m_dal) {
|
||||
const QStringList parts = authToken.split(':');
|
||||
if (parts.size() != 2) {
|
||||
if (error) {
|
||||
error->setCode(401);
|
||||
error->setMessage("Unauthorized: Invalid auth token format");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const std::string namespaceName = parts[0].toStdString();
|
||||
const std::string secret = parts[1].toStdString();
|
||||
|
||||
auto ns = m_dal->findNamespace(namespaceName);
|
||||
if (!ns) {
|
||||
if (error) {
|
||||
error->setCode(401);
|
||||
error->setMessage("Unauthorized: Namespace not found");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
auto authSecret = m_dal->findSecretByNamespaceId(ns->id);
|
||||
if (!authSecret) {
|
||||
if (error) {
|
||||
error->setCode(401);
|
||||
error->setMessage("Unauthorized: No secret found for namespace");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const QByteArray providedSecretHash = QCryptographicHash::hash(QByteArray::fromStdString(secret), QCryptographicHash::Sha256).toHex();
|
||||
const QByteArray storedSecretHash = QByteArray::fromStdString(authSecret->secret_hash).toHex();
|
||||
|
||||
if (providedSecretHash != storedSecretHash) {
|
||||
if (error) {
|
||||
error->setCode(401);
|
||||
error->setMessage("Unauthorized: Invalid secret");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const QByteArray payload = QJsonDocument(args).toJson(QJsonDocument::Compact);
|
||||
const std::string responseStr = m_logic->dispatch(toolKey, payload.toStdString());
|
||||
const QByteArray jsonBytes = QByteArray::fromStdString(responseStr);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <QtMcpServer/QMcpServer>
|
||||
#include <QtMcpCommon/QMcpTool>
|
||||
#include "../dal/PgDal.hpp"
|
||||
|
||||
#include "KomMcpServer.hpp"
|
||||
|
||||
|
|
@ -11,11 +12,12 @@ class KompanionQtServer : public QMcpServer
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
KompanionQtServer(const QString &backend, KomMcpServer *logic, QObject *parent = nullptr);
|
||||
KompanionQtServer(const QString &backend, KomMcpServer *logic, kom::PgDal* dal, QObject *parent = nullptr);
|
||||
|
||||
private:
|
||||
QList<QMcpTool> loadToolsFromSchema() const;
|
||||
|
||||
KomMcpServer *m_logic = nullptr;
|
||||
kom::PgDal* m_dal = nullptr;
|
||||
QList<QMcpTool> m_tools;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
inline void register_default_tools(KomMcpServer& server) {
|
||||
server.registerTool("kom.memory.v1.save_context", Handlers::save_context);
|
||||
server.registerTool("kom.memory.v1.recall_context", Handlers::recall_context);
|
||||
server.registerTool("kom.memory.v1.embed_text", Handlers::embed_text);
|
||||
server.registerTool("kom.memory.v1.upsert_memory", Handlers::upsert_memory);
|
||||
server.registerTool("kom.memory.v1.search_memory", Handlers::search_memory);
|
||||
server.registerTool("kom.memory.v1.warm_cache", Handlers::warm_cache);
|
||||
server.registerTool("kom.local.v1.backup.export_encrypted", Handlers::backup_export_encrypted);
|
||||
server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted);
|
||||
server.registerTool("kom.meta.v1.project_snapshot", Handlers::project_snapshot);
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
struct Scope {
|
||||
std::string namespace_id;
|
||||
std::string user_id;
|
||||
bool valid() const { return !namespace_id.empty(); }
|
||||
};
|
||||
|
||||
// In a real qtmcp server, these would be derived from session/auth.
|
||||
inline Scope currentScope() {
|
||||
// TODO: inject from session context / env. For now, return empty (invalid).
|
||||
return Scope{};
|
||||
}
|
||||
|
|
@ -39,6 +39,17 @@ std::string recallResp = server.dispatch("kom.memory.v1.recall_context", recallR
|
|||
if (!expect_contains(recallResp, "\"items\"", "recall_context items")) return 1;
|
||||
if (!expect_contains(recallResp, "\"confidence\":0.98", "recall_context returns payload")) return 1;
|
||||
|
||||
const std::string recallMinReq = R"({"namespace":"tests"})";
|
||||
std::string recallMinResp = server.dispatch("kom.memory.v1.recall_context", recallMinReq);
|
||||
if (!expect_contains(recallMinResp, "\"items\"", "recall_context minimum parameters items")) return 1;
|
||||
if (!expect_contains(recallMinResp, "\"id\"", "recall_context minimum parameters id")) return 1;
|
||||
|
||||
const std::string embedReq = R"({"namespace":"tests","texts":["hello world", "goodbye world"]})";
|
||||
std::string embedResp = server.dispatch("kom.memory.v1.embed_text", embedReq);
|
||||
if (!expect_contains(embedResp, "\"model\"", "embed_text model key")) return 1;
|
||||
if (!expect_contains(embedResp, "\"vectors\"", "embed_text vectors key")) return 1;
|
||||
if (!expect_contains(embedResp, "[[", "embed_text vectors content")) return 1;
|
||||
|
||||
std::string firstId;
|
||||
const std::string idsAnchor = "\"ids\":[\"";
|
||||
auto idsPos = upsertResp.find(idsAnchor);
|
||||
|
|
@ -63,6 +74,10 @@ const std::string idsAnchor = "\"ids\":[\"";
|
|||
std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
|
||||
if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1;
|
||||
|
||||
const std::string warmCacheReq = R"({"namespace":"tests"})";
|
||||
std::string warmCacheResp = server.dispatch("kom.memory.v1.warm_cache", warmCacheReq);
|
||||
if (!expect_contains(warmCacheResp, "\"queued\"", "warm_cache queued key")) return 1;
|
||||
|
||||
const std::string snapshotResp = server.dispatch("kom.meta.v1.project_snapshot", "{}");
|
||||
if (!expect_contains(snapshotResp, "\"sections\"", "snapshot sections key")) return 1;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue