Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/en/guides/22-gateway-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ Each request's detail in the request log shows the window number, whether the mo

**Isolation by user.** Each gateway key is bound to one OpenViking user. The gateway searches memory, saves conversations and calls tools with that user's own OpenViking key, so it can never do more than the user can, and each user's conversation state is kept separate. Sessions and memories always live in OpenViking; the gateway keeps only the state it needs to continue conversations.

**What the gateway stores.** `storage_path` holds two SQLite databases. Only the system user that runs the gateway can read the files, and with [encryption](#encryption) on, every stored value is encrypted as well:
**What the gateway stores.** `storage_path` holds two SQLite databases (for an embedded gateway, `storage.workspace/gateway` by default; see [Configuration reference](#configuration-reference)). Only the system user that runs the gateway can read the files, and with [encryption](#encryption) on, every stored value is encrypted as well:

- `management.sqlite3`: upstreams, context profiles, gateway keys, request logs, and which upstream created each Responses response. This includes upstream API keys, header values and the OpenViking keys bound to gateway keys, which are in plain text when encryption is off; gateway keys themselves are stored only as a hash and a short prefix.
- `kernel.sqlite3`: the state of each conversation, including the memory text added to messages (needed for replay), OpenViking tool rounds, summaries and conversation text waiting to be saved.
Expand Down Expand Up @@ -661,7 +661,7 @@ Both forms use the same fields; they differ only in where the file lives and how
| `workers` | Standalone | `1` | Worker processes, 1–64 |
| `encryption` | Standalone | not set (plain text) | Same fields as the `encryption` section of `ov.conf`; see [Encryption](#encryption). Embedded gateways use the server's section |
| `public_url` | Both | empty | The external address clients use, without `/gateway` (the gateway appends it); when empty, an embedded gateway uses the server's public address; with neither, setup guides use the request's origin and file uploads do not work |
| `storage_path` | Both | `~/.openviking/gateway` | Local storage directory |
| `storage_path` | Both | Embedded: `gateway` under `storage.workspace`; standalone: `~/.openviking/gateway` | Local storage directory. When unset, an embedded gateway keeps its data with the server's workspace, so a container that mounts a volume only on the workspace keeps it too; if `~/.openviking/gateway` already holds gateway data, the gateway keeps using it and says so in the startup log |
| `management_key` | Both | empty | Optional gateway management key, at least 32 characters |
| `min_server_version` | Both | `0.4.16` | Lowest OpenViking Server version a standalone gateway accepts |
| `session_ttl_days` | Both | `30` | Days of inactivity before a conversation expires; minimum 1 |
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/guides/22-gateway-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ Token 预算只计算模型使用 OpenViking 工具时新增的调用、结果

**按用户隔离。** 每把网关密钥绑定一位 OpenViking 用户。网关用这位用户自己的 OpenViking 密钥检索记忆、保存对话和调用工具,所以能做的事不会超出用户本人的权限;各用户的对话状态互相隔离。会话和记忆的正本始终在 OpenViking,网关只保存让对话继续所需的状态。

**网关存储什么。** `storage_path` 下有两个 SQLite 数据库,文件只有运行网关的系统用户能读取;开启[加密](#加密)后,存储的每个值也都会加密:
**网关存储什么。** `storage_path` 下有两个 SQLite 数据库(嵌入网关默认在 `storage.workspace/gateway`,见[配置参考](#配置参考)),文件只有运行网关的系统用户能读取;开启[加密](#加密)后,存储的每个值也都会加密:

- `management.sqlite3`:上游、上下文配置、网关密钥、请求日志,以及每个 Responses 响应由哪个上游创建。其中包括上游 API Key、请求头的值,以及网关密钥绑定的 OpenViking 密钥,加密关闭时它们以明文存储;网关密钥本身只存哈希和一段短前缀。
- `kernel.sqlite3`:每段对话的状态,包括补充到消息中的记忆原文(原样放回需要它)、OpenViking 工具的往返记录、摘要,以及等待保存的对话文本。
Expand Down Expand Up @@ -661,7 +661,7 @@ Token 预算只计算模型使用 OpenViking 工具时新增的调用、结果
| `workers` | 独立 | `1` | worker 进程数,1–64 |
| `encryption` | 独立 | 不设置(明文) | 字段同 `ov.conf` 的 `encryption` 段,见[加密](#加密);嵌入网关沿用 Server 的设置 |
| `public_url` | 两者 | 空 | 客户端访问的对外地址,不带 `/gateway`(网关自己加);嵌入时为空则沿用 Server 的对外地址;都没有时接入指南用请求来源地址,文件上传不可用 |
| `storage_path` | 两者 | `~/.openviking/gateway` | 本地存储目录 |
| `storage_path` | 两者 | 嵌入:`storage.workspace` 下的 `gateway`;独立:`~/.openviking/gateway` | 本地存储目录。嵌入网关不设置时跟随 Server 的数据目录,容器只把卷挂在数据目录上也不会丢;`~/.openviking/gateway` 里已经有网关数据时继续用它,并在启动日志里提示 |
| `management_key` | 两者 | 空 | 可选的网关管理密钥,至少 32 个字符 |
| `min_server_version` | 两者 | `0.4.16` | 独立网关要求的 OpenViking Server 最低版本 |
| `session_ttl_days` | 两者 | `30` | 对话闲置多少天后过期,最小 1 |
Expand Down
36 changes: 35 additions & 1 deletion openviking/server/gateway_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
"""Construction of the optional embedded gateway, and the route class /mcp shares."""

import os
from pathlib import Path

from starlette.routing import Match, Route

from openviking.server.config import get_server_url_from_server_data
from openviking_cli.utils.logger import get_logger

logger = get_logger(__name__)

# Where the gateway stored its databases before it followed the server's workspace.
LEGACY_STORAGE_PATH = "~/.openviking/gateway"
GATEWAY_DATABASES = ("management.sqlite3", "kernel.sqlite3")


class ScopedRoute(Route):
Expand All @@ -20,6 +28,28 @@ def matches(self, scope):
return match, child_scope


def embedded_storage_path(config, workspace):
"""Directory for the embedded gateway's databases.

An explicit ``storage_path`` (ov.conf or ``OPENVIKING_GATEWAY_STORAGE_PATH``) always wins.
Otherwise the data lives under the server's ``storage.workspace``, so it persists wherever
OpenViking's own data persists, for example on a container volume that does not cover
``$HOME``. A server whose gateway already wrote to the old home-directory default keeps
using that directory, so an upgrade never hides existing upstreams, profiles or keys.
"""
if "storage_path" in config.model_fields_set or not workspace:
return config.storage_path
legacy = Path(LEGACY_STORAGE_PATH).expanduser()
if any((legacy / name).exists() for name in GATEWAY_DATABASES):
logger.warning(
"OpenViking Gateway data is in %s, outside storage.workspace. Set gateway.storage_path "
"if that directory is not persistent; see docs/en/guides/22-gateway-operations.md",
legacy,
)
return str(legacy)
return str(Path(workspace).expanduser() / "gateway")


def create_embedded_gateway(openviking_config, server_config, service_source):
config = openviking_config.gateway
if not config.enabled:
Expand All @@ -40,7 +70,11 @@ async def vlm_source(account_id):
# The gateway shares the server's encryption settings; None when OV encryption is disabled.
encryption = {"encryption": openviking_config.encryption.model_dump()}
# The embedded gateway ships with this server, so the minimum-version gate never applies.
update = {"min_server_version": "0"}
workspace = getattr(getattr(openviking_config, "storage", None), "workspace", None)
update = {
"min_server_version": "0",
"storage_path": embedded_storage_path(config, workspace),
}
# Without its own public_url the gateway shares the server's public address, which
# MCP upload links already use (env first, then ov.conf).
if not config.public_url and (
Expand Down
43 changes: 43 additions & 0 deletions tests/server/test_gateway_embedded.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,49 @@ def public_url(gateway):
assert public_url(own) == "https://gw.example.com"


def embedded_storage(monkeypatch, gateway, workspace):
monkeypatch.setattr(
"openviking_gateway.app.create_app", lambda config, **kwargs: {"config": config, **kwargs}
)
config = openviking_config(gateway)
config.storage = SimpleNamespace(workspace=str(workspace))
options = gateway_host.create_embedded_gateway(
config, SimpleNamespace(host="127.0.0.1", port=1987, public_base_url=None), lambda: None
)
return options["config"].storage_path


def test_embedded_gateway_stores_data_in_the_server_workspace(tmp_path, monkeypatch):
# A container volume often covers only the workspace, not $HOME.
monkeypatch.setenv("HOME", str(tmp_path / "home"))
monkeypatch.delenv("OPENVIKING_GATEWAY_STORAGE_PATH", raising=False)
path = embedded_storage(monkeypatch, EmbeddedGatewayConfig(enabled=True), tmp_path / "data")
assert path == str(tmp_path / "data" / "gateway")


def test_embedded_gateway_keeps_data_already_in_the_home_directory(tmp_path, monkeypatch):
legacy = tmp_path / "home" / ".openviking" / "gateway"
legacy.mkdir(parents=True)
(legacy / "management.sqlite3").touch()
monkeypatch.setenv("HOME", str(tmp_path / "home"))
monkeypatch.delenv("OPENVIKING_GATEWAY_STORAGE_PATH", raising=False)
path = embedded_storage(monkeypatch, EmbeddedGatewayConfig(enabled=True), tmp_path / "data")
assert path == str(legacy)


def test_explicit_gateway_storage_path_wins(tmp_path, monkeypatch):
legacy = tmp_path / "home" / ".openviking" / "gateway"
legacy.mkdir(parents=True)
(legacy / "kernel.sqlite3").touch()
monkeypatch.setenv("HOME", str(tmp_path / "home"))
monkeypatch.delenv("OPENVIKING_GATEWAY_STORAGE_PATH", raising=False)
configured = EmbeddedGatewayConfig(enabled=True, storage_path=str(tmp_path / "conf"))
assert embedded_storage(monkeypatch, configured, tmp_path / "data") == str(tmp_path / "conf")
monkeypatch.setenv("OPENVIKING_GATEWAY_STORAGE_PATH", str(tmp_path / "env"))
from_env = EmbeddedGatewayConfig.model_validate({"enabled": True})
assert embedded_storage(monkeypatch, from_env, tmp_path / "data") == str(tmp_path / "env")


@pytest.fixture
async def model_server():
from aiohttp import web
Expand Down
Loading