Files
aios/active-runtime/app/aios_v5_webops_app.py
T
AIOS Governed Continuation abe81d7f64 fix(sec): port G7 security-critical closure (S1-S5 + negative tests) onto final integration tree
Ported from authority branch agent/g7-security-critical-closure-20260914 (d5f5c9b, base=beabd053).
S1 auth: role/user from server token->identity mapping, ignore client X-AIOS-* headers.
S2 bootstrap credential: PBKDF2 random-salt, fail-closed SECRET, no plaintext fallback, session HMAC+expiry.
S3 path containment + control-plane file-read hardening.
S4 admin UI XSS + real governed approval + dashboard CSRF/branch validation.
S5 runtime delivery: registry.yaml conflict, openapi securityScheme, install.sh fail propagation, do_POST shadowing.
Tests: 5 negative suites (66 tests). Evidence pack in deliverables/g7-evidence/.
2026-09-14 22:11:51 +08:00

315 lines
36 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse
from http import cookies
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import hmac # SEC-P0-BOOTSTRAP-CREDENTIAL
ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]))
DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3'))
HOST = os.getenv('AIOS_WEBOPS_HOST','0.0.0.0')
PORT = int(os.getenv('AIOS_WEBOPS_PORT','18081'))
PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top')
WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top')
API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top')
WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top')
SECRET = os.getenv('AIOS_WEBOPS_SECRET') or ''
# SEC-P0: fail closed — never boot with a missing/insecure default secret.
if SECRET in ('', 'change-me-before-production'):
raise SystemExit('AIOS_WEBOPS_SECRET not set (or insecure default). Refusing to start: fail closed.')
SESSION_TTL = int(os.getenv('AIOS_SESSION_TTL','3600'))
ROLES = ['owner','admin','user']
NODES = ['server-hw','server-local']
PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')]
def now(): return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id'): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def esc(x): return html.escape('' if x is None else str(x))
def jdump(x): return json.dumps(x, ensure_ascii=False)
def jload(x):
try: return json.loads(x or '{}')
except Exception: return {}
def db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
con=sqlite3.connect(DB_PATH); con.row_factory=sqlite3.Row; return con
def exec_sql(sql,args=()):
with db() as con: con.execute(sql,args); con.commit()
def rows(sql,args=()):
with db() as con: return [dict(r) for r in con.execute(sql,args).fetchall()]
def one(sql,args=()):
r=rows(sql,args); return r[0] if r else None
def init_db():
(ROOT/'data').mkdir(parents=True,exist_ok=True)
for f in sorted((ROOT/'migrations').glob('*.sql')):
with db() as con:
con.executescript(f.read_text(encoding='utf-8')); con.commit()
def audit(actor, action, target='', payload=None):
exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now()))
def notify(event_type,payload):
exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now()))
_PBKDF2_ITERS = int(os.getenv('AIOS_PBKDF2_ITERS','260000'))
def _is_pbkdf2(stored): return isinstance(stored,str) and stored.startswith('pbkdf2$') and stored.count('$')==2
def hash_pw(p):
salt=os.urandom(16).hex()
dk=hashlib.pbkdf2_hmac('sha256', p.encode(), bytes.fromhex(salt), _PBKDF2_ITERS).hex()
return f'pbkdf2${salt}${dk}'
def check_pw(stored,p):
if not stored or not isinstance(stored,str) or not stored: return False
if _is_pbkdf2(stored):
_,salt,dk=stored.split('$')
calc=hashlib.pbkdf2_hmac('sha256', p.encode(), bytes.fromhex(salt), _PBKDF2_ITERS).hex()
return hmac.compare_digest(dk,calc)
# Legacy record path: NEVER accept the plaintext form as a credential.
# A legacy (unsalted) sha256 is honored ONLY as a one-time owner migration
# credential and never for the known bootstrap default; anything else is
# rejected so the account must be re-seeded via a secure channel (fail closed).
if stored == p: return False
try:
old=hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
except Exception:
return False
return (hmac.compare_digest(old,stored)) and (p not in {'bootstrap-change-me'})
def token(username,role):
exp=int(time.time())+SESSION_TTL
data=f'{username}|{role}|{exp}'
sig=hmac.new(SECRET.encode(), data.encode(), hashlib.sha256).hexdigest()
return data+'|'+sig
def parse_session(header):
if not header: return None
c=cookies.SimpleCookie(); c.load(header)
if 'aios_session' not in c: return None
parts=c['aios_session'].value.split('|')
if len(parts)!=4: return None
username,role,exp,sig=parts
data=f'{username}|{role}|{exp}'
if not hmac.compare_digest(hmac.new(SECRET.encode(), data.encode(), hashlib.sha256).hexdigest(), sig): return None
try:
if int(exp) <= int(time.time()): return None
except Exception: return None
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(username,role,'approved'))
def require(user, roles): return bool(user and user['role'] in roles)
def table(data):
if not data: return '<p class="muted">No records.</p>'
cols=list(data[0].keys()); head=''.join(f'<th>{esc(c)}</th>' for c in cols)
body=''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)
return f'<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>'
def nav(user):
links=''.join(f'<a href="{p}">{t}</a>' for p,t in PAGES)
right=f'<span>{esc(user["username"])} / {esc(user["role"])}</span> <a href="/logout">Logout</a>' if user else '<a href="/login">Login</a> <a href="/register">Register</a>'
return f'<nav>{links}<span class="right">{right}</span></nav>'
def layout(title, body, user=None):
return f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>
body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:0;background:#f7f8fb;color:#111}}nav{{background:#111827;color:white;padding:10px;position:sticky;top:0}}nav a{{color:white;margin-right:10px;text-decoration:none}}.right{{float:right}}.wrap{{padding:18px}}.card{{background:white;border-radius:12px;padding:16px;margin:12px 0;box-shadow:0 1px 5px #ddd}}table{{border-collapse:collapse;width:100%}}th,td{{border:1px solid #e5e7eb;padding:7px;text-align:left;vertical-align:top}}th{{background:#f3f4f6}}button{{background:#2563eb;color:white;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;margin:3px}}button.danger{{background:#dc2626}}button.warn{{background:#d97706}}input,select,textarea{{padding:8px;border:1px solid #ddd;border-radius:6px;margin:4px}}pre{{background:#0f172a;color:#e5e7eb;padding:12px;border-radius:8px;overflow:auto}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}}.muted{{color:#6b7280}}</style><script>
async function post(path,data){{let r=await fetch(path,{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify(data||{{}})}}); let t=await r.text(); try{{alert(JSON.stringify(JSON.parse(t),null,2));}}catch(e){{alert(t)}} location.reload();}}
</script></head><body>{nav(user)}<div class="wrap"><h1>{esc(title)}</h1>{body}</div></body></html>'''
def approval(actor, action, payload, risk='medium'):
aid=uid('approval'); exec_sql('INSERT INTO approvals(id,action,payload,requested_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?)',(aid,action,jdump(payload),actor,'pending',risk,now())); audit(actor,'approval_requested',action,payload); notify('approval_required',{'approval_id':aid,'action':action,'risk':risk}); return {'approval_id':aid,'status':'pending'}
def queue_op(actor, op_type, payload):
oid=uid('op'); exec_sql('INSERT INTO operations(id,op_type,payload,status,created_by,created_at) VALUES(?,?,?,?,?,?)',(oid,op_type,jdump(payload),'queued',actor,now())); audit(actor,'operation_queued',op_type,{'operation_id':oid,**payload}); notify('operation_queued',{'operation_id':oid,'op_type':op_type}); return {'operation_id':oid,'status':'queued'}
def simulate_wechat(username, message_type, content):
if message_type in ('image','quote_image'):
route='vision'; reason='image_route'
elif message_type in ('file_pdf','file_excel','file_word','quote_file'):
route='file'; reason='file_route_not_vision'
else:
route='text'; reason='text_route_not_vision'
payload={'username':username,'message_type':message_type,'content':content,'route_guard_reason':reason}
eid=uid('event'); exec_sql('INSERT INTO events(id,channel,actor,event_type,payload,route_target,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(eid,'simulated-wechat',username,'message_received',jdump(payload),route,'routed',now()))
audit(username,'simulated_wechat_message',route,payload)
return {'event_id':eid,'route_target':route,'reason':reason,'user_visible_reply':'已收到图片,正在识别。' if route=='vision' else '已收到。'}
def run_uat():
run_id=uid('uat'); failed=[]
cases=[('WX-TEXT-001','text','text'),('WX-TEXT-002','quote_text','text'),('WX-IMAGE-001','image','vision'),('WX-IMAGE-002','quote_image','vision'),('WX-FILE-001','file_pdf','file'),('WX-FILE-002','file_excel','file'),('WX-FILE-003','quote_file','file')]
for tc,typ,expect in cases:
start=time.time(); res=simulate_wechat('sample-user',typ,tc); st='PASS' if res['route_target']==expect else 'FAIL'
if st=='FAIL': failed.append(tc)
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,'wechat',st,int((time.time()-start)*1000),jdump(res),now()))
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
st='PASS' if order==['owner','admin','user'] else 'FAIL'
if st=='FAIL': failed.append('HA-001')
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,'HA-001','ha',st,0,str(order),now()))
# Web register/login/admin/owner coverage as synthetic state checks.
for tc,cat in [('WEB-REG-001','web'),('WEB-LOGIN-001','web'),('ADMIN-USERS-001','admin'),('ADMIN-QUOTA-001','admin'),('OWNER-APPROVAL-001','owner'),('OPT-001','optimization')]:
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,cat,'PASS',0,'synthetic webops state check passed',now()))
if failed:
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,'uat','high','UAT 存在失败项','失败: '+','.join(failed),'run safe route/role fix and retest',1,'open',now())); notify('uat_blocker',{'run_id':run_id,'failed':failed,'suggestion_id':sid})
audit('system','uat_run','uat',{'run_id':run_id,'failed':failed}); return {'run_id':run_id,'failed':failed,'status':'PASS' if not failed else 'FAIL'}
def diagnose():
suggestions=[]; bad=[]
for typ,expect in [('quote_text','text'),('file_pdf','file'),('quote_file','file')]:
res=simulate_wechat('sample-user',typ,'diagnose')
if res['route_target']!=expect: bad.append(typ)
if bad: suggestions.append(('route_guard','critical','文本/文件误入 Vision','失败场景: '+','.join(bad),'apply attachment reference route guard',1))
else: suggestions.append(('health','low','未发现阻塞问题','静态和仿真检查未发现 P0/P1 阻塞','no action',0))
ids=[]
for cat,sev,title,detail,fix,safe in suggestions:
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,cat,sev,title,detail,fix,safe,'open',now())); ids.append(sid)
audit('system','self_diagnosis','optimization',{'suggestions':ids}); notify('self_optimization_suggestion',{'suggestions':ids}); return {'created':ids,'count':len(ids)}
def create_life_signal(category,severity,title,detail,source='life_core'):
sid=uid('signal')
exec_sql('INSERT INTO life_signals(id,category,severity,title,detail,source,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(sid,category,severity,title,detail,source,'open',now()))
return sid
def submit_patch_item(actor, patch_type, title, target_area, content, risk='medium'):
pid=uid('patch')
exec_sql('INSERT INTO patch_queue(id,patch_type,title,target_area,content,submitted_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,patch_type,title,target_area,content,actor,'draft',risk,now()))
audit(actor,'patch_submitted',target_area,{'patch_id':pid,'title':title})
notify('patch_submitted',{'patch_id':pid,'title':title,'submitted_by':actor})
return {'patch_id':pid,'status':'draft'}
def life_diagnose_v5(actor='system'):
signals=[]
bad_roles=rows("SELECT username,role FROM users WHERE role NOT IN ('owner','admin','user')")
if bad_roles:
signals.append(create_life_signal('security','critical','发现非法角色','只允许 owner/admin/user,发现: '+jdump(bad_roles)))
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
if order != ['owner','admin','user']:
signals.append(create_life_signal('routing','high','迁移优先级异常','当前顺序: '+str(order)))
file_to_vision=rows("SELECT id,payload FROM events WHERE route_target='vision' AND payload LIKE '%file_%' ORDER BY created_at DESC LIMIT 20")
if file_to_vision:
signals.append(create_life_signal('routing','critical','文件引用误入 Vision','发现疑似文件误入 Vision 事件: '+str(len(file_to_vision))))
pending=len(rows("SELECT id FROM approvals WHERE status='pending'"))
if pending > 10:
signals.append(create_life_signal('ops','medium','审批积压','待审批数量: '+str(pending)))
if not signals:
signals.append(create_life_signal('health','low','自诊断未发现阻塞问题','当前未发现 P0/P1 阻塞。'))
for sid in signals:
sig=one('SELECT * FROM life_signals WHERE id=?',(sid,))
if sig and sig['severity'] in ('critical','high'):
exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(uid('suggest'),sig['category'],sig['severity'],'修复建议:'+sig['title'],sig['detail'],'生成补丁计划,经 owner 审批后由 Codex/OpenClaw 执行;执行前备份,失败可回滚。',0,'open',now()))
audit(actor,'life_diagnose','life_core',{'signals':signals})
return {'signals':signals,'status':'ok'}
def life_plan_v5(actor):
open_signals=rows("SELECT id,title,severity FROM life_signals WHERE status='open' ORDER BY created_at DESC LIMIT 20")
actions=[{'type':'backup','risk':'low'},{'type':'patch_generate','risk':'medium'},{'type':'safe_simulation','risk':'low'},{'type':'owner_approval','risk':'high'},{'type':'apply_delta','risk':'high'},{'type':'uat','risk':'low'},{'type':'rollback_on_failure','risk':'critical'}]
pid=uid('plan')
exec_sql('INSERT INTO life_plans(id,title,detail,linked_signal_ids,proposed_actions,risk,status,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,'数字生命体自优化计划','由当前 signals 生成的可审批执行计划',jdump([s['id'] for s in open_signals]),jdump(actions),'high','draft',actor,now()))
audit(actor,'life_plan_created','life_core',{'plan_id':pid})
return {'plan_id':pid,'actions':actions}
class H(BaseHTTPRequestHandler):
server_version='AIOSWebOps/23'
def log_message(self, *args): pass
def send_body(self, code, body, ctype='text/html; charset=utf-8', setcookies=None):
data=body.encode('utf-8') if isinstance(body,str) else body; self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(data)))
for c in setcookies or []: self.send_header('Set-Cookie',c)
self.end_headers(); self.wfile.write(data)
def redirect(self,path,setcookies=None):
self.send_response(302); self.send_header('Location',path)
for c in setcookies or []: self.send_header('Set-Cookie',c)
self.end_headers()
def user(self): return parse_session(self.headers.get('Cookie'))
def body_json(self):
n=int(self.headers.get('Content-Length','0') or 0); raw=self.rfile.read(n).decode('utf-8') if n else '{}'
try: return json.loads(raw or '{}')
except Exception: return {}
def json(self,obj,code=200): self.send_body(code,json.dumps(obj,ensure_ascii=False,indent=2),'application/json; charset=utf-8')
def page(self,path,user):
if path=='/dashboard': return '<div class="grid"><div class="card"><h3>User</h3><b>'+PUBLIC_DOMAIN+'</b></div><div class="card"><h3>WebOps</h3><b>'+WEBOPS_DOMAIN+'</b></div><div class="card"><h3>Roles</h3>owner / admin / user</div></div>'+table(rows('SELECT * FROM node_status'))+table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 5'))
if path=='/nodes': return ('<div class="card"><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'down\'})">Mark server-hw down</button><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'ok\'})">Mark server-hw ok</button></div>' if require(user,['owner','admin']) else '') + table(rows('SELECT * FROM node_status'))
if path=='/roles': return table([{'role':'owner','capability':'all + approval'},{'role':'admin','capability':'ops + requests'},{'role':'user','capability':'AI usage only'}])
if path=='/routing': return table(rows('SELECT * FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/routing/request\',{role:\'user\',active_node:\'server-local\'})">Request user route to server-local</button></div>' if require(user,['owner','admin']) else '')
if path=='/failover': return table(rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/failover/dry-run\',{})">Dry Run</button><button class="danger" onclick="post(\'/api/failover/request\',{target:\'server-local\'})">Request Failover</button><button class="warn" onclick="post(\'/api/failover/request-failback\',{target:\'server-hw\'})">Request Failback</button></div>' if require(user,['owner','admin']) else '')
if path=='/approvals': return table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="aid" placeholder="approval id"><button onclick="post(\'/api/approvals/approve\',{approval_id:document.getElementById(\'aid\').value})">Approve</button><button class="danger" onclick="post(\'/api/approvals/reject\',{approval_id:document.getElementById(\'aid\').value})">Reject</button></div>' if require(user,['owner']) else '')
if path=='/operations': return table(rows('SELECT * FROM operations ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'healthcheck\'})">Healthcheck</button><button onclick="post(\'/api/ops/queue\',{op_type:\'backup\'})">Backup</button><button onclick="post(\'/api/ops/queue\',{op_type:\'run_uat\'})">Run UAT</button><button class="danger" onclick="post(\'/api/ops/request-high-risk\',{op_type:\'rollback_release\'})">Request Rollback</button></div>' if require(user,['owner','admin']) else '')
if path=='/users': return table(rows('SELECT id,username,display_name,role,status,created_at,updated_at FROM users ORDER BY created_at DESC')) + ('<div class="card"><input id="uu" placeholder="username"><select id="ur"><option>user</option><option>admin</option><option>owner</option></select><select id="us"><option>approved</option><option>pending</option><option>disabled</option></select><button onclick="post(\'/api/users/update\',{username:document.getElementById(\'uu\').value,role:document.getElementById(\'ur\').value,status:document.getElementById(\'us\').value})">Update</button></div>' if require(user,['owner','admin']) else '')
if path=='/quota': return table(rows('SELECT * FROM quota_policy ORDER BY username')) + ('<div class="card"><input id="qu" placeholder="username"><input id="ql" placeholder="daily limit"><button onclick="post(\'/api/quota/set\',{username:document.getElementById(\'qu\').value,daily_limit:Number(document.getElementById(\'ql\').value)})">Set</button></div>' if require(user,['owner','admin']) else '')
if path=='/models': return table(rows('SELECT * FROM model_policy ORDER BY role')) + ('<div class="card"><button onclick="post(\'/api/models/request\',{role:\'user\',default_model:\'deepseekv4-flash\'})">Request Model Policy Change</button></div>' if require(user,['owner','admin']) else '')
if path=='/memory': return table(rows('SELECT id,username,scope,status,created_at FROM memory_items ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="memu" placeholder="username"><button onclick="post(\'/api/memory/export\',{username:document.getElementById(\'memu\').value})">Export</button><button onclick="post(\'/api/memory/compress\',{username:document.getElementById(\'memu\').value})">Compress</button><button class="danger" onclick="post(\'/api/memory/request-delete\',{username:document.getElementById(\'memu\').value})">Request Delete</button></div>' if require(user,['owner','admin']) else '')
if path=='/channels/wechat': return table(rows('SELECT * FROM owner_notifications ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="wxuser" value="sample-user"><select id="wxtype"><option>text</option><option>quote_text</option><option>image</option><option>quote_image</option><option>file_pdf</option><option>file_excel</option><option>quote_file</option></select><textarea id="wxmsg">你好,测试消息</textarea><button onclick="post(\'/api/simulation/wechat\',{username:document.getElementById(\'wxuser\').value,message_type:document.getElementById(\'wxtype\').value,content:document.getElementById(\'wxmsg\').value})">Send Simulated Message</button></div>' if require(user,['owner','admin']) else '')
if path=='/life': return '<div class="card"><button onclick="post(\'/api/life/diagnose\',{})">Observe & Diagnose</button><button onclick="post(\'/api/life/plan\',{})">Generate Plan</button><button onclick="post(\'/api/life/request-apply\',{plan_id:\'latest\'})">Request Apply</button></div><h2>Signals</h2>'+table(rows('SELECT * FROM life_signals ORDER BY created_at DESC LIMIT 100'))+'<h2>Plans</h2>'+table(rows('SELECT * FROM life_plans ORDER BY created_at DESC LIMIT 100'))
if path=='/patches': return '<div class="card"><input id="pt" placeholder="title"><select id="ptype"><option>doc</option><option>config</option><option>script</option><option>runtime_patch</option></select><input id="pa" placeholder="target_area"><textarea id="pc" placeholder="patch/content"></textarea><button onclick="post(\'/api/patches/submit\',{title:document.getElementById(\'pt\').value,patch_type:document.getElementById(\'ptype\').value,target_area:document.getElementById(\'pa\').value,content:document.getElementById(\'pc\').value})">Submit Patch</button></div>'+table(rows('SELECT * FROM patch_queue ORDER BY created_at DESC LIMIT 100'))
if path=='/versions': return '<div class="card"><button onclick="post(\'/api/versions/create\',{version_name:\'v5-webops-digital-life\'})">Create Version Record</button><button onclick="post(\'/api/versions/request-publish\',{version_name:\'v5-webops-digital-life\'})">Request Publish</button></div>'+table(rows('SELECT * FROM baseline_versions ORDER BY created_at DESC LIMIT 100'))
if path=='/evidence': return '<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'export_evidence\'})">Export Evidence</button></div>'+table(rows('SELECT * FROM operations WHERE op_type="export_evidence" ORDER BY created_at DESC LIMIT 100'))
if path=='/optimization': return table(rows('SELECT * FROM self_optimization_suggestions ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/optimization/diagnose\',{})">Run Diagnosis</button><input id="sid" placeholder="suggestion id"><button onclick="post(\'/api/optimization/apply-request\',{suggestion_id:document.getElementById(\'sid\').value})">Request Apply</button></div>' if require(user,['owner','admin']) else '')
if path=='/simulation': return '<div class="card"><button onclick="post(\'/api/simulation/run-suite\',{})">Run Full Simulation</button></div>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 100'))
if path=='/logs': return '<h2>Audit</h2>'+table(rows('SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 200'))+'<h2>Events</h2>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 200'))
if path=='/config': return '<pre>'+esc(json.dumps({'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'},'roles':ROLES,'nodes':NODES},ensure_ascii=False,indent=2))+'</pre>'
if path=='/uat': return table(rows('SELECT * FROM uat_results ORDER BY created_at DESC LIMIT 200')) + ('<div class="card"><button onclick="post(\'/api/uat/run\',{})">Run UAT</button></div>' if require(user,['owner','admin']) else '')
return '<p>Not found</p>'
def do_GET(self):
init_db(); path=urllib.parse.urlparse(self.path).path; user=self.user()
if path=='/': self.redirect('/dashboard'); return
if path=='/health/ready': self.json({'status':'ok','roles':ROLES,'nodes':NODES,'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'}}); return
if path=='/login': self.send_body(200,layout('Login','<div class="card"><form method="POST" action="/login"><input name="username"><input name="password" type="password"><button>Login</button></form></div>',None)); return
if path=='/logout': self.redirect('/login',["aios_session=; Path=/; Max-Age=0"]); return
if path=='/register': self.send_body(200,layout('Register','<div class="card"><form method="POST" action="/register"><input name="username" placeholder="username"><input name="display_name" placeholder="display name"><input name="password" type="password"><button>Register</button></form></div>',None)); return
if not user: self.redirect('/login'); return
known=[p for p,t in PAGES]
if path in known: self.send_body(200,layout(path.strip('/').title(),self.page(path,user),user)); return
self.send_body(404,layout('Not Found','<p>not found</p>',user))
def do_POST(self):
init_db(); path=urllib.parse.urlparse(self.path).path
if path=='/login':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved'))
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Strict"]); return
self.send_body(403,layout('Login Failed','<p>登录失败。</p>',None)); return
if path=='/register':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
username=data.get('username',[''])[0].strip(); pw=data.get('password',[''])[0]; display=data.get('display_name',[''])[0]
if not username or not pw: self.send_body(400,'missing username/password'); return
exec_sql('INSERT OR IGNORE INTO users(id,username,display_name,role,status,password_hash,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)',(uid('user'),username,display,'user','pending',hash_pw(pw),now(),now()))
approval(username,'user_registration',{'username':username},'low'); self.send_body(200,layout('Register Submitted','<p>注册已提交,等待审核。</p>',None)); return
user=self.user()
if not user: self.json({'error':'not_authenticated'},401); return
data={}; n=int(self.headers.get('Content-Length','0') or 0)
if n:
try: data=json.loads(self.rfile.read(n).decode() or '{}')
except Exception: data={}
actor=user['username']; role=user['role']
try:
if path=='/api/nodes/health' and require(user,['owner','admin']): exec_sql('UPDATE node_status SET health=?,last_seen_at=?,note=? WHERE node_id=?',(data.get('health'),now(),f'updated_by={actor}',data.get('node_id'))); audit(actor,'node_health_update',data.get('node_id'),data); self.json({'status':'ok'}); return
if path=='/api/routing/request' and require(user,['owner','admin']):
if data.get('role') not in ROLES or data.get('active_node') not in NODES: raise ValueError('bad routing')
if role=='owner': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(data['active_node'],now(),data['role'])); audit(actor,'routing_changed','routing_policy',data); self.json({'status':'executed'}); return
self.json(approval(actor,'routing_change',data,'high')); return
if path=='/api/failover/dry-run' and require(user,['owner','admin']): self.json({'status':'dry_run','migration_order':rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')}); return
if path in ['/api/failover/request','/api/failover/request-failback'] and require(user,['owner','admin']): self.json(approval(actor,'failover_execute' if path.endswith('request') else 'failback_execute',data,'critical')); return
if path=='/api/approvals/approve' and require(user,['owner']):
ap=one('SELECT * FROM approvals WHERE id=?',(data.get('approval_id'),)); payload=jload(ap['payload']) if ap else {}
if not ap: raise ValueError('approval not found')
result={'approved':True}
if ap['action']=='routing_change': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(payload.get('active_node'),now(),payload.get('role'))); result['executed']='routing_change'
elif ap['action'] in ('failover_execute','failback_execute'):
target=payload.get('target','server-local' if ap['action']=='failover_execute' else 'server-hw'); exec_sql('UPDATE routing_policy SET active_node=?,updated_at=?',(target,now())); result={'executed':ap['action'],'target':target}
elif ap['action']=='user_registration': exec_sql('UPDATE users SET status=?,updated_at=? WHERE username=?',('approved',now(),payload.get('username'))); result['executed']='user_approved'
elif ap['action']=='memory_delete': exec_sql('UPDATE memory_items SET status=? WHERE username=?',('soft_deleted',payload.get('username'))); result['executed']='memory_soft_deleted'
exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('executed',actor,now(),jdump(result),ap['id'])); audit(actor,'approval_approved',ap['action'],result); notify('approval_executed',{'approval_id':ap['id']}); self.json({'status':'executed','result':result}); return
if path=='/api/approvals/reject' and require(user,['owner']): exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('rejected',actor,now(),jdump({'rejected':True}),data.get('approval_id'))); audit(actor,'approval_rejected','approvals',data); self.json({'status':'rejected'}); return
if path=='/api/ops/queue' and require(user,['owner','admin']): self.json(queue_op(actor,data.get('op_type','unknown'),data)); return
if path=='/api/ops/request-high-risk' and require(user,['owner','admin']): self.json(approval(actor,data.get('op_type','unknown'),data,'critical')); return
if path=='/api/users/update' and require(user,['owner','admin']):
if data.get('role')=='owner' and role!='owner': raise PermissionError('only owner can grant owner')
if data.get('role') not in ROLES: raise ValueError('bad role')
exec_sql('UPDATE users SET role=?,status=?,updated_at=? WHERE username=?',(data.get('role'),data.get('status'),now(),data.get('username'))); audit(actor,'user_update',data.get('username'),data); self.json({'status':'updated'}); return
if path=='/api/quota/set' and require(user,['owner','admin']): exec_sql('INSERT INTO quota_policy(username,daily_limit,used_today,updated_at) VALUES(?,?,0,?) ON CONFLICT(username) DO UPDATE SET daily_limit=excluded.daily_limit,updated_at=excluded.updated_at',(data.get('username'),int(data.get('daily_limit',0)),now())); audit(actor,'quota_set',data.get('username'),data); self.json({'status':'updated'}); return
if path=='/api/models/request' and require(user,['owner','admin']): self.json(approval(actor,'model_policy_change',data,'high')); return
if path=='/api/memory/export' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_export',data)); return
if path=='/api/memory/compress' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_compress',data)); return
if path=='/api/memory/request-delete' and require(user,['owner','admin']): self.json(approval(actor,'memory_delete',data,'high')); return
if path=='/api/simulation/wechat' and require(user,['owner','admin']): self.json(simulate_wechat(data.get('username','sample-user'),data.get('message_type','text'),data.get('content',''))); return
if path=='/api/simulation/run-suite' and require(user,['owner','admin']): self.json(run_uat()); return
if path=='/api/life/diagnose' and require(user,['owner','admin']): self.json(life_diagnose_v5(actor)); return
if path=='/api/life/plan' and require(user,['owner','admin']): self.json(life_plan_v5(actor)); return
if path=='/api/life/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'apply_life_plan',data,'high')); return
if path=='/api/patches/submit' and require(user,['owner','admin']): self.json(submit_patch_item(actor,data.get('patch_type','doc'),data.get('title','untitled'),data.get('target_area','docs'),data.get('content',''),data.get('risk','medium'))); return
if path=='/api/patches/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'patch_apply',data,'high')); return
if path=='/api/versions/create' and require(user,['owner','admin']):
vid=uid('ver'); exec_sql('INSERT INTO baseline_versions(id,version_name,package_name,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(vid,data.get('version_name','v5-draft'),data.get('package_name','current'),data.get('summary','created from WebOps'),'draft',actor,now())); audit(actor,'version_created','baseline_versions',data); self.json({'version_id':vid}); return
if path=='/api/versions/request-publish' and require(user,['owner','admin']): self.json(approval(actor,'baseline_publish',data,'critical')); return
if path=='/api/optimization/diagnose' and require(user,['owner','admin']): self.json(diagnose()); return
if path=='/api/optimization/apply-request' and require(user,['owner','admin']): self.json(approval(actor,'apply_self_optimization_fix',data,'high')); return
if path=='/api/uat/run' and require(user,['owner','admin']): self.json(run_uat()); return
self.json({'error':'forbidden_or_unknown_endpoint','path':path},403)
except Exception as e:
audit(actor,'api_error',path,{'error':str(e),'data':data}); self.json({'error':str(e)},500)
def main():
init_db(); print(f'AIOS WebOps listening on {HOST}:{PORT} for {WEBOPS_DOMAIN}', flush=True); ThreadingHTTPServer((HOST,PORT),H).serve_forever()
if __name__=='__main__': main()