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
2 changes: 2 additions & 0 deletions docs/en/api/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ This catalog follows the routes actually mounted by the server. Each group headi
| DELETE | `/api/v1/admin/accounts/{account_id}` | Delete an account |
| POST | `/api/v1/admin/accounts/{account_id}/users` | Register a user |
| GET | `/api/v1/admin/accounts/{account_id}/users` | List users |
| GET | `/api/v1/admin/accounts/{account_id}/users/{user_id}/settings` | Get a user's memory policy |
| PATCH | `/api/v1/admin/accounts/{account_id}/users/{user_id}/settings` | Update a user's memory policy |
| DELETE | `/api/v1/admin/accounts/{account_id}/users/{user_id}` | Remove a user |
| PUT | `/api/v1/admin/accounts/{account_id}/users/{user_id}/role` | Promote a user to ADMIN |
| POST | `/api/v1/admin/accounts/{account_id}/users/{user_id}/key` | Regenerate a user key |
Expand Down
25 changes: 25 additions & 0 deletions docs/en/api/08-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,31 @@ Content-Type: application/json
Before an existing setting is replaced, it is backed up to
`/local/{account_id}/_system/setting.backup.json`.

### user_settings

ROOT can manage any User and ADMIN can manage Users in its own account. The
User settings endpoint currently allowlists only `memory_policy`. Each target
has its own `enabled` switch and `memory_types` filter. Agent memory types are
self-only; configuring them for `peer` is rejected.

```http
GET /api/v1/admin/accounts/{account_id}/users/{user_id}/settings
PATCH /api/v1/admin/accounts/{account_id}/users/{user_id}/settings
Content-Type: application/json

{
"memory_policy": {
"self": {"enabled": true, "memory_types": ["profile", "experiences"]},
"peer": {"enabled": false, "memory_types": []}
}
}
```

The response contains the explicit `overrides`, the effective `settings`, and
the account-level `agent_evolution_enabled` switch. Updates are backed up to
the User's `settings/user_config.backup.json` before replacement. A Session
without an explicit policy reads the latest User policy when it is committed.

---

### create_account
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/api/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,8 @@ JSON 输出 - 错误:
| DELETE | `/api/v1/admin/accounts/{account_id}` | 删除账号 |
| POST | `/api/v1/admin/accounts/{account_id}/users` | 注册用户 |
| GET | `/api/v1/admin/accounts/{account_id}/users` | 列出用户 |
| GET | `/api/v1/admin/accounts/{account_id}/users/{user_id}/settings` | 获取用户记忆策略 |
| PATCH | `/api/v1/admin/accounts/{account_id}/users/{user_id}/settings` | 更新用户记忆策略 |
| DELETE | `/api/v1/admin/accounts/{account_id}/users/{user_id}` | 移除用户 |
| PUT | `/api/v1/admin/accounts/{account_id}/users/{user_id}/role` | 将用户提升为 ADMIN |
| POST | `/api/v1/admin/accounts/{account_id}/users/{user_id}/key` | 重新生成用户 Key |
Expand Down
24 changes: 24 additions & 0 deletions docs/zh/api/08-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ Content-Type: application/json
覆盖已有配置前,内核会先备份到
`/local/{account_id}/_system/setting.backup.json`。

### user_settings

ROOT 可管理任意 User,ADMIN 仅可管理所属 account 内的 User。User 配置接口当前
仅允许修改 `memory_policy`。`self` 和 `peer` 分别配置 `enabled` 与
`memory_types`;Agent 记忆只允许写入 self,peer 配置 Agent 记忆类型会被拒绝。

```http
GET /api/v1/admin/accounts/{account_id}/users/{user_id}/settings
PATCH /api/v1/admin/accounts/{account_id}/users/{user_id}/settings
Content-Type: application/json

{
"memory_policy": {
"self": {"enabled": true, "memory_types": ["profile", "experiences"]},
"peer": {"enabled": false, "memory_types": []}
}
}
```

响应同时返回显式 `overrides`、生效 `settings` 和 account 级
`agent_evolution_enabled`。更新前会备份到该 User 的
`settings/user_config.backup.json`。未显式配置策略的 Session 在 commit 时读取
该 User 最新策略。

---

### create_account
Expand Down
12 changes: 11 additions & 1 deletion openviking/server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""Server configuration for OpenViking HTTP Server."""

import sys
from typing import Dict, List, Literal, Optional
from typing import Any, Dict, List, Literal, Optional

from pydantic import BaseModel, Field, ValidationError, field_validator

Expand Down Expand Up @@ -103,13 +103,23 @@ class UserConfig(BaseModel):
"""User configuration values that can be defaulted or initialized."""

add_targets: AddTargetsConfig = Field(default_factory=AddTargetsConfig)
memory_policy: Optional[Dict[str, Any]] = None
agent_evolution: DeprecatedUserAgentEvolutionConfig = Field(
default_factory=DeprecatedUserAgentEvolutionConfig,
exclude=True,
)

model_config = {"extra": "forbid"}

@field_validator("memory_policy", mode="before")
@classmethod
def validate_memory_policy(cls, value: Any) -> Optional[Dict[str, Any]]:
if value is None:
return None
from openviking.session.memory_policy import MemoryPolicy

return MemoryPolicy.from_dict(value).to_dict()


class MetricsAccountDimensionConfig(BaseModel):
"""Account-dimension configuration for metrics label injection."""
Expand Down
147 changes: 137 additions & 10 deletions openviking/server/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@
from openviking.server.dependencies import get_service
from openviking.server.identity import RequestContext, Role
from openviking.server.models import Response
from openviking.server.user_config import validate_add_targets, write_user_config
from openviking.server.user_config import (
delete_user_config,
read_user_config,
validate_add_targets,
validate_user_memory_policy,
write_user_config,
write_user_memory_policy,
)
from openviking.service.legacy_migration import LegacyDataMigration
from openviking.service.task_store import (
SYSTEM_TASK_ACCOUNT_ID,
Expand All @@ -34,6 +41,8 @@
from openviking.service.task_tracker import (
get_task_tracker,
)
from openviking.session.memory.memory_type_registry import MemoryTypeRegistry
from openviking.session.memory_policy import MemoryPolicy
from openviking.storage.viking_fs import get_viking_fs
from openviking_cli.exceptions import (
FailedPreconditionError,
Expand Down Expand Up @@ -80,6 +89,12 @@ class SetAgentEvolutionRequest(BaseModel):
enabled: bool


class UserSettingsPatch(BaseModel):
memory_policy: dict

model_config = {"extra": "forbid"}


def _agent_evolution_account_id(ctx: RequestContext) -> str:
if ctx.role == Role.ROOT:
return get_openviking_config().default_account
Expand Down Expand Up @@ -177,23 +192,26 @@ def _has_add_targets(user_config: UserConfig | None) -> bool:


def _has_initial_user_config(user_config: UserConfig | None) -> bool:
return _has_add_targets(user_config)
return bool(_has_add_targets(user_config) or (user_config and user_config.memory_policy))


def _validate_initial_user_config(
service,
user_ctx: RequestContext,
user_config: UserConfig | None,
) -> None:
if not _has_add_targets(user_config):
if not _has_initial_user_config(user_config):
return
if service.viking_fs is None:
raise FailedPreconditionError("OpenViking service is not initialized.")
validate_add_targets(
user_config.add_targets,
ctx=user_ctx,
viking_fs=service.viking_fs,
)
if _has_add_targets(user_config):
validate_add_targets(
user_config.add_targets,
ctx=user_ctx,
viking_fs=service.viking_fs,
)
if user_config is not None:
validate_user_memory_policy(user_config.memory_policy)


async def _write_initial_user_config(
Expand All @@ -206,6 +224,34 @@ async def _write_initial_user_config(
await write_user_config(service.viking_fs, user_ctx, user_config)


def _check_user_exists(request: Request, account_id: str, user_id: str) -> None:
manager = _get_api_key_manager(request)
if not manager.has_user(account_id, user_id):
raise NotFoundError(user_id, "user")


async def _user_settings_result(
account_id: str,
user_id: str,
user_config: UserConfig,
) -> dict:
configured = user_config.memory_policy
policy = MemoryPolicy.from_dict(configured)
policy.validate_memory_types(set(MemoryTypeRegistry().list_names(include_disabled=False)))
enabled = await get_service().sessions.get_agent_evolution_enabled(account_id)
effective = policy.resolve(
set(MemoryTypeRegistry().list_names(include_disabled=False)),
agent_evolution_enabled=enabled,
)
return {
"account_id": account_id,
"user_id": user_id,
"settings": {"memory_policy": effective.to_dict()},
"overrides": {"memory_policy": configured} if configured is not None else {},
"agent_evolution_enabled": enabled,
}


async def _run_legacy_migration_task(
task_id: str,
migration: LegacyDataMigration,
Expand Down Expand Up @@ -435,8 +481,35 @@ async def register_user(
str(resolved_role),
seed=body.seed,
)
await service.initialize_user_directories(user_ctx)
await _write_initial_user_config(service, user_ctx, body.user_config)
try:
await service.initialize_user_directories(user_ctx)
await _write_initial_user_config(service, user_ctx, body.user_config)
except Exception:
logger.exception(
"Failed to initialize user %s in account %s; rolling back registration",
body.user_id,
account_id,
)
if service.viking_fs is not None:
try:
await delete_user_config(service.viking_fs, user_ctx)
except Exception:
logger.warning(
"Failed to remove user config while rolling back %s/%s",
account_id,
body.user_id,
exc_info=True,
)
try:
await manager.remove_user(account_id, body.user_id)
except Exception:
logger.warning(
"Failed to remove user registration while rolling back %s/%s",
account_id,
body.user_id,
exc_info=True,
)
raise
result = {
"account_id": account_id,
"user_id": body.user_id,
Expand Down Expand Up @@ -466,6 +539,60 @@ async def list_users(
return Response(status="ok", result=users)


@router.get("/accounts/{account_id}/users/{user_id}/settings")
@require_auth_root_or_admin
async def get_user_settings(
request: Request,
account_id: str = Path(..., description="Account ID"),
user_id: str = Path(..., description="User ID"),
ctx: RequestContext = Depends(get_request_context),
):
"""Return the configured and effective memory policy for one User."""
_check_account_access(ctx, account_id)
_check_account_exists(request, account_id)
_check_user_exists(request, account_id, user_id)
service = get_service()
if service.viking_fs is None:
raise FailedPreconditionError("OpenViking service is not initialized.")
user_ctx = RequestContext(
user=UserIdentifier(account_id, user_id),
role=Role.USER,
)
user_config = await read_user_config(service.viking_fs, user_ctx)
return Response(
status="ok",
result=await _user_settings_result(account_id, user_id, user_config),
)


@router.patch("/accounts/{account_id}/users/{user_id}/settings")
@require_auth_root_or_admin
async def patch_user_settings(
body: UserSettingsPatch,
request: Request,
account_id: str = Path(..., description="Account ID"),
user_id: str = Path(..., description="User ID"),
ctx: RequestContext = Depends(get_request_context),
):
"""Update the allowlisted User memory policy without restarting the server."""
_check_account_access(ctx, account_id)
_check_account_exists(request, account_id)
_check_user_exists(request, account_id, user_id)
service = get_service()
if service.viking_fs is None:
raise FailedPreconditionError("OpenViking service is not initialized.")
user_ctx = RequestContext(
user=UserIdentifier(account_id, user_id),
role=Role.USER,
)
await write_user_memory_policy(service.viking_fs, user_ctx, body.memory_policy)
user_config = await read_user_config(service.viking_fs, user_ctx)
return Response(
status="ok",
result=await _user_settings_result(account_id, user_id, user_config),
)


@router.delete("/accounts/{account_id}/users/{user_id}")
@require_auth_root_or_admin
async def remove_user(
Expand Down
Loading
Loading