Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abee512866 | ||
|
|
af0dee05ec | ||
|
|
83d4892acb | ||
|
|
011bc918a8 | ||
|
|
c38c575d8e | ||
|
|
b1c1e49cc5 | ||
|
|
b9e2358901 | ||
|
|
a1874c90ae |
@@ -185,15 +185,15 @@ class H(BaseHTTPRequestHandler):
|
||||
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
|
||||
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
|
||||
if path in ('/api','/api/status'): return self.sendj(api_spec())
|
||||
if path in ('/api/base','/base'):
|
||||
if path in ('/api/base','/api/baseline','/base'):
|
||||
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
|
||||
if path in ('/api/ver','/ver'):
|
||||
if path in ('/api/ver','/api/versions','/ver'):
|
||||
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
|
||||
if path in ('/api/patch','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
|
||||
if path in ('/api/run','/run'):
|
||||
if path in ('/api/run','/api/runtime-status','/run'):
|
||||
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
|
||||
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
|
||||
if path in ('/api/action-monitor','/action-monitor'):
|
||||
@@ -211,11 +211,11 @@ class H(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
|
||||
try:
|
||||
if path == '/api/branch': return self.branch(data)
|
||||
if path == '/api/file': return self.file(data)
|
||||
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
|
||||
if path in ('/api/file','/api/commit-file'): return self.file(data)
|
||||
if path == '/api/commit': return self.commit(data)
|
||||
if path == '/api/patch': return self.patch(data)
|
||||
if path == '/api/tag': return self.tag(data)
|
||||
if path in ('/api/patch','/api/create-patch'): return self.patch(data)
|
||||
if path in ('/api/tag','/api/create-tag'): return self.tag(data)
|
||||
if path == '/api/rollback': return self.rollback(data)
|
||||
if path == '/api/model': return self.model(data)
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""V1R3C0 User Portal RBAC Ops core services.
|
||||
|
||||
Pure application layer: no secrets, no external credentials.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
ROLES = {"user", "group_admin", "owner"}
|
||||
|
||||
@dataclass
|
||||
class PortalContext:
|
||||
user_id: str
|
||||
group_id: str | None
|
||||
role: str
|
||||
|
||||
def scope(self):
|
||||
if self.role == "owner":
|
||||
return "global"
|
||||
if self.role == "group_admin":
|
||||
return "group"
|
||||
return "self"
|
||||
|
||||
@dataclass
|
||||
class Approval:
|
||||
request_id: str
|
||||
request_type: str
|
||||
state: str = "pending"
|
||||
|
||||
@dataclass
|
||||
class Alert:
|
||||
alert_id: str
|
||||
level: str
|
||||
state: str = "open"
|
||||
|
||||
@dataclass
|
||||
class AuditEvent:
|
||||
action: str
|
||||
actor: str
|
||||
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
|
||||
class PortalRBACService:
|
||||
def authorize(self, ctx: PortalContext, target_scope: str):
|
||||
if ctx.role not in ROLES:
|
||||
return False
|
||||
if ctx.scope() == "global":
|
||||
return True
|
||||
return ctx.scope() == target_scope
|
||||
|
||||
def navigation(self, role: str):
|
||||
common = ["/", "/me", "/model", "/usage", "/quota", "/tasks", "/alerts", "/approvals", "/audit"]
|
||||
if role == "owner":
|
||||
return common + ["/owner/system", "/owner/gitops", "/owner/runtime", "/owner/security"]
|
||||
if role == "group_admin":
|
||||
return common + ["/group", "/group/users", "/group/quota", "/group/audit"]
|
||||
return common
|
||||
|
||||
class PortalAuditService:
|
||||
def append(self, action: str, actor: str):
|
||||
return AuditEvent(action=action, actor=actor)
|
||||
|
||||
class SecretPolicy:
|
||||
@staticmethod
|
||||
def redact(value: str):
|
||||
return "***REDACTED***" if value else value
|
||||
|
||||
__all__ = ["PortalContext", "PortalRBACService", "PortalAuditService", "SecretPolicy", "Approval", "Alert"]
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS portal_approvals (
|
||||
id TEXT PRIMARY KEY,
|
||||
request_type TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_alerts (
|
||||
id TEXT PRIMARY KEY,
|
||||
level TEXT NOT NULL,
|
||||
state TEXT NOT NULL DEFAULT 'open',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_audit_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
actor TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS portal_notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
+10
-20
@@ -1,22 +1,12 @@
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"architecture_version": "V1",
|
||||
"release_version": "R1",
|
||||
"change_version": "C0",
|
||||
"components": [
|
||||
"webops",
|
||||
"owner-admin-user",
|
||||
"npm",
|
||||
"casdoor",
|
||||
"openclaw",
|
||||
"hermes",
|
||||
"one-api",
|
||||
"gitops-control",
|
||||
"baseline-registry",
|
||||
"custom-llm-registry",
|
||||
"runtime-selector"
|
||||
],
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"proxy": "npm",
|
||||
"runtime_sync": "openclaw"
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"previous_version": "V1R2C2",
|
||||
"rollback_version": "V1R2C2",
|
||||
"mode": "gitops-online",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_deprecated": true,
|
||||
"chatgpt_access": true,
|
||||
"codex_access": true,
|
||||
"updated_at": "2026-07-08T00:00:00Z"
|
||||
}
|
||||
|
||||
+9
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"active_version": "V1R2C2",
|
||||
"active_tag": "V1R2C2",
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"mode": "gitops-online",
|
||||
"version_policy": "VxRxCx",
|
||||
"update_mode": "git_patch_only",
|
||||
@@ -25,10 +25,11 @@
|
||||
"chatgpt_rw": true,
|
||||
"codex_rw": true,
|
||||
"portal_gitops_dashboard": true,
|
||||
"human_shortlinks_login_required": true
|
||||
"human_shortlinks_login_required": true,
|
||||
"autonomous_recovery_loop": true
|
||||
},
|
||||
"version": "V1R2C2",
|
||||
"baseline_tag": "V1R2C2-baseline",
|
||||
"version": "V1R2C3",
|
||||
"baseline_tag": "V1R2C3",
|
||||
"source": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"source_package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"zip_update_mode": "deprecated",
|
||||
@@ -46,7 +47,7 @@
|
||||
"/api",
|
||||
"/hook"
|
||||
],
|
||||
"updated_at": "2026-07-07T15:40:00Z",
|
||||
"updated_at": "2026-07-08T00:00:00Z",
|
||||
"auth": {
|
||||
"primary_domain": "https://auth.cnnetmkt.top",
|
||||
"backup_domain": "https://auth.h.imruyi.top:8443",
|
||||
@@ -55,7 +56,7 @@
|
||||
"casdoor_application": "admin/aios-portal",
|
||||
"backup_domain_role": "standby-only"
|
||||
},
|
||||
"previous_version": "V1R2C1",
|
||||
"previous_version": "V1R2C2",
|
||||
"codex_gitops": {
|
||||
"config": "config/codex/gitops-online.json",
|
||||
"token_account": "codex-gitops-bot",
|
||||
@@ -95,14 +96,5 @@
|
||||
"owner_password_sync": "aios-gitea-owner-sync.timer",
|
||||
"owner_password_source": "secret_ref://aios/user-control-center/UCC_OWNER_PASSWORD"
|
||||
}
|
||||
},
|
||||
"last_auth_portal_fix": {
|
||||
"version": "V1R1C4",
|
||||
"timestamp": "2026-07-06T11:16:23Z",
|
||||
"shortlinks": "redirect_to_login_protected_portal_gitops_section",
|
||||
"gitea_owner_sync": "enabled",
|
||||
"portal_overlay": "overlays/user-control-center",
|
||||
"portal_baseline_cache": "/app/runtime/gitops/current.json",
|
||||
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "patch/v1r3c0-user-portal-rbac-ops-20260708",
|
||||
"base": "main",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-07T18:32:29Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# V1R2C2 自动恢复链路验收测试报告
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **版本**: V1R2C2
|
||||
- **测试时间**: 2026-07-07
|
||||
- **测试范围**: Action Monitor → Error Classifier → Failure Router → Recovery Controller
|
||||
- **测试方式**:
|
||||
1. 单元测试(模块级):验证各模块独立功能
|
||||
2. 集成测试(管线级):验证完整闭环
|
||||
3. API 端到端测试(HTTP 级):验证 REST API 集成
|
||||
|
||||
## 测试结果
|
||||
|
||||
### 1. Action Monitor 测试(✅ PASS)
|
||||
|
||||
| 测试项 | 结果 |
|
||||
|-------|------|
|
||||
| 事件记录 | ✅ 成功记录失败事件 |
|
||||
| 事件检索(list_events) | ✅ 正确返回所有事件 |
|
||||
| 最新事件(latest) | ✅ 正确返回最后一条事件 |
|
||||
| 事件属性完整性 | ✅ event_id, action, error, context, created_at 均正确 |
|
||||
|
||||
### 2. Error Classifier 测试(✅ PASS)
|
||||
|
||||
| 错误类型 | 示例输入 | 预期分类 | 实际结果 |
|
||||
|---------|---------|---------|---------|
|
||||
| Permission Error | "Permission denied" | permission_error | ✅ permission_error |
|
||||
| Git Error | "git failed: merge conflict" | git_error | ✅ git_error |
|
||||
| Git Error | "push rejected" | git_error | ✅ git_error |
|
||||
| Runtime Error | "Connection refused" | runtime_error | ✅ runtime_error |
|
||||
| Runtime Error | "timeout" | runtime_error | ✅ runtime_error |
|
||||
| Runtime Error | "Internal Server Error" | runtime_error | ✅ runtime_error |
|
||||
| Secret Violation | "secret_rejected: use secret_ref" | secret_violation | ✅ secret_violation |
|
||||
| Auth Error | "unauthorized" | auth_error | ✅ auth_error |
|
||||
| Unknown Error (file) | "File exists" | unknown_error | ✅ unknown_error |
|
||||
| Unknown Error (file) | "No such file or directory" | unknown_error | ✅ unknown_error |
|
||||
|
||||
### 3. Failure Router 测试(✅ PASS)
|
||||
|
||||
| 错误类型 | 主路由 | 备份路由 | 逃生路由 | 结果 |
|
||||
|---------|-------|---------|---------|------|
|
||||
| permission_error | openclaw | hermes | windows_codex_app | ✅ |
|
||||
| network/runtime_error | hermes | openclaw | windows_codex_app | ✅ |
|
||||
| secret_violation | windows_codex_app | openclaw | openclaw | ✅ |
|
||||
|
||||
### 4. Recovery Controller 管线测试(✅ PASS)
|
||||
|
||||
| 动作 | 错误 | 分类 | 路由 | 恢复计划 | 验证 |
|
||||
|-----|------|------|------|---------|------|
|
||||
| branch:create | Permission denied | permission_error | openclaw | ✅ 生成 | ✅ 包含 |
|
||||
| file:write | ClientResponseError | runtime_error | hermes | ✅ 生成 | ✅ 包含 |
|
||||
| model:write | secret_rejected: use secret_ref | secret_violation | windows_codex_app | ✅ 生成 | ✅ 包含 |
|
||||
|
||||
### 5. API 500 场景模拟测试(✅ PASS)
|
||||
|
||||
模拟了生产环境中遇到的 API 500 错误:
|
||||
- file:write → Internal Server Error → 正确记录
|
||||
- branch:create → ClientResponseError → 正确记录
|
||||
- file:write → timeout → 正确记录
|
||||
|
||||
### 6. 编译验证(✅ PASS)
|
||||
|
||||
三个核心模块和测试代码编译通过,无语法错误。
|
||||
|
||||
## 执行覆盖
|
||||
|
||||
```
|
||||
测试代码:runtime/agent-os/orchestrator/integration_test_v1r2c2.py
|
||||
被测模块:runtime/agent-os/orchestrator/action_monitor.py
|
||||
runtime/agent-os/orchestrator/error_classifier.py
|
||||
runtime/agent-os/orchestrator/failure_router.py
|
||||
runtime/agent-os/orchestrator/recovery_controller.py
|
||||
API 集成: active-runtime/app/aios_chatgpt_actions_api.py
|
||||
```
|
||||
|
||||
## 结论
|
||||
|
||||
**V1R2C2 自动恢复链路基础模块测试全部通过** ✅
|
||||
|
||||
- Action Monitor 负责记录和检索失败事件,与 API 集成正常
|
||||
- Error Classifier 能根据错误字符串准确分类(permission/git/runtime/secret/auth/unknown)
|
||||
- Failure Router 能根据错误类型将故障路由到合适处理代理人(openclaw/hermes/windows_codex_app)
|
||||
- Recovery Controller 能编排完整恢复管道:分类→路由→计划→执行→验证→记录
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 将 Error Classifier/Failure Router/Recovery Controller 集成到 aios_chatgpt_actions_api.py 的 do_POST 错误处理路径中
|
||||
2. 添加 GitOps 提交和版本标记
|
||||
3. 微信通知 Owner 报告
|
||||
4. 虾马一体进展汇报
|
||||
@@ -0,0 +1,24 @@
|
||||
from active_runtime.app.portal_rbac_ops import PortalContext, PortalRBACService, PortalAuditService, SecretPolicy
|
||||
|
||||
|
||||
def test_user_scope():
|
||||
ctx = PortalContext('u1', 'g1', 'user')
|
||||
assert PortalRBACService().authorize(ctx, 'self')
|
||||
|
||||
|
||||
def test_owner_scope():
|
||||
ctx = PortalContext('o1', None, 'owner')
|
||||
assert PortalRBACService().authorize(ctx, 'global')
|
||||
|
||||
|
||||
def test_navigation():
|
||||
assert '/owner/system' in PortalRBACService().navigation('owner')
|
||||
|
||||
|
||||
def test_audit_append():
|
||||
event = PortalAuditService().append('login', 'u1')
|
||||
assert event.action == 'login'
|
||||
|
||||
|
||||
def test_secret_redaction():
|
||||
assert SecretPolicy.redact('secret') == '***REDACTED***'
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "V1R2C3",
|
||||
"tag": "V1R2C3",
|
||||
"previous_version": "V1R2C2",
|
||||
"rollback_version": "V1R2C2",
|
||||
"release_reason": "GPT Actions ClientResponseError compatibility recovery",
|
||||
"validated_actions": [
|
||||
"getRuntimeStatus",
|
||||
"getBaseline",
|
||||
"listVersions",
|
||||
"listPatches",
|
||||
"getDiff",
|
||||
"createBranch",
|
||||
"createPatch",
|
||||
"commitFile"
|
||||
],
|
||||
"secret_plaintext_written": false,
|
||||
"created_at": "2026-07-08T00:00:00Z",
|
||||
"author": "openclaw"
|
||||
}
|
||||
Reference in New Issue
Block a user