Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 0376c0251d feat(admin): add /admin config surface (API + control plane)
Server-level admin surface, gated by HINDSIGHT_API_ENABLE_ADMIN_API with an
optional, independent HINDSIGHT_API_ADMIN_TOKEN:

- API: GET /admin/config returns the resolved HindsightConfig with credentials
  redacted (set -> "***", unset -> null) via the credential denylist plus a
  name-suffix heuristic; features.admin_api exposed on /version.
- Control plane: top-level /admin page reusing the shared (now data-driven)
  Sidebar with a single "Configuration" item + a dedicated AdminHeader; server
  proxy forwards an independent HINDSIGHT_CP_ADMIN_TOKEN to /admin/*.
- Regenerated OpenAPI spec + Python/TS/Go clients; docs + .env.example; admin
  i18n across all locales.

Groundwork for the visibility leg of #2034. The connectivity test + health
surfacing land separately as a per-bank health endpoint.
2026-06-09 15:38:13 +02:00
41 changed files with 1875 additions and 45 deletions
+17
View File
@@ -159,3 +159,20 @@ HINDSIGHT_API_LOG_LEVEL=info
# When set, visitors see a login page and must enter the key before
# accessing the dashboard or any /api/* routes (except /api/health).
# HINDSIGHT_CP_ACCESS_KEY=your-shared-secret-key
# Optional: Token the CP forwards to the dataplane admin API (/admin/*).
# Must match HINDSIGHT_API_ADMIN_TOKEN below. Leave unset for an open admin API.
# HINDSIGHT_CP_ADMIN_TOKEN=your-admin-token
# -----------------------------------------------------------------------------
# Admin surface (Optional, server-level)
# -----------------------------------------------------------------------------
# Enable the admin API (GET /admin/config) and the Control Plane /admin page.
# Off by default — the admin surface is invisible (404) until enabled.
# HINDSIGHT_API_ENABLE_ADMIN_API=true
# Optional: Require this bearer token for the admin API. When unset, the admin
# API is open (once enabled). When set, callers must send
# `Authorization: Bearer <token>`. Independent of the tenant API key.
# HINDSIGHT_API_ADMIN_TOKEN=your-admin-token
+84 -1
View File
@@ -6,6 +6,8 @@ the FastAPI application with all API endpoints.
"""
import asyncio
import dataclasses
import hmac
import json
import logging
import re
@@ -79,7 +81,7 @@ def FieldWithDefault(default_factory: Callable, **kwargs) -> Any:
return Field(default_factory=default_factory, json_schema_extra=json_extra, **kwargs)
from hindsight_api.config import get_config
from hindsight_api.config import _get_raw_config, get_config
from hindsight_api.engine.memory_engine import Budget, _current_schema, _get_tiktoken_encoding, fq_table
from hindsight_api.engine.providers.none_llm import LLMNotAvailableError
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES, MemoryFact, TokenUsage
@@ -1206,6 +1208,33 @@ class BankConfigResponse(BaseModel):
overrides: dict[str, Any] = Field(description="Bank-specific configuration overrides only (Python field names)")
class AdminConfigResponse(BaseModel):
"""Response model for the server-level (admin) configuration view.
Returns the resolved ``HindsightConfig`` as a flat dict keyed by Python field
name. Credential fields (API keys, tokens, service-account keys, base URLs) are
masked: present as ``"***"`` when set and ``None`` when unset, so an operator can
see which credentials are configured without ever seeing their values.
"""
config: dict[str, Any] = Field(
description="Resolved server-level configuration (Python field names); credentials are redacted"
)
# Name suffixes that mark a config field as secret-bearing. Used in addition to
# HindsightConfig._CREDENTIAL_FIELDS so the admin config view never leaks a provider
# credential even if the (denylist) credential set misses a field. Suffixes are
# singular on purpose — "_token" must not also match value-bearing "_tokens" fields
# like recall_max_tokens.
_SENSITIVE_FIELD_SUFFIXES = ("_api_key", "_token", "_secret", "_access_key", "_account_key", "_password")
def _is_sensitive_config_field(field_name: str, credential_fields: set[str]) -> bool:
"""Whether a config field must be redacted in the admin view."""
return field_name in credential_fields or field_name.endswith(_SENSITIVE_FIELD_SUFFIXES)
class GraphDataResponse(BaseModel):
"""Response model for graph data endpoint."""
@@ -2413,6 +2442,7 @@ class FeaturesInfo(BaseModel):
mcp: bool = Field(description="Whether MCP (Model Context Protocol) server is enabled")
worker: bool = Field(description="Whether the background worker is enabled")
bank_config_api: bool = Field(description="Whether per-bank configuration API is enabled")
admin_api: bool = Field(description="Whether the admin API (/admin) is enabled")
file_upload_api: bool = Field(description="Whether file upload/conversion API is enabled")
document_export_api: bool = Field(description="Whether the document export endpoint is enabled")
document_import_api: bool = Field(description="Whether the document import endpoint is enabled")
@@ -2971,6 +3001,31 @@ def _register_routes(app: FastAPI):
api_key = authorization.strip()
return RequestContext(api_key=api_key)
def require_admin(authorization: str | None = Header(default=None)) -> None:
"""Guard for the admin surface.
- 404 when the admin API is disabled (so the surface is invisible by default).
- When ``HINDSIGHT_API_ADMIN_TOKEN`` is set, require it as a bearer token
(or a bare token) and reject with 401 otherwise. When unset, the admin API
is open (auth is optional) consistent with the rest of the deployment.
"""
config = _get_raw_config()
if not config.enable_admin_api:
raise HTTPException(
status_code=404,
detail="Admin API is disabled. Set HINDSIGHT_API_ENABLE_ADMIN_API=true to enable.",
)
expected = config.admin_api_token
if expected:
token = None
if authorization:
if authorization.lower().startswith("bearer "):
token = authorization[7:].strip()
else:
token = authorization.strip()
if not token or not hmac.compare_digest(token, expected):
raise HTTPException(status_code=401, detail="Invalid or missing admin token")
def precheck_for(operation: str):
"""
Build a FastAPI dependency that runs ``OperationValidator.precheck``.
@@ -3080,6 +3135,7 @@ def _register_routes(app: FastAPI):
mcp=config.mcp_enabled,
worker=config.worker_enabled,
bank_config_api=config.enable_bank_config_api,
admin_api=config.enable_admin_api,
file_upload_api=config.enable_file_upload_api,
document_export_api=config.enable_document_export_api,
document_import_api=config.enable_document_import_api,
@@ -3088,6 +3144,33 @@ def _register_routes(app: FastAPI):
),
)
@app.get(
"/admin/config",
response_model=AdminConfigResponse,
summary="Get resolved server-level configuration",
description="Returns the resolved server-level configuration with credentials redacted. "
"Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.",
tags=["Admin"],
operation_id="get_admin_config",
dependencies=[Depends(require_admin)],
)
async def admin_config_endpoint() -> AdminConfigResponse:
"""Expose the resolved ``HindsightConfig`` for operator inspection.
Sensitive fields (the credential denylist plus any field whose name ends in a
secret-bearing suffix see ``_is_sensitive_config_field``) are masked so values
never leave the server: ``"***"`` when set, ``None`` when unset. All other fields
are returned as-is.
"""
config = _get_raw_config()
credential_fields = type(config).get_credential_fields()
raw = dataclasses.asdict(config)
redacted = {
key: ("***" if value is not None else None) if _is_sensitive_config_field(key, credential_fields) else value
for key, value in raw.items()
}
return AdminConfigResponse(config=redacted)
@app.get(
"/metrics",
summary="Prometheus metrics endpoint",
@@ -351,6 +351,8 @@ ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
ENV_MCP_ENABLED_TOOLS = "HINDSIGHT_API_MCP_ENABLED_TOOLS"
ENV_MCP_STATELESS = "HINDSIGHT_API_MCP_STATELESS"
ENV_ENABLE_BANK_CONFIG_API = "HINDSIGHT_API_ENABLE_BANK_CONFIG_API"
ENV_ENABLE_ADMIN_API = "HINDSIGHT_API_ENABLE_ADMIN_API"
ENV_ADMIN_API_TOKEN = "HINDSIGHT_API_ADMIN_TOKEN"
ENV_DEFAULT_BANK_TEMPLATE = "HINDSIGHT_API_DEFAULT_BANK_TEMPLATE"
ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
ENV_RECALL_MAX_CONCURRENT = "HINDSIGHT_API_RECALL_MAX_CONCURRENT"
@@ -792,6 +794,8 @@ DEFAULT_MCP_ENABLED = True
DEFAULT_MCP_ENABLED_TOOLS: list[str] | None = None # None = all tools enabled
DEFAULT_MCP_STATELESS = False # False = stateful (supports SSE/GET); True = stateless (POST-only)
DEFAULT_ENABLE_BANK_CONFIG_API = True
DEFAULT_ENABLE_ADMIN_API = False # Admin surface (server config view) is off unless explicitly enabled
DEFAULT_ADMIN_API_TOKEN: str | None = None # None = admin API open (when enabled); set = required bearer token
DEFAULT_DEFAULT_BANK_TEMPLATE: dict | None = None # BankTemplateManifest dict applied to newly-created banks
DEFAULT_GRAPH_RETRIEVER = "link_expansion"
DEFAULT_RECALL_MAX_CONCURRENT = 32 # Max concurrent recall operations per worker
@@ -1387,6 +1391,10 @@ class HindsightConfig:
mcp_enabled_tools: list[str] | None # None = all tools; explicit list = allowlist
mcp_stateless: bool # True = stateless HTTP (POST-only); False = stateful (supports GET/SSE)
enable_bank_config_api: bool
# Admin surface (static, server-level only). enable_admin_api gates the /admin API +
# control-plane page; admin_api_token (when set) is the required bearer token.
enable_admin_api: bool
admin_api_token: str | None
# Default bank template (static, server-level only). When set, the manifest is applied
# to every newly-created bank, overriding the env/config defaults for any fields it sets.
default_bank_template: dict | None
@@ -1612,6 +1620,8 @@ class HindsightConfig:
# File parser credentials
"file_parser_iris_token",
"file_parser_llama_parse_api_key",
# Admin surface token (never exposed via the admin config view itself)
"admin_api_token",
}
# CONFIGURABLE_FIELDS: Safe behavioral settings that can be customized per-tenant/bank
@@ -2218,6 +2228,8 @@ class HindsightConfig:
mcp_stateless=os.getenv(ENV_MCP_STATELESS, str(DEFAULT_MCP_STATELESS)).lower() == "true",
enable_bank_config_api=os.getenv(ENV_ENABLE_BANK_CONFIG_API, str(DEFAULT_ENABLE_BANK_CONFIG_API)).lower()
== "true",
enable_admin_api=os.getenv(ENV_ENABLE_ADMIN_API, str(DEFAULT_ENABLE_ADMIN_API)).lower() == "true",
admin_api_token=os.getenv(ENV_ADMIN_API_TOKEN) or DEFAULT_ADMIN_API_TOKEN,
default_bank_template=_parse_default_bank_template(os.getenv(ENV_DEFAULT_BANK_TEMPLATE)),
# Recall
graph_retriever=os.getenv(ENV_GRAPH_RETRIEVER, DEFAULT_GRAPH_RETRIEVER),
+128
View File
@@ -0,0 +1,128 @@
"""Tests for the admin surface: GET /admin/config + the admin_api feature flag.
These are deterministic (no LLM): the endpoint only reads server-level config. We
toggle env vars + clear the config cache to exercise the enable flag, the optional
admin token, and credential redaction.
"""
import httpx
import pytest
import pytest_asyncio
from hindsight_api.api import create_app
from hindsight_api.config import clear_config_cache
@pytest_asyncio.fixture
async def admin_client(memory):
"""Async test client for the FastAPI app (mock LLM)."""
app = create_app(memory, initialize_memory=False)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
yield client
def _set_env(monkeypatch, **values: str | None) -> None:
"""Set/unset env vars and reset the cached config so the next read reflects them."""
for key, value in values.items():
if value is None:
monkeypatch.delenv(key, raising=False)
else:
monkeypatch.setenv(key, value)
clear_config_cache()
@pytest.fixture(autouse=True)
def _restore_config_cache():
"""Ensure the global config cache is reset after each test."""
yield
clear_config_cache()
@pytest.mark.asyncio
async def test_admin_config_disabled_by_default(admin_client, monkeypatch):
"""When the admin API is disabled (default), the endpoint is invisible (404)."""
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API=None, HINDSIGHT_API_ADMIN_TOKEN=None)
response = await admin_client.get("/admin/config")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_admin_config_enabled_no_token(admin_client, monkeypatch):
"""When enabled without a token, the endpoint is open and returns config."""
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API="true", HINDSIGHT_API_ADMIN_TOKEN=None)
response = await admin_client.get("/admin/config")
assert response.status_code == 200
config = response.json()["config"]
# A representative spread of non-credential fields should be present.
assert "llm_provider" in config
assert "enable_admin_api" in config
assert config["enable_admin_api"] is True
@pytest.mark.asyncio
async def test_admin_config_redacts_credentials(admin_client, monkeypatch):
"""Credential fields are masked, never returned in cleartext."""
_set_env(
monkeypatch,
HINDSIGHT_API_ENABLE_ADMIN_API="true",
HINDSIGHT_API_ADMIN_TOKEN="s3cret-token",
HINDSIGHT_API_LLM_API_KEY="super-secret-key",
)
response = await admin_client.get("/admin/config", headers={"Authorization": "Bearer s3cret-token"})
assert response.status_code == 200
config = response.json()["config"]
# The configured LLM key is present but masked.
assert config["llm_api_key"] == "***"
assert "super-secret-key" not in response.text
# Provider keys that fall back to the LLM key (and aren't in the credential
# denylist) must also be masked — the view redacts by name, not just the set.
assert config["embeddings_openrouter_api_key"] == "***"
assert config["reranker_openrouter_api_key"] == "***"
# The admin token must never leak through its own config view.
assert config["admin_api_token"] == "***"
assert "s3cret-token" not in response.text
# Value-bearing fields that merely contain "token" in their name (plural) are
# NOT redacted — they carry useful config, not secrets.
assert config["recall_max_tokens"] != "***"
@pytest.mark.asyncio
async def test_admin_config_requires_token_when_set(admin_client, monkeypatch):
"""With a token configured, missing/wrong tokens are rejected; the right one passes."""
_set_env(
monkeypatch,
HINDSIGHT_API_ENABLE_ADMIN_API="true",
HINDSIGHT_API_ADMIN_TOKEN="right-token",
)
missing = await admin_client.get("/admin/config")
assert missing.status_code == 401
wrong = await admin_client.get("/admin/config", headers={"Authorization": "Bearer wrong-token"})
assert wrong.status_code == 401
bearer = await admin_client.get("/admin/config", headers={"Authorization": "Bearer right-token"})
assert bearer.status_code == 200
# A bare token (no "Bearer " prefix) is also accepted.
bare = await admin_client.get("/admin/config", headers={"Authorization": "right-token"})
assert bare.status_code == 200
@pytest.mark.asyncio
async def test_version_reports_admin_api_flag(admin_client, monkeypatch):
"""The /version feature flags track the admin enable flag."""
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API="true")
enabled = await admin_client.get("/version")
assert enabled.json()["features"]["admin_api"] is True
_set_env(monkeypatch, HINDSIGHT_API_ENABLE_ADMIN_API="false")
disabled = await admin_client.get("/version")
assert disabled.json()["features"]["admin_api"] is False
+55
View File
@@ -39,6 +39,36 @@ paths:
summary: Get API version and feature flags
tags:
- Monitoring
/admin/config:
get:
description: "Returns the resolved server-level configuration with credentials\
\ redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN."
operationId: get_admin_config
parameters:
- explode: false
in: header
name: authorization
required: false
schema:
nullable: true
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/AdminConfigResponse'
description: Successful Response
"422":
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Get resolved server-level configuration
tags:
- Admin
/metrics:
get:
description: Exports metrics in Prometheus format for scraping
@@ -3796,6 +3826,26 @@ components:
required:
- content
title: AddBackgroundRequest
AdminConfigResponse:
description: |-
Response model for the server-level (admin) configuration view.
Returns the resolved ``HindsightConfig`` as a flat dict keyed by Python field
name. Credential fields (API keys, tokens, service-account keys, base URLs) are
masked: present as ``"***"`` when set and ``None`` when unset, so an operator can
see which credentials are configured without ever seeing their values.
example:
config:
key: ""
properties:
config:
additionalProperties: {}
description: Resolved server-level configuration (Python field names); credentials
are redacted
title: Config
required:
- config
title: AdminConfigResponse
AsyncOperationSubmitResponse:
description: Response model for submitting an async operation.
example:
@@ -5406,6 +5456,10 @@ components:
description: Whether per-bank configuration API is enabled
title: Bank Config Api
type: boolean
admin_api:
description: Whether the admin API (/admin) is enabled
title: Admin Api
type: boolean
file_upload_api:
description: Whether file upload/conversion API is enabled
title: File Upload Api
@@ -5427,6 +5481,7 @@ components:
title: Llm Trace
type: boolean
required:
- admin_api
- audit_log
- bank_config_api
- document_export_api
+141
View File
@@ -0,0 +1,141 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.8.0
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"bytes"
"context"
"io"
"net/http"
"net/url"
)
// AdminAPIService AdminAPI service
type AdminAPIService service
type ApiGetAdminConfigRequest struct {
ctx context.Context
ApiService *AdminAPIService
authorization *string
}
func (r ApiGetAdminConfigRequest) Authorization(authorization string) ApiGetAdminConfigRequest {
r.authorization = &authorization
return r
}
func (r ApiGetAdminConfigRequest) Execute() (*AdminConfigResponse, *http.Response, error) {
return r.ApiService.GetAdminConfigExecute(r)
}
/*
GetAdminConfig Get resolved server-level configuration
Returns the resolved server-level configuration with credentials redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@return ApiGetAdminConfigRequest
*/
func (a *AdminAPIService) GetAdminConfig(ctx context.Context) ApiGetAdminConfigRequest {
return ApiGetAdminConfigRequest{
ApiService: a,
ctx: ctx,
}
}
// Execute executes the request
// @return AdminConfigResponse
func (a *AdminAPIService) GetAdminConfigExecute(r ApiGetAdminConfigRequest) (*AdminConfigResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodGet
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *AdminConfigResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "AdminAPIService.GetAdminConfig")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/admin/config"
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
if r.authorization != nil {
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 422 {
var v HTTPValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
+3
View File
@@ -49,6 +49,8 @@ type APIClient struct {
// API Services
AdminAPI *AdminAPIService
AuditAPI *AuditAPIService
BankTemplatesAPI *BankTemplatesAPIService
@@ -94,6 +96,7 @@ func NewAPIClient(cfg *Configuration) *APIClient {
c.common.client = c
// API Services
c.AdminAPI = (*AdminAPIService)(&c.common)
c.AuditAPI = (*AuditAPIService)(&c.common)
c.BankTemplatesAPI = (*BankTemplatesAPIService)(&c.common)
c.BanksAPI = (*BanksAPIService)(&c.common)
@@ -0,0 +1,159 @@
/*
Hindsight HTTP API
HTTP API for Hindsight
API version: 0.8.0
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package hindsight
import (
"encoding/json"
"bytes"
"fmt"
)
// checks if the AdminConfigResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &AdminConfigResponse{}
// AdminConfigResponse Response model for the server-level (admin) configuration view. Returns the resolved ``HindsightConfig`` as a flat dict keyed by Python field name. Credential fields (API keys, tokens, service-account keys, base URLs) are masked: present as ``\"***\"`` when set and ``None`` when unset, so an operator can see which credentials are configured without ever seeing their values.
type AdminConfigResponse struct {
// Resolved server-level configuration (Python field names); credentials are redacted
Config map[string]interface{} `json:"config"`
}
type _AdminConfigResponse AdminConfigResponse
// NewAdminConfigResponse instantiates a new AdminConfigResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewAdminConfigResponse(config map[string]interface{}) *AdminConfigResponse {
this := AdminConfigResponse{}
this.Config = config
return &this
}
// NewAdminConfigResponseWithDefaults instantiates a new AdminConfigResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewAdminConfigResponseWithDefaults() *AdminConfigResponse {
this := AdminConfigResponse{}
return &this
}
// GetConfig returns the Config field value
func (o *AdminConfigResponse) GetConfig() map[string]interface{} {
if o == nil {
var ret map[string]interface{}
return ret
}
return o.Config
}
// GetConfigOk returns a tuple with the Config field value
// and a boolean to check if the value has been set.
func (o *AdminConfigResponse) GetConfigOk() (map[string]interface{}, bool) {
if o == nil {
return map[string]interface{}{}, false
}
return o.Config, true
}
// SetConfig sets field value
func (o *AdminConfigResponse) SetConfig(v map[string]interface{}) {
o.Config = v
}
func (o AdminConfigResponse) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o AdminConfigResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["config"] = o.Config
return toSerialize, nil
}
func (o *AdminConfigResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"config",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err;
}
for _, requiredProperty := range(requiredProperties) {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varAdminConfigResponse := _AdminConfigResponse{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
err = decoder.Decode(&varAdminConfigResponse)
if err != nil {
return err
}
*o = AdminConfigResponse(varAdminConfigResponse)
return err
}
type NullableAdminConfigResponse struct {
value *AdminConfigResponse
isSet bool
}
func (v NullableAdminConfigResponse) Get() *AdminConfigResponse {
return v.value
}
func (v *NullableAdminConfigResponse) Set(val *AdminConfigResponse) {
v.value = val
v.isSet = true
}
func (v NullableAdminConfigResponse) IsSet() bool {
return v.isSet
}
func (v *NullableAdminConfigResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableAdminConfigResponse(val *AdminConfigResponse) *NullableAdminConfigResponse {
return &NullableAdminConfigResponse{value: val, isSet: true}
}
func (v NullableAdminConfigResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableAdminConfigResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
+30 -1
View File
@@ -29,6 +29,8 @@ type FeaturesInfo struct {
Worker bool `json:"worker"`
// Whether per-bank configuration API is enabled
BankConfigApi bool `json:"bank_config_api"`
// Whether the admin API (/admin) is enabled
AdminApi bool `json:"admin_api"`
// Whether file upload/conversion API is enabled
FileUploadApi bool `json:"file_upload_api"`
// Whether the document export endpoint is enabled
@@ -47,12 +49,13 @@ type _FeaturesInfo FeaturesInfo
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewFeaturesInfo(observations bool, mcp bool, worker bool, bankConfigApi bool, fileUploadApi bool, documentExportApi bool, documentImportApi bool, auditLog bool, llmTrace bool) *FeaturesInfo {
func NewFeaturesInfo(observations bool, mcp bool, worker bool, bankConfigApi bool, adminApi bool, fileUploadApi bool, documentExportApi bool, documentImportApi bool, auditLog bool, llmTrace bool) *FeaturesInfo {
this := FeaturesInfo{}
this.Observations = observations
this.Mcp = mcp
this.Worker = worker
this.BankConfigApi = bankConfigApi
this.AdminApi = adminApi
this.FileUploadApi = fileUploadApi
this.DocumentExportApi = documentExportApi
this.DocumentImportApi = documentImportApi
@@ -165,6 +168,30 @@ func (o *FeaturesInfo) SetBankConfigApi(v bool) {
o.BankConfigApi = v
}
// GetAdminApi returns the AdminApi field value
func (o *FeaturesInfo) GetAdminApi() bool {
if o == nil {
var ret bool
return ret
}
return o.AdminApi
}
// GetAdminApiOk returns a tuple with the AdminApi field value
// and a boolean to check if the value has been set.
func (o *FeaturesInfo) GetAdminApiOk() (*bool, bool) {
if o == nil {
return nil, false
}
return &o.AdminApi, true
}
// SetAdminApi sets field value
func (o *FeaturesInfo) SetAdminApi(v bool) {
o.AdminApi = v
}
// GetFileUploadApi returns the FileUploadApi field value
func (o *FeaturesInfo) GetFileUploadApi() bool {
if o == nil {
@@ -299,6 +326,7 @@ func (o FeaturesInfo) ToMap() (map[string]interface{}, error) {
toSerialize["mcp"] = o.Mcp
toSerialize["worker"] = o.Worker
toSerialize["bank_config_api"] = o.BankConfigApi
toSerialize["admin_api"] = o.AdminApi
toSerialize["file_upload_api"] = o.FileUploadApi
toSerialize["document_export_api"] = o.DocumentExportApi
toSerialize["document_import_api"] = o.DocumentImportApi
@@ -316,6 +344,7 @@ func (o *FeaturesInfo) UnmarshalJSON(data []byte) (err error) {
"mcp",
"worker",
"bank_config_api",
"admin_api",
"file_upload_api",
"document_export_api",
"document_import_api",
@@ -1,6 +1,7 @@
.openapi-generator-ignore
hindsight_client_api/__init__.py
hindsight_client_api/api/__init__.py
hindsight_client_api/api/admin_api.py
hindsight_client_api/api/audit_api.py
hindsight_client_api/api/bank_templates_api.py
hindsight_client_api/api/banks_api.py
@@ -21,6 +22,7 @@ hindsight_client_api/configuration.py
hindsight_client_api/exceptions.py
hindsight_client_api/models/__init__.py
hindsight_client_api/models/add_background_request.py
hindsight_client_api/models/admin_config_response.py
hindsight_client_api/models/async_operation_submit_response.py
hindsight_client_api/models/audit_log_entry.py
hindsight_client_api/models/audit_log_list_response.py
@@ -17,6 +17,7 @@
__version__ = "0.0.7"
# import apis into sdk package
from hindsight_client_api.api.admin_api import AdminApi
from hindsight_client_api.api.audit_api import AuditApi
from hindsight_client_api.api.bank_templates_api import BankTemplatesApi
from hindsight_client_api.api.banks_api import BanksApi
@@ -45,6 +46,7 @@ from hindsight_client_api.exceptions import ApiException
# import models into sdk package
from hindsight_client_api.models.add_background_request import AddBackgroundRequest
from hindsight_client_api.models.admin_config_response import AdminConfigResponse
from hindsight_client_api.models.async_operation_submit_response import AsyncOperationSubmitResponse
from hindsight_client_api.models.audit_log_entry import AuditLogEntry
from hindsight_client_api.models.audit_log_list_response import AuditLogListResponse
@@ -1,6 +1,7 @@
# flake8: noqa
# import apis into api package
from hindsight_client_api.api.admin_api import AdminApi
from hindsight_client_api.api.audit_api import AuditApi
from hindsight_client_api.api.bank_templates_api import BankTemplatesApi
from hindsight_client_api.api.banks_api import BanksApi
@@ -0,0 +1,301 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.8.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
import warnings
from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt
from typing import Any, Dict, List, Optional, Tuple, Union
from typing_extensions import Annotated
from pydantic import StrictStr
from typing import Optional
from hindsight_client_api.models.admin_config_response import AdminConfigResponse
from hindsight_client_api.api_client import ApiClient, RequestSerialized
from hindsight_client_api.api_response import ApiResponse
from hindsight_client_api.rest import RESTResponseType
class AdminApi:
"""NOTE: This class is auto generated by OpenAPI Generator
Ref: https://openapi-generator.tech
Do not edit the class manually.
"""
def __init__(self, api_client=None) -> None:
if api_client is None:
api_client = ApiClient.get_default()
self.api_client = api_client
@validate_call
async def get_admin_config(
self,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> AdminConfigResponse:
"""Get resolved server-level configuration
Returns the resolved server-level configuration with credentials redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.
:param authorization:
:type authorization: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_admin_config_serialize(
authorization=authorization,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "AdminConfigResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
).data
@validate_call
async def get_admin_config_with_http_info(
self,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[AdminConfigResponse]:
"""Get resolved server-level configuration
Returns the resolved server-level configuration with credentials redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.
:param authorization:
:type authorization: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_admin_config_serialize(
authorization=authorization,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "AdminConfigResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
await response_data.read()
return self.api_client.response_deserialize(
response_data=response_data,
response_types_map=_response_types_map,
)
@validate_call
async def get_admin_config_without_preload_content(
self,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)],
Annotated[StrictFloat, Field(gt=0)]
]
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Get resolved server-level configuration
Returns the resolved server-level configuration with credentials redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.
:param authorization:
:type authorization: str
:param _request_timeout: timeout setting for this request. If one
number provided, it will be total request
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
:type _request_timeout: int, tuple(int, int), optional
:param _request_auth: set to override the auth_settings for an a single
request; this effectively ignores the
authentication in the spec for a single request.
:type _request_auth: dict, optional
:param _content_type: force content-type for the request.
:type _content_type: str, Optional
:param _headers: set to override the headers for a single
request; this effectively ignores the headers
in the spec for a single request.
:type _headers: dict, optional
:param _host_index: set to override the host_index for a single
request; this effectively ignores the host_index
in the spec for a single request.
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
_param = self._get_admin_config_serialize(
authorization=authorization,
_request_auth=_request_auth,
_content_type=_content_type,
_headers=_headers,
_host_index=_host_index
)
_response_types_map: Dict[str, Optional[str]] = {
'200': "AdminConfigResponse",
'422': "HTTPValidationError",
}
response_data = await self.api_client.call_api(
*_param,
_request_timeout=_request_timeout
)
return response_data.response
def _get_admin_config_serialize(
self,
authorization,
_request_auth,
_content_type,
_headers,
_host_index,
) -> RequestSerialized:
_host = None
_collection_formats: Dict[str, str] = {
}
_path_params: Dict[str, str] = {}
_query_params: List[Tuple[str, str]] = []
_header_params: Dict[str, Optional[str]] = _headers or {}
_form_params: List[Tuple[str, str]] = []
_files: Dict[
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
] = {}
_body_params: Optional[bytes] = None
# process the path parameters
# process the query parameters
# process the header parameters
if authorization is not None:
_header_params['authorization'] = authorization
# process the form parameters
# process the body parameter
# set the HTTP header `Accept`
if 'Accept' not in _header_params:
_header_params['Accept'] = self.api_client.select_header_accept(
[
'application/json'
]
)
# authentication setting
_auth_settings: List[str] = [
]
return self.api_client.param_serialize(
method='GET',
resource_path='/admin/config',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
body=_body_params,
post_params=_form_params,
files=_files,
auth_settings=_auth_settings,
collection_formats=_collection_formats,
_host=_host,
_request_auth=_request_auth
)
@@ -15,6 +15,7 @@
# import models into model package
from hindsight_client_api.models.add_background_request import AddBackgroundRequest
from hindsight_client_api.models.admin_config_response import AdminConfigResponse
from hindsight_client_api.models.async_operation_submit_response import AsyncOperationSubmitResponse
from hindsight_client_api.models.audit_log_entry import AuditLogEntry
from hindsight_client_api.models.audit_log_list_response import AuditLogListResponse
@@ -0,0 +1,87 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.8.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field
from typing import Any, ClassVar, Dict, List
from typing import Optional, Set
from typing_extensions import Self
class AdminConfigResponse(BaseModel):
"""
Response model for the server-level (admin) configuration view. Returns the resolved ``HindsightConfig`` as a flat dict keyed by Python field name. Credential fields (API keys, tokens, service-account keys, base URLs) are masked: present as ``\"***\"`` when set and ``None`` when unset, so an operator can see which credentials are configured without ever seeing their values.
""" # noqa: E501
config: Dict[str, Any] = Field(description="Resolved server-level configuration (Python field names); credentials are redacted")
__properties: ClassVar[List[str]] = ["config"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of AdminConfigResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of AdminConfigResponse from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"config": obj.get("config")
})
return _obj
@@ -30,12 +30,13 @@ class FeaturesInfo(BaseModel):
mcp: StrictBool = Field(description="Whether MCP (Model Context Protocol) server is enabled")
worker: StrictBool = Field(description="Whether the background worker is enabled")
bank_config_api: StrictBool = Field(description="Whether per-bank configuration API is enabled")
admin_api: StrictBool = Field(description="Whether the admin API (/admin) is enabled")
file_upload_api: StrictBool = Field(description="Whether file upload/conversion API is enabled")
document_export_api: StrictBool = Field(description="Whether the document export endpoint is enabled")
document_import_api: StrictBool = Field(description="Whether the document import endpoint is enabled")
audit_log: StrictBool = Field(description="Whether audit logging is enabled")
llm_trace: StrictBool = Field(description="Whether per-bank LLM request tracing is enabled")
__properties: ClassVar[List[str]] = ["observations", "mcp", "worker", "bank_config_api", "file_upload_api", "document_export_api", "document_import_api", "audit_log", "llm_trace"]
__properties: ClassVar[List[str]] = ["observations", "mcp", "worker", "bank_config_api", "admin_api", "file_upload_api", "document_export_api", "document_import_api", "audit_log", "llm_trace"]
model_config = ConfigDict(
populate_by_name=True,
@@ -92,6 +93,7 @@ class FeaturesInfo(BaseModel):
"mcp": obj.get("mcp"),
"worker": obj.get("worker"),
"bank_config_api": obj.get("bank_config_api"),
"admin_api": obj.get("admin_api"),
"file_upload_api": obj.get("file_upload_api"),
"document_export_api": obj.get("document_export_api"),
"document_import_api": obj.get("document_import_api"),
@@ -65,6 +65,9 @@ import type {
FileRetainData,
FileRetainErrors,
FileRetainResponses,
GetAdminConfigData,
GetAdminConfigErrors,
GetAdminConfigResponses,
GetAgentStatsData,
GetAgentStatsErrors,
GetAgentStatsResponses,
@@ -262,6 +265,19 @@ export const getVersion = <ThrowOnError extends boolean = false>(
...options,
});
/**
* Get resolved server-level configuration
*
* Returns the resolved server-level configuration with credentials redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.
*/
export const getAdminConfig = <ThrowOnError extends boolean = false>(
options?: Options<GetAdminConfigData, ThrowOnError>
) =>
(options?.client ?? client).get<GetAdminConfigResponses, GetAdminConfigErrors, ThrowOnError>({
url: "/admin/config",
...options,
});
/**
* Prometheus metrics endpoint
*
@@ -24,6 +24,27 @@ export type AddBackgroundRequest = {
update_disposition?: boolean;
};
/**
* AdminConfigResponse
*
* Response model for the server-level (admin) configuration view.
*
* Returns the resolved ``HindsightConfig`` as a flat dict keyed by Python field
* name. Credential fields (API keys, tokens, service-account keys, base URLs) are
* masked: present as ``"***"`` when set and ``None`` when unset, so an operator can
* see which credentials are configured without ever seeing their values.
*/
export type AdminConfigResponse = {
/**
* Config
*
* Resolved server-level configuration (Python field names); credentials are redacted
*/
config: {
[key: string]: unknown;
};
};
/**
* AsyncOperationSubmitResponse
*
@@ -1658,6 +1679,12 @@ export type FeaturesInfo = {
* Whether per-bank configuration API is enabled
*/
bank_config_api: boolean;
/**
* Admin Api
*
* Whether the admin API (/admin) is enabled
*/
admin_api: boolean;
/**
* File Upload Api
*
@@ -3783,6 +3810,37 @@ export type GetVersionResponses = {
export type GetVersionResponse = GetVersionResponses[keyof GetVersionResponses];
export type GetAdminConfigData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
};
path?: never;
query?: never;
url: "/admin/config";
};
export type GetAdminConfigErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetAdminConfigError = GetAdminConfigErrors[keyof GetAdminConfigErrors];
export type GetAdminConfigResponses = {
/**
* Successful Response
*/
200: AdminConfigResponse;
};
export type GetAdminConfigResponse = GetAdminConfigResponses[keyof GetAdminConfigResponses];
export type MetricsEndpointMetricsGetData = {
body?: never;
path?: never;
@@ -0,0 +1,48 @@
"use client";
import { useTranslations } from "next-intl";
import { Settings } from "lucide-react";
import { AdminHeader } from "@/components/admin-header";
import { Sidebar } from "@/components/sidebar";
import { FeatureNotEnabled } from "@/components/feature-not-enabled";
import { AdminConfigView } from "@/components/admin-config-view";
import { useFeatures } from "@/lib/features-context";
export default function AdminPage() {
const t = useTranslations("admin");
const { features, loading } = useFeatures();
return (
<div className="min-h-screen bg-background flex flex-col">
<AdminHeader />
<div className="flex flex-1 min-h-0">
<Sidebar
items={[
{ id: "configuration", label: t("configHeading"), icon: Settings, href: "/admin" },
]}
currentTab="configuration"
/>
<main className="flex-1 px-6 py-6 overflow-y-auto">
<div className="max-w-5xl w-full mx-auto">
{loading ? null : features?.admin_api ? (
<AdminConfigView />
) : (
<FeatureNotEnabled
title={t("notEnabledTitle")}
description={t.rich("notEnabledDescription", {
envVar: () => (
<code className="px-1 py-0.5 bg-muted rounded text-xs">
HINDSIGHT_API_ENABLE_ADMIN_API=true
</code>
),
})}
/>
)}
</div>
</main>
</div>
</div>
);
}
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { BankSelector } from "@/components/bank-selector";
import { Sidebar } from "@/components/sidebar";
import type { SidebarItem } from "@/components/sidebar";
import { DataView } from "@/components/data-view";
import { DocumentsView } from "@/components/documents-view";
import { EntitiesView } from "@/components/entities-view";
@@ -42,7 +43,21 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Brain, Download, Trash2, Loader2, MoreVertical, Pencil, RotateCcw } from "lucide-react";
import {
Brain,
Download,
Trash2,
Loader2,
MoreVertical,
Pencil,
RotateCcw,
Database,
Search,
Sparkles,
FileText,
Users,
Settings,
} from "lucide-react";
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
type DataSubTab = "world" | "experience" | "observations" | "mental-models";
@@ -53,6 +68,7 @@ export default function BankPage() {
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations("bank");
const tSidebar = useTranslations("bank.sidebar");
const tCommon = useTranslations("common");
const { features } = useFeatures();
const { currentBank: bankId, setCurrentBank, loadBanks } = useBank();
@@ -80,6 +96,47 @@ export default function BankPage() {
router.push(bankRoute(bankId, `?view=${tab}`));
};
const sidebarItems: SidebarItem[] = bankId
? [
{
id: "data",
label: tSidebar("memories"),
icon: Database,
href: bankRoute(bankId, "?view=data"),
},
{
id: "recall",
label: tSidebar("recall"),
icon: Search,
href: bankRoute(bankId, "?view=recall"),
},
{
id: "reflect",
label: tSidebar("reflect"),
icon: Sparkles,
href: bankRoute(bankId, "?view=reflect"),
},
{
id: "documents",
label: tSidebar("documents"),
icon: FileText,
href: bankRoute(bankId, "?view=documents"),
},
{
id: "entities",
label: tSidebar("entities"),
icon: Users,
href: bankRoute(bankId, "?view=entities"),
},
{
id: "profile",
label: t("bankConfiguration"),
icon: Settings,
href: bankRoute(bankId, "?view=profile"),
},
]
: [];
const handleDataSubTabChange = (newSubTab: DataSubTab) => {
if (!bankId) return;
router.push(bankRoute(bankId, `?view=data&subTab=${newSubTab}`));
@@ -169,7 +226,13 @@ export default function BankPage() {
<BankSelector />
<div className="flex flex-1 overflow-hidden">
<Sidebar currentTab={view} onTabChange={handleTabChange} />
{bankId && (
<Sidebar
items={sidebarItems}
currentTab={view}
onTabChange={(id) => handleTabChange(id as NavItem)}
/>
)}
<main className="flex-1 overflow-y-auto">
<div className="p-6">
@@ -0,0 +1,41 @@
import { NextResponse } from "next/server";
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
import { DATAPLANE_URL, getAdminHeaders } from "@/lib/hindsight-client";
/**
* Proxy for the dataplane admin config endpoint (`GET /admin/config`).
*
* Forwards the independent admin token (HINDSIGHT_CP_ADMIN_TOKEN) rather than the
* tenant API key. The dataplane returns 404 when the admin API is disabled and 401
* when a token is required but missing/wrong — both are surfaced to the caller.
*/
export async function GET(request: Request) {
try {
const response = await fetch(`${DATAPLANE_URL}/admin/config`, {
headers: getAdminHeaders({ Accept: "application/json" }),
cache: "no-store",
});
if (!response.ok) {
return NextResponse.json(
localizeApiErrorPayload(request, {
error: "Failed to get admin config",
errorKey: "api.errors.admin.config.fetch",
}),
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error getting admin config:", error);
return NextResponse.json(
localizeApiErrorPayload(request, {
error: "Failed to get admin config",
errorKey: "api.errors.admin.config.fetch",
}),
{ status: 500 }
);
}
}
@@ -0,0 +1,132 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { Loader2, Search } from "lucide-react";
import { client } from "@/lib/api";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
/** Render a config value compactly: primitives inline, objects/arrays as JSON. */
function formatValue(value: unknown): string {
if (value === null || value === undefined) return "—";
if (typeof value === "string") return value === "" ? '""' : value;
if (typeof value === "boolean" || typeof value === "number") return String(value);
return JSON.stringify(value);
}
/** Group key, derived from the first underscore-delimited token (e.g. "retain_chunk_size" -> "retain"). */
function groupOf(key: string): string {
const head = key.split("_")[0];
return head || "general";
}
export function AdminConfigView() {
const t = useTranslations("admin");
const [config, setConfig] = useState<Record<string, unknown> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [filter, setFilter] = useState("");
useEffect(() => {
let cancelled = false;
client
.getAdminConfig()
.then((resp) => {
if (!cancelled) {
setConfig(resp.config);
setError(false);
}
})
.catch(() => {
if (!cancelled) setError(true);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
const groups = useMemo(() => {
if (!config) return [];
const needle = filter.trim().toLowerCase();
const byGroup = new Map<string, [string, unknown][]>();
for (const [key, value] of Object.entries(config).sort(([a], [b]) => a.localeCompare(b))) {
if (
needle &&
!key.toLowerCase().includes(needle) &&
!formatValue(value).toLowerCase().includes(needle)
) {
continue;
}
const group = groupOf(key);
const rows = byGroup.get(group) ?? [];
rows.push([key, value]);
byGroup.set(group, rows);
}
return [...byGroup.entries()].sort(([a], [b]) => a.localeCompare(b));
}, [config, filter]);
if (loading) {
return (
<div className="flex items-center justify-center gap-2 py-16 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
<span>{t("loading")}</span>
</div>
);
}
if (error) {
return <p className="py-16 text-center text-sm text-destructive">{t("loadError")}</p>;
}
return (
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-foreground">{t("configHeading")}</h2>
<p className="text-sm text-muted-foreground">{t("configDescription")}</p>
</div>
<div className="relative max-w-sm">
<Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder={t("searchPlaceholder")}
className="pl-8"
/>
</div>
<p className="text-xs text-muted-foreground">{t("redactedHint")}</p>
{groups.length === 0 ? (
<p className="py-12 text-center text-sm text-muted-foreground">{t("empty")}</p>
) : (
<div className="grid gap-4">
{groups.map(([group, rows]) => (
<Card key={group}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold capitalize text-foreground">
{group}
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<dl className="divide-y divide-border">
{rows.map(([key, value]) => (
<div key={key} className="grid grid-cols-1 gap-1 py-2 sm:grid-cols-3 sm:gap-4">
<dt className="font-mono text-xs text-muted-foreground break-all">{key}</dt>
<dd className="font-mono text-xs text-foreground break-all sm:col-span-2">
{formatValue(value)}
</dd>
</div>
))}
</dl>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,99 @@
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { ArrowLeft, Moon, ShieldCheck, Sun, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { LanguageSwitcher } from "@/components/language-switcher";
import { useTheme } from "@/lib/theme-context";
import { useFeatures } from "@/lib/features-context";
import { withBasePath } from "@/lib/base-path";
/**
* Header for the global admin surface.
*
* Deliberately NOT the bank-scoped BankSelector: the admin area is not tied to a
* memory bank, so it shows a back-to-app link, the admin title, and global controls
* (theme, language, logout) instead of the bank dropdown / add-document actions.
*/
export function AdminHeader() {
const tNav = useTranslations("nav");
const t = useTranslations("admin");
const { theme, toggleTheme } = useTheme();
const { features } = useFeatures();
return (
<header className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary-gradient">
<div className="flex items-center gap-4 text-sm">
{/* Logo → back to app */}
<Link href="/dashboard" title={tNav("home")}>
<Image
src={withBasePath("/logo.png")}
alt="Hindsight"
width={40}
height={40}
className="h-10 w-auto"
unoptimized
/>
</Link>
<div className="h-8 w-px bg-border" />
{/* Admin title */}
<div className="flex items-center gap-2 font-bold">
<ShieldCheck className="h-5 w-5 text-primary" />
<span>{t("title")}</span>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Back to app */}
<Link
href="/dashboard"
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
<span className="text-sm font-medium">{t("backToApp")}</span>
</Link>
<div className="h-8 w-px bg-border" />
{/* Dark Mode Toggle */}
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="h-9 w-9"
title={theme === "light" ? tNav("darkMode") : tNav("lightMode")}
>
{theme === "light" ? <Moon className="h-5 w-5" /> : <Sun className="h-5 w-5" />}
</Button>
<LanguageSwitcher />
{features?.access_key_auth && (
<>
<div className="h-8 w-px bg-border" />
<Button
variant="ghost"
size="icon"
className="h-9 w-9"
title="Logout"
onClick={async () => {
try {
await fetch(withBasePath("/api/auth/logout"), { method: "POST" });
} finally {
window.location.href = withBasePath("/login");
}
}}
>
<LogOut className="h-5 w-5" />
</Button>
</>
)}
</div>
</header>
);
}
@@ -2,47 +2,42 @@
import { useState } from "react";
import { useTranslations } from "next-intl";
import { useBank } from "@/lib/bank-context";
import { bankRoute } from "@/lib/bank-url";
import {
Search,
Sparkles,
Database,
FileText,
Users,
ChevronLeft,
ChevronRight,
Settings,
} from "lucide-react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import Link from "next/link";
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
interface SidebarProps {
currentTab: NavItem;
onTabChange: (tab: NavItem) => void;
export interface SidebarItem {
/** Stable id compared against `currentTab` to mark the active entry. */
id: string;
label: string;
icon: LucideIcon;
/** Destination href (Next.js applies basePath automatically). */
href: string;
}
export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
interface SidebarProps {
/** Navigation entries to render, top to bottom. */
items: SidebarItem[];
/** Id of the active entry. */
currentTab: string;
/**
* Optional in-app navigation handler. When provided, a plain left-click is
* intercepted and routed via the callback (middle-click / Cmd+click still open
* the href in a new tab). When omitted, the entry behaves as a normal link.
*/
onTabChange?: (id: string) => void;
}
/**
* Collapsible left navigation, shared across the app (bank workspace, admin, …).
* It is purely presentational: callers provide the items, the active id, and an
* optional click handler.
*/
export function Sidebar({ items, currentTab, onTabChange }: SidebarProps) {
const t = useTranslations("bank.sidebar");
const tBank = useTranslations("bank");
const { currentBank } = useBank();
const [isCollapsed, setIsCollapsed] = useState(true);
if (!currentBank) {
return null;
}
const navItems = [
{ id: "data" as NavItem, label: t("memories"), icon: Database },
{ id: "recall" as NavItem, label: t("recall"), icon: Search },
{ id: "reflect" as NavItem, label: t("reflect"), icon: Sparkles },
{ id: "documents" as NavItem, label: t("documents"), icon: FileText },
{ id: "entities" as NavItem, label: t("entities"), icon: Users },
{ id: "profile" as NavItem, label: tBank("bankConfiguration"), icon: Settings },
];
return (
<aside
className={cn(
@@ -52,23 +47,22 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
>
<nav className="flex-1 p-3 pt-4">
<ul className="space-y-1">
{navItems.map((item) => {
{items.map((item) => {
const Icon = item.icon;
const isActive = currentTab === item.id;
const href = bankRoute(currentBank, `?view=${item.id}`);
return (
<li key={item.id}>
<Link
href={href}
href={item.href}
onClick={(e) => {
// For left-click, prevent default and use the callback
// This allows the parent to handle navigation without full page reload
if (e.button === 0 && !e.ctrlKey && !e.metaKey) {
// For left-click, prevent default and use the callback so the
// parent can navigate without a full page reload. Middle-click
// or Ctrl/Cmd+click open in a new tab naturally.
if (onTabChange && e.button === 0 && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
onTabChange(item.id);
}
// Middle-click or Ctrl/Cmd+click will naturally open in new tab
}}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all",
+10
View File
@@ -1302,6 +1302,7 @@ export class ControlPlaneClient {
mcp: boolean;
worker: boolean;
bank_config_api: boolean;
admin_api: boolean;
file_upload_api: boolean;
document_export_api: boolean;
document_import_api: boolean;
@@ -1311,6 +1312,15 @@ export class ControlPlaneClient {
}>("/api/version");
}
/**
* Fetch the resolved server-level configuration for the admin surface.
* Credentials are redacted by the dataplane. Requires the admin API to be enabled
* (and, if configured, the admin token to be present on the server-side proxy).
*/
async getAdminConfig() {
return this.fetchApi<{ config: Record<string, unknown> }>("/api/admin/config");
}
/**
* Export documents from a bank as a transfer ZIP archive (no LLM re-extraction).
* Pass documentIds to export specific documents, or omit to export the whole bank.
@@ -8,6 +8,7 @@ interface Features {
mcp: boolean;
worker: boolean;
bank_config_api: boolean;
admin_api: boolean;
access_key_auth: boolean;
document_export_api: boolean;
document_import_api: boolean;
@@ -26,6 +27,7 @@ const defaultFeatures: Features = {
mcp: false,
worker: false,
bank_config_api: false,
admin_api: false,
access_key_auth: false,
document_export_api: false,
document_import_api: false,
@@ -13,6 +13,7 @@ import {
export const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
const DATAPLANE_API_KEY = process.env.HINDSIGHT_CP_DATAPLANE_API_KEY || "";
const ADMIN_TOKEN = process.env.HINDSIGHT_CP_ADMIN_TOKEN || "";
/**
* Auth headers for direct fetch calls to the dataplane API.
@@ -25,6 +26,21 @@ export function getDataplaneHeaders(extra?: Record<string, string>): Record<stri
return headers;
}
/**
* Auth headers for the dataplane admin endpoints (`/admin/*`).
*
* The admin surface uses its own independent token (HINDSIGHT_CP_ADMIN_TOKEN ->
* HINDSIGHT_API_ADMIN_TOKEN), distinct from the tenant API key. When unset, no auth
* header is sent and the dataplane decides whether the admin API is open.
*/
export function getAdminHeaders(extra?: Record<string, string>): Record<string, string> {
const headers: Record<string, string> = { ...extra };
if (ADMIN_TOKEN) {
headers["Authorization"] = `Bearer ${ADMIN_TOKEN}`;
}
return headers;
}
/**
* Build a dataplane URL for a bank-scoped endpoint with the bank id properly encoded.
* Bank ids may contain `:`, `/`, `%`, etc. (e.g. openclaw `agent::channel::user`),
@@ -1451,6 +1451,19 @@
"selectBank": "Wählen Sie eine Speicherbank aus dem Dropdown oben, um loszulegen.",
"sidebarHint": "Die Seitenleiste erscheint, sobald Sie eine Speicherbank ausgewählt haben."
},
"admin": {
"title": "Administration",
"backToApp": "Zurück zur App",
"configHeading": "Konfiguration",
"configDescription": "Aufgelöste Konfiguration auf Serverebene für diese Bereitstellung. Anmeldedaten sind ausgeblendet.",
"searchPlaceholder": "Einstellungen filtern...",
"loading": "Konfiguration wird geladen...",
"loadError": "Konfiguration konnte nicht geladen werden.",
"empty": "Keine Einstellungen entsprechen dem Filter.",
"redactedHint": "Anmeldedaten-Werte sind maskiert; „***“ bedeutet, dass ein Wert gesetzt ist.",
"notEnabledTitle": "Administrations-API nicht aktiviert",
"notEnabledDescription": "Aktivieren Sie den Administrationsbereich, indem Sie {envVar} auf dem API-Server setzen."
},
"login": {
"title": "Hindsight Control Plane",
"description": "Geben Sie Ihren Zugangsschlüssel ein, um fortzufahren",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "Version konnte nicht abgerufen werden"
},
"admin": {
"config": {
"fetch": "Administrationskonfiguration konnte nicht abgerufen werden"
}
},
"webhooks": {
"list": "Webhooks konnten nicht aufgelistet werden",
"create": "Webhook konnte nicht erstellt werden",
@@ -1451,6 +1451,19 @@
"selectBank": "Select a memory bank from the dropdown above to get started.",
"sidebarHint": "The sidebar will appear once you select a memory bank."
},
"admin": {
"title": "Admin",
"backToApp": "Back to app",
"configHeading": "Configuration",
"configDescription": "Resolved server-level configuration for this deployment. Credentials are redacted.",
"searchPlaceholder": "Filter settings...",
"loading": "Loading configuration...",
"loadError": "Failed to load configuration.",
"empty": "No settings match your filter.",
"redactedHint": "Credential values are masked; \"***\" means a value is set.",
"notEnabledTitle": "Admin API Not Enabled",
"notEnabledDescription": "Enable the admin surface by setting {envVar} on the API server."
},
"login": {
"title": "Hindsight Control Plane",
"description": "Enter your access key to continue",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "Failed to get version"
},
"admin": {
"config": {
"fetch": "Failed to get admin configuration"
}
},
"webhooks": {
"list": "Failed to list webhooks",
"create": "Failed to create webhook",
@@ -1451,6 +1451,19 @@
"selectBank": "Selecciona un banco de memoria del menú superior para comenzar.",
"sidebarHint": "La barra lateral aparecerá una vez que selecciones un banco de memoria."
},
"admin": {
"title": "Administración",
"backToApp": "Volver a la app",
"configHeading": "Configuración",
"configDescription": "Configuración a nivel de servidor resuelta para esta implementación. Las credenciales están ocultas.",
"searchPlaceholder": "Filtrar ajustes...",
"loading": "Cargando configuración...",
"loadError": "No se pudo cargar la configuración.",
"empty": "Ningún ajuste coincide con el filtro.",
"redactedHint": "Los valores de las credenciales están ocultos; \"***\" indica que hay un valor configurado.",
"notEnabledTitle": "API de administración no habilitada",
"notEnabledDescription": "Habilita la sección de administración configurando {envVar} en el servidor de la API."
},
"login": {
"title": "Plano de Control de Hindsight",
"description": "Introduce tu clave de acceso para continuar",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "No se pudo obtener la versión"
},
"admin": {
"config": {
"fetch": "No se pudo obtener la configuración de administración"
}
},
"webhooks": {
"list": "No se pudieron listar los webhooks",
"create": "No se pudo crear el webhook",
@@ -1451,6 +1451,19 @@
"selectBank": "Sélectionnez une banque de mémoire dans le menu ci-dessus pour commencer.",
"sidebarHint": "La barre latérale apparaîtra une fois que vous aurez sélectionné une banque de mémoire."
},
"admin": {
"title": "Administration",
"backToApp": "Retour à l'application",
"configHeading": "Configuration",
"configDescription": "Configuration au niveau du serveur résolue pour ce déploiement. Les identifiants sont masqués.",
"searchPlaceholder": "Filtrer les paramètres...",
"loading": "Chargement de la configuration...",
"loadError": "Échec du chargement de la configuration.",
"empty": "Aucun paramètre ne correspond au filtre.",
"redactedHint": "Les valeurs des identifiants sont masquées ; « *** » signifie qu'une valeur est définie.",
"notEnabledTitle": "API d'administration non activée",
"notEnabledDescription": "Activez la section d'administration en définissant {envVar} sur le serveur API."
},
"login": {
"title": "Plan de contrôle Hindsight",
"description": "Entrez votre clé d'accès pour continuer",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "Impossible de récupérer la version"
},
"admin": {
"config": {
"fetch": "Échec de la récupération de la configuration d'administration"
}
},
"webhooks": {
"list": "Impossible de lister les webhooks",
"create": "Impossible de créer le webhook",
@@ -1451,6 +1451,19 @@
"selectBank": "上のドロップダウンからメモリバンクを選択して始めましょう。",
"sidebarHint": "メモリバンクを選択するとサイドバーが表示されます。"
},
"admin": {
"title": "管理",
"backToApp": "アプリに戻る",
"configHeading": "構成",
"configDescription": "このデプロイメントで解決されたサーバーレベルの構成です。認証情報は秘匿されています。",
"searchPlaceholder": "設定をフィルター...",
"loading": "構成を読み込み中...",
"loadError": "構成の読み込みに失敗しました。",
"empty": "フィルターに一致する設定はありません。",
"redactedHint": "認証情報の値はマスクされています。「***」は値が設定されていることを示します。",
"notEnabledTitle": "管理 API が有効になっていません",
"notEnabledDescription": "API サーバーで {envVar} を設定して管理セクションを有効にしてください。"
},
"login": {
"title": "Hindsight コントロールプレーン",
"description": "続行するにはアクセスキーを入力してください",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "バージョンの取得に失敗しました"
},
"admin": {
"config": {
"fetch": "管理構成の取得に失敗しました"
}
},
"webhooks": {
"list": "Webhookの一覧取得に失敗しました",
"create": "Webhookの作成に失敗しました",
@@ -1451,6 +1451,19 @@
"selectBank": "위의 드롭다운에서 메모리 뱅크를 선택하여 시작하세요.",
"sidebarHint": "메모리 뱅크를 선택하면 사이드바가 나타납니다."
},
"admin": {
"title": "관리",
"backToApp": "앱으로 돌아가기",
"configHeading": "구성",
"configDescription": "이 배포에 대해 확인된 서버 수준 구성입니다. 자격 증명은 가려져 있습니다.",
"searchPlaceholder": "설정 필터링...",
"loading": "구성을 불러오는 중...",
"loadError": "구성을 불러오지 못했습니다.",
"empty": "필터와 일치하는 설정이 없습니다.",
"redactedHint": "자격 증명 값은 가려져 있습니다. \"***\"는 값이 설정되어 있음을 의미합니다.",
"notEnabledTitle": "관리 API가 활성화되지 않음",
"notEnabledDescription": "API 서버에서 {envVar}을(를) 설정하여 관리 섹션을 활성화하세요."
},
"login": {
"title": "Hindsight 제어 플레인",
"description": "계속하려면 액세스 키를 입력하세요",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "버전을 가져오지 못했습니다"
},
"admin": {
"config": {
"fetch": "관리 구성을 가져오지 못했습니다"
}
},
"webhooks": {
"list": "Webhook 목록을 가져오지 못했습니다",
"create": "Webhook을 생성하지 못했습니다",
@@ -1451,6 +1451,19 @@
"selectBank": "Selecione um banco de memória no menu acima para começar.",
"sidebarHint": "A barra lateral aparecerá quando você selecionar um banco de memória."
},
"admin": {
"title": "Administração",
"backToApp": "Voltar ao app",
"configHeading": "Configuração",
"configDescription": "Configuração no nível do servidor resolvida para esta implantação. As credenciais estão ocultas.",
"searchPlaceholder": "Filtrar configurações...",
"loading": "Carregando configuração...",
"loadError": "Falha ao carregar a configuração.",
"empty": "Nenhuma configuração corresponde ao filtro.",
"redactedHint": "Os valores das credenciais estão mascarados; \"***\" significa que há um valor definido.",
"notEnabledTitle": "API de administração não habilitada",
"notEnabledDescription": "Habilite a seção de administração definindo {envVar} no servidor da API."
},
"login": {
"title": "Plano de Controle Hindsight",
"description": "Insira sua chave de acesso para continuar",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "Falha ao buscar versão"
},
"admin": {
"config": {
"fetch": "Falha ao obter a configuração de administração"
}
},
"webhooks": {
"list": "Falha ao listar webhooks",
"create": "Falha ao criar webhook",
@@ -1451,6 +1451,19 @@
"selectBank": "請先在上方下拉選單選擇一個記憶庫。",
"sidebarHint": "選擇記憶庫後,側邊欄會出現。"
},
"admin": {
"title": "管理",
"backToApp": "返回應用程式",
"configHeading": "設定",
"configDescription": "為呢個部署解析嘅伺服器層級設定。憑證已經隱去。",
"searchPlaceholder": "篩選設定...",
"loading": "正在載入設定...",
"loadError": "載入設定失敗。",
"empty": "冇符合篩選條件嘅設定。",
"redactedHint": "憑證值已經遮蔽;「***」表示已經設定咗值。",
"notEnabledTitle": "管理 API 未啟用",
"notEnabledDescription": "喺 API 伺服器設定 {envVar} 嚟啟用管理區段。"
},
"login": {
"title": "Hindsight 控制面板",
"description": "輸入存取金鑰以繼續",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "未能載入版本"
},
"admin": {
"config": {
"fetch": "取得管理設定失敗"
}
},
"webhooks": {
"list": "未能列出 Webhook",
"create": "未能建立 Webhook",
@@ -1451,6 +1451,19 @@
"selectBank": "请从上方下拉菜单选择一个记忆库开始使用。",
"sidebarHint": "选择记忆库后,侧边栏会出现。"
},
"admin": {
"title": "管理",
"backToApp": "返回应用",
"configHeading": "配置",
"configDescription": "为此部署解析的服务器级配置。凭据已被隐去。",
"searchPlaceholder": "筛选设置...",
"loading": "正在加载配置...",
"loadError": "加载配置失败。",
"empty": "没有与筛选条件匹配的设置。",
"redactedHint": "凭据值已被屏蔽;“***”表示已设置值。",
"notEnabledTitle": "管理 API 未启用",
"notEnabledDescription": "在 API 服务器上设置 {envVar} 以启用管理部分。"
},
"login": {
"title": "Hindsight 控制面板",
"description": "输入访问密钥以继续",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "获取版本失败"
},
"admin": {
"config": {
"fetch": "获取管理配置失败"
}
},
"webhooks": {
"list": "列出 Webhook 失败",
"create": "创建 Webhook 失败",
@@ -1451,6 +1451,19 @@
"selectBank": "請從上方下拉選單選擇一個記憶庫開始使用。",
"sidebarHint": "選擇記憶庫後,側邊欄會出現。"
},
"admin": {
"title": "管理",
"backToApp": "返回應用程式",
"configHeading": "設定",
"configDescription": "為此部署解析的伺服器層級設定。憑證已被隱去。",
"searchPlaceholder": "篩選設定...",
"loading": "正在載入設定...",
"loadError": "載入設定失敗。",
"empty": "沒有符合篩選條件的設定。",
"redactedHint": "憑證值已被遮蔽;「***」表示已設定值。",
"notEnabledTitle": "管理 API 未啟用",
"notEnabledDescription": "在 API 伺服器上設定 {envVar} 以啟用管理區段。"
},
"login": {
"title": "Hindsight 控制面板",
"description": "輸入存取金鑰以繼續",
@@ -1591,6 +1604,11 @@
"version": {
"fetch": "取得版本失敗"
},
"admin": {
"config": {
"fetch": "取得管理設定失敗"
}
},
"webhooks": {
"list": "列出 Webhook 失敗",
"create": "建立 Webhook 失敗",
@@ -1747,6 +1747,8 @@ Configuration fields are categorized for security:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_ENABLE_BANK_CONFIG_API` | Enable per-bank config API | `true` |
| `HINDSIGHT_API_ENABLE_ADMIN_API` | Enable the admin API (`GET /admin/config`) and the Control Plane `/admin` page. Off by default — the surface returns `404` until enabled. Server-level (static). | `false` |
| `HINDSIGHT_API_ADMIN_TOKEN` | Optional bearer token required by the admin API. When unset, the admin API is open (once enabled); when set, callers must send `Authorization: Bearer <token>`. Independent of the tenant API key. Server-level (static). | _(unset)_ |
| `HINDSIGHT_API_DEFAULT_BANK_TEMPLATE` | Bank template manifest (JSON) applied automatically to every newly-created bank. See below. | _(unset)_ |
##### `HINDSIGHT_API_DEFAULT_BANK_TEMPLATE`
+72
View File
@@ -55,6 +55,56 @@
}
}
},
"/admin/config": {
"get": {
"tags": [
"Admin"
],
"summary": "Get resolved server-level configuration",
"description": "Returns the resolved server-level configuration with credentials redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.",
"operationId": "get_admin_config",
"parameters": [
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminConfigResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/metrics": {
"get": {
"tags": [
@@ -5554,6 +5604,22 @@
"update_disposition": true
}
},
"AdminConfigResponse": {
"properties": {
"config": {
"additionalProperties": true,
"type": "object",
"title": "Config",
"description": "Resolved server-level configuration (Python field names); credentials are redacted"
}
},
"type": "object",
"required": [
"config"
],
"title": "AdminConfigResponse",
"description": "Response model for the server-level (admin) configuration view.\n\nReturns the resolved ``HindsightConfig`` as a flat dict keyed by Python field\nname. Credential fields (API keys, tokens, service-account keys, base URLs) are\nmasked: present as ``\"***\"`` when set and ``None`` when unset, so an operator can\nsee which credentials are configured without ever seeing their values."
},
"AsyncOperationSubmitResponse": {
"properties": {
"operation_id": {
@@ -8215,6 +8281,11 @@
"title": "Bank Config Api",
"description": "Whether per-bank configuration API is enabled"
},
"admin_api": {
"type": "boolean",
"title": "Admin Api",
"description": "Whether the admin API (/admin) is enabled"
},
"file_upload_api": {
"type": "boolean",
"title": "File Upload Api",
@@ -8247,6 +8318,7 @@
"mcp",
"worker",
"bank_config_api",
"admin_api",
"file_upload_api",
"document_export_api",
"document_import_api",
@@ -1747,6 +1747,8 @@ Configuration fields are categorized for security:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_ENABLE_BANK_CONFIG_API` | Enable per-bank config API | `true` |
| `HINDSIGHT_API_ENABLE_ADMIN_API` | Enable the admin API (`GET /admin/config`) and the Control Plane `/admin` page. Off by default — the surface returns `404` until enabled. Server-level (static). | `false` |
| `HINDSIGHT_API_ADMIN_TOKEN` | Optional bearer token required by the admin API. When unset, the admin API is open (once enabled); when set, callers must send `Authorization: Bearer <token>`. Independent of the tenant API key. Server-level (static). | _(unset)_ |
| `HINDSIGHT_API_DEFAULT_BANK_TEMPLATE` | Bank template manifest (JSON) applied automatically to every newly-created bank. See below. | _(unset)_ |
##### `HINDSIGHT_API_DEFAULT_BANK_TEMPLATE`
@@ -55,6 +55,56 @@
}
}
},
"/admin/config": {
"get": {
"tags": [
"Admin"
],
"summary": "Get resolved server-level configuration",
"description": "Returns the resolved server-level configuration with credentials redacted. Gated by HINDSIGHT_API_ENABLE_ADMIN_API and, when set, HINDSIGHT_API_ADMIN_TOKEN.",
"operationId": "get_admin_config",
"parameters": [
{
"name": "authorization",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Authorization"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AdminConfigResponse"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/metrics": {
"get": {
"tags": [
@@ -5554,6 +5604,22 @@
"update_disposition": true
}
},
"AdminConfigResponse": {
"properties": {
"config": {
"additionalProperties": true,
"type": "object",
"title": "Config",
"description": "Resolved server-level configuration (Python field names); credentials are redacted"
}
},
"type": "object",
"required": [
"config"
],
"title": "AdminConfigResponse",
"description": "Response model for the server-level (admin) configuration view.\n\nReturns the resolved ``HindsightConfig`` as a flat dict keyed by Python field\nname. Credential fields (API keys, tokens, service-account keys, base URLs) are\nmasked: present as ``\"***\"`` when set and ``None`` when unset, so an operator can\nsee which credentials are configured without ever seeing their values."
},
"AsyncOperationSubmitResponse": {
"properties": {
"operation_id": {
@@ -8215,6 +8281,11 @@
"title": "Bank Config Api",
"description": "Whether per-bank configuration API is enabled"
},
"admin_api": {
"type": "boolean",
"title": "Admin Api",
"description": "Whether the admin API (/admin) is enabled"
},
"file_upload_api": {
"type": "boolean",
"title": "File Upload Api",
@@ -8247,6 +8318,7 @@
"mcp",
"worker",
"bank_config_api",
"admin_api",
"file_upload_api",
"document_export_api",
"document_import_api",