0.10.1: 安全与业务逻辑加固、新品牌与部署加固

三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
  与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
  撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
  全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
  >4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
  后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
  nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
This commit is contained in:
2026-08-13 10:50:51 +08:00
parent b536672000
commit 9501751792
136 changed files with 8024 additions and 1476 deletions
+356
View File
@@ -447,6 +447,145 @@ paths:
responses:
"200": { description: Daily request, failure, and token aggregates }
"403": { description: usage:read permission required }
/api/v1/admin/traces:
get:
operationId: listLLMTraces
security: [{ bearerAuth: [] }]
parameters:
- { name: from, in: query, schema: { type: string, format: date-time } }
- { name: to, in: query, schema: { type: string, format: date-time } }
- { name: trace_type, in: query, schema: { type: string, enum: [application, digital_employee] } }
- { name: target_code, in: query, schema: { type: string } }
- { name: request_id, in: query, schema: { type: string } }
- { name: status, in: query, schema: { type: string, enum: [running, success, error] } }
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
responses:
"200": { description: Metadata-only LLM Trace summaries }
"403": { description: trace:read permission required }
/api/v1/admin/traces/{trace_id}:
get:
operationId: getLLMTrace
security: [{ bearerAuth: [] }]
parameters: [{ name: trace_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"200":
description: Trace detail with ordered model, retrieval, and tool spans
content:
application/json:
schema: { $ref: "#/components/schemas/LLMTrace" }
"403": { description: trace:read permission required }
"404": { description: Trace not found }
/api/v1/admin/agent-sessions:
get:
operationId: listAgentSessions
security: [{ bearerAuth: [] }]
parameters:
- { name: from, in: query, schema: { type: string, format: date-time } }
- { name: to, in: query, schema: { type: string, format: date-time } }
- { name: trace_type, in: query, schema: { type: string, enum: [application, digital_employee] } }
- { name: target_code, in: query, schema: { type: string, maxLength: 128 } }
- { name: session_id, in: query, schema: { type: string, maxLength: 512 } }
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
responses:
"200":
description: Metadata-only application and digital-employee session aggregates
content:
application/json:
schema:
type: object
required: [code, data]
properties:
code: { type: integer }
data:
type: object
required: [items]
properties:
items: { type: array, items: { $ref: "#/components/schemas/AgentSession" } }
"403": { description: trace:read permission required }
/api/v1/admin/agent-nodes:
get:
operationId: listAgentNodes
security: [{ bearerAuth: [] }]
responses:
"200":
description: Registered agent nodes with derived heartbeat status
content:
application/json:
schema: { type: array, items: { $ref: "#/components/schemas/AgentNode" } }
"403": { description: agent_node:read permission required }
post:
operationId: createAgentNode
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/AgentNodeInput" }
responses:
"200": { description: Node metadata and one-time registration token }
"403": { description: agent_node:manage permission required }
"409": { description: Node code already exists }
/api/v1/admin/agent-nodes/route-preview:
post:
operationId: previewAgentNodeRoute
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/AgentNodeRoutePreviewInput" }
responses:
"200":
description: Read-only online candidate list and stable-hash selection; no remote task is dispatched
content:
application/json:
schema: { $ref: "#/components/schemas/AgentNodeRoutePreview" }
"400": { description: Invalid pool, request key or capability constraints }
"403": { description: agent_node:read permission required }
/api/v1/admin/agent-nodes/{node_id}:
put:
operationId: updateAgentNode
security: [{ bearerAuth: [] }]
parameters: [{ name: node_id, in: path, required: true, schema: { type: string, format: uuid } }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/AgentNodeInput" }
responses:
"200": { description: Updated node metadata }
"403": { description: agent_node:manage permission required }
"404": { description: Node not found }
delete:
operationId: deleteAgentNode
security: [{ bearerAuth: [] }]
parameters: [{ name: node_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"200": { description: Node removed and its token invalidated }
"403": { description: agent_node:manage permission required }
"404": { description: Node not found }
/api/v1/admin/agent-nodes/{node_id}/rotate-token:
post:
operationId: rotateAgentNodeToken
security: [{ bearerAuth: [] }]
parameters: [{ name: node_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"200": { description: New one-time registration token; previous token is immediately invalid }
"403": { description: agent_node:manage permission required }
"404": { description: Node not found }
/api/v1/agent/nodes/{code}/heartbeat:
post:
operationId: heartbeatAgentNode
security: [{ agentTokenAuth: [] }]
parameters: [{ name: code, in: path, required: true, schema: { type: string, pattern: "^[a-z0-9][a-z0-9._-]{0,127}$" } }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/AgentNodeHeartbeat" }
responses:
"200": { description: Heartbeat accepted and derived online status returned }
"401": { description: Invalid or disabled node token }
/api/v1/admin/outbox-events:
get:
operationId: listOutboxEvents
@@ -1164,6 +1303,65 @@ paths:
operationId: listAdministrativeModelRequests
security: [{ bearerAuth: [] }]
responses: { "200": { description: Portal model access requests by status } }
/api/v1/admin/scheduled-tasks:
get:
operationId: listScheduledTasks
security: [{ bearerAuth: [] }]
responses: { "200": { description: Scheduled task definitions with next and last execution state } }
post:
operationId: createScheduledTask
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ScheduledTaskInput" }
responses: { "200": { description: Task created with KEK-encrypted execution credential } }
/api/v1/admin/scheduled-tasks/{task_id}:
get:
operationId: getScheduledTask
security: [{ bearerAuth: [] }]
parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses: { "200": { description: Task detail without plaintext credentials } }
put:
operationId: updateScheduledTask
security: [{ bearerAuth: [] }]
parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ScheduledTaskInput" }
responses: { "200": { description: Task updated and next execution recomputed } }
delete:
operationId: deleteScheduledTask
security: [{ bearerAuth: [] }]
parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses: { "200": { description: Task and execution history deleted } }
/api/v1/admin/scheduled-tasks/{task_id}/start:
post:
operationId: startScheduledTask
security: [{ bearerAuth: [] }]
parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses: { "200": { description: Task enabled and next execution computed } }
/api/v1/admin/scheduled-tasks/{task_id}/pause:
post:
operationId: pauseScheduledTask
security: [{ bearerAuth: [] }]
parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses: { "200": { description: Task paused without deleting history } }
/api/v1/admin/scheduled-tasks/{task_id}/run:
post:
operationId: runScheduledTaskNow
security: [{ bearerAuth: [] }]
parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses: { "200": { description: Manual execution durably queued } }
/api/v1/admin/scheduled-tasks/{task_id}/runs:
get:
operationId: listScheduledTaskRuns
security: [{ bearerAuth: [] }]
parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses: { "200": { description: Execution history with status, response and error } }
/api/v1/admin/model-requests/{request_id}/approve:
post:
operationId: approveModelRequest
@@ -1287,6 +1485,11 @@ components:
type: http
scheme: bearer
description: Gateway API Key with gateway:invoke scope; X-Gateway-API-Key is also accepted.
agentTokenAuth:
type: apiKey
in: header
name: X-Agent-Token
description: One-time-issued node registration token used for heartbeats.
schemas:
Health:
type: object
@@ -1625,3 +1828,156 @@ components:
maxItems: 100
items: { type: string, description: Exact event type or a prefix ending in one asterisk. }
enabled: { type: boolean }
ScheduledTaskInput:
type: object
required: [code, name, description, cron_expression, timezone, target_type, target_code, prompt, variables, skill_ids, mcp_server_ids, conversation_id, enabled]
properties:
code: { type: string, pattern: "^[a-z][a-z0-9_-]{1,63}$" }
name: { type: string, minLength: 1, maxLength: 128 }
description: { type: string, maxLength: 4000 }
cron_expression: { type: string, maxLength: 128, description: Standard five-field cron expression. }
timezone: { type: string, maxLength: 128, example: Asia/Shanghai }
target_type: { type: string, enum: [application, digital_employee] }
target_code: { type: string, maxLength: 64 }
prompt: { type: string, minLength: 1, maxLength: 100000 }
variables: { type: object, additionalProperties: true }
skill_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
mcp_server_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
conversation_id: { type: string, maxLength: 128 }
notification_channel_id: { type: [string, "null"], format: uuid }
api_key:
type: string
writeOnly: true
maxLength: 512
description: Required on create; omit on update to retain the encrypted credential.
enabled: { type: boolean }
LLMTraceSpan:
type: object
required: [id, trace_id, span_type, name, status, started_at, input_tokens, output_tokens, round, error, metadata]
properties:
id: { type: string, format: uuid }
trace_id: { type: string, format: uuid }
parent_id: { type: string, format: uuid }
span_type: { type: string, enum: [model, tool, retrieval] }
name: { type: string }
status: { type: string, enum: [running, success, error] }
started_at: { type: string, format: date-time }
finished_at: { type: string, format: date-time }
latency_ms: { type: integer, minimum: 0 }
provider_code: { type: string }
model: { type: string }
input_tokens: { type: integer, minimum: 0 }
output_tokens: { type: integer, minimum: 0 }
round: { type: integer, minimum: 0 }
error: { type: string }
metadata: { type: object, additionalProperties: true }
LLMTrace:
type: object
required: [id, request_id, trace_type, target_code, conversation_id, status, started_at, retrieval_count, model_call_count, tool_call_count, error, metadata]
properties:
id: { type: string, format: uuid }
request_id: { type: string }
api_key_id: { type: string, format: uuid }
tenant_id: { type: string, format: uuid }
trace_type: { type: string, enum: [application, digital_employee] }
target_id: { type: string, format: uuid }
target_code: { type: string }
conversation_id: { type: string }
status: { type: string, enum: [running, success, error] }
started_at: { type: string, format: date-time }
finished_at: { type: string, format: date-time }
latency_ms: { type: integer, minimum: 0 }
retrieval_count: { type: integer, minimum: 0 }
model_call_count: { type: integer, minimum: 0 }
tool_call_count: { type: integer, minimum: 0 }
error: { type: string }
metadata: { type: object, additionalProperties: true }
spans: { type: array, items: { $ref: "#/components/schemas/LLMTraceSpan" } }
AgentSession:
type: object
required: [id, trace_type, target_code, trace_count, latest_trace_id, latest_status, started_at, updated_at, retrieval_count, model_call_count, tool_call_count]
properties:
id: { type: string, description: Session key; request-derived for stateless calls }
trace_type: { type: string, enum: [application, digital_employee] }
target_code: { type: string }
trace_count: { type: integer, minimum: 1 }
latest_trace_id: { type: string, format: uuid }
latest_status: { type: string, enum: [running, success, error] }
started_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
retrieval_count: { type: integer, minimum: 0 }
model_call_count: { type: integer, minimum: 0 }
tool_call_count: { type: integer, minimum: 0 }
AgentNodeInput:
type: object
required: [name, description, endpoint, node_type, pool_type, pool_code, enabled]
properties:
code: { type: string, pattern: "^[a-z0-9][a-z0-9._-]{0,127}$" }
name: { type: string, minLength: 1, maxLength: 128 }
description: { type: string, maxLength: 4000 }
endpoint: { type: string, maxLength: 512 }
node_type: { type: string, enum: [worker, gateway, executor] }
pool_type: { type: string, enum: [public, private] }
pool_code: { type: string, minLength: 1, maxLength: 64 }
enabled: { type: boolean }
AgentNodeRoutePreviewInput:
type: object
required: [pool_type, pool_code, request_key]
properties:
pool_type: { type: string, enum: [public, private] }
pool_code: { type: string, minLength: 1, maxLength: 64 }
required_capabilities: { type: array, maxItems: 32, items: { type: string, maxLength: 128 } }
request_key: { type: string, minLength: 1, maxLength: 512 }
AgentNodeHeartbeat:
type: object
required: [version, capabilities, metadata, error]
properties:
version: { type: string, maxLength: 128 }
capabilities: { type: object, additionalProperties: true }
metadata: { type: object, additionalProperties: true }
error: { type: string, maxLength: 4000 }
AgentNode:
type: object
required: [id, code, name, description, endpoint, node_type, pool_type, pool_code, enabled, status, token_prefix, version, capabilities, metadata, last_error, created_at, updated_at]
properties:
id: { type: string, format: uuid }
code: { type: string }
name: { type: string }
description: { type: string }
endpoint: { type: string }
node_type: { type: string, enum: [worker, gateway, executor] }
pool_type: { type: string, enum: [public, private] }
pool_code: { type: string }
enabled: { type: boolean }
status: { type: string, enum: [pending, online, offline, disabled] }
token_prefix: { type: string }
version: { type: string }
capabilities: { type: object, additionalProperties: true }
metadata: { type: object, additionalProperties: true }
last_heartbeat_at: { type: string, format: date-time }
last_heartbeat_ip: { type: string }
last_error: { type: string }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
AgentNodeTokenResponse:
type: object
required: [node, token, warning]
properties:
node: { $ref: "#/components/schemas/AgentNode" }
token: { type: string, writeOnly: true }
warning: { type: string }
AgentNodeRoutePreview:
type: object
required: [pool_type, pool_code, required_capabilities, request_key, selection_policy, reason, selected, candidates]
properties:
pool_type: { type: string, enum: [public, private] }
pool_code: { type: string }
required_capabilities: { type: array, items: { type: string } }
request_key: { type: string }
selection_policy: { type: string, example: stable-hash(request_key,node_id) }
reason: { type: string, enum: [selected_online_node, no_online_node, no_capable_node] }
selected:
anyOf:
- { $ref: "#/components/schemas/AgentNode" }
- { type: 'null' }
candidates: { type: array, items: { $ref: "#/components/schemas/AgentNode" } }