Compare commits

...
Author SHA1 Message Date
openclaw a0b6c9b78e chore: refresh branch timestamp 2026-07-09 11:05:19 +08:00
aios 187dcc3554 Add OpenClaw V1R3C1 corrective release execution instructions 2026-07-08 14:31:24 +08:00
aios 75fd3c6274 Add V1R3C1 corrective self-loop release proposal 2026-07-08 14:24:49 +08:00
aios ac5518588f Add V1R3C0 version proposal manifest 2026-07-08 10:24:51 +08:00
aios 54a2b7ea41 Add self-loop supervisor service deployment manifest 2026-07-08 10:24:49 +08:00
aios 3803f33d7b Add self evolution report generator 2026-07-08 10:24:47 +08:00
aios 62fb0613a0 Add resident self-loop worker model 2026-07-08 10:24:42 +08:00
aios 64e3e087f2 Add persistent failure memory stores 2026-07-08 10:24:38 +08:00
aios 5c470b8833 Add self-loop supervisor learning tests 2026-07-08 10:22:01 +08:00
aios 89a0a26b3b Add persistent self-loop supervisor runtime model 2026-07-08 10:21:58 +08:00
aios 0a6d50fff3 Add event hooks for autonomous self-loop triggers 2026-07-08 10:21:54 +08:00
aios 6c4f0a988d Add adaptive policy engine based on failure history 2026-07-08 10:21:47 +08:00
aios 7d79f40324 Add failure memory for autonomous self-loop learning 2026-07-08 10:21:42 +08:00
aios 1fec447ca2 Add self-loop runtime API integration test 2026-07-08 10:15:55 +08:00
aios c30d5aa12b Add self-loop runtime wiring configuration 2026-07-08 10:15:53 +08:00
aios 90b50a853e Add GitOps Actions adapter plan for self-loop 2026-07-08 10:15:50 +08:00
aios a1de052ff8 Add runtime API wiring for autonomous self-loop jobs 2026-07-08 10:15:47 +08:00
aios b3f9bdb759 Add full self-loop scheduler and production gate test 2026-07-08 10:14:25 +08:00
aios 2d3061d825 Add production acceptance gate 2026-07-08 10:14:22 +08:00
aios 9becff85ee Add GitOps commit executor adapter 2026-07-08 10:14:17 +08:00
aios b63a893d37 Add autonomous self-loop scheduler 2026-07-08 10:14:13 +08:00
aios 363a061cd3 Add patch executor acceptance pipeline test 2026-07-08 10:11:45 +08:00
aios 9ce79275e0 Add self-loop patch executor and acceptance pipeline 2026-07-08 10:11:42 +08:00
aios 087366bafb Add multi-agent self-loop orchestration test 2026-07-08 10:10:25 +08:00
aios cd495c15fe Add multi-agent self-loop orchestrator 2026-07-08 10:10:22 +08:00
aios f211fc1a96 Propagate owner gate policy through self-loop executor 2026-07-08 10:10:14 +08:00
aios 6b879c7539 Add self-loop executor governance test 2026-07-08 10:08:18 +08:00
aios 5ea27d7529 Add autonomous self-loop agent executor and safe test runner 2026-07-08 10:08:15 +08:00
aios af01b48f11 Add autonomous self-loop governance policy 2026-07-08 10:06:39 +08:00
aios 84c327816b Add autonomous self-loop acceptance test 2026-07-08 10:06:37 +08:00
aios edbc1397c1 Add autonomous self-loop orchestration core 2026-07-08 10:06:34 +08:00
aios 86e3bc351d Add end-to-end portal role workflow self tests 2026-07-08 10:00:00 +08:00
aios 60cefc1a60 Add portal RBAC approval alert audit persistence schema 2026-07-08 09:59:57 +08:00
aios 81c9a3759a Implement complete portal RBAC operations domain services 2026-07-08 09:59:53 +08:00
aios ab9a744d63 Add portal operations self loop tests 2026-07-08 09:56:52 +08:00
aios 65f88f6a7f Add portal OpenAPI operationId contract 2026-07-08 09:56:49 +08:00
aios 08a1c3f24d Add portal operations API domain layer for role dashboards 2026-07-08 09:56:46 +08:00
aios 730fbb61ab Create user portal RBAC ops dashboard implementation branch 2026-07-08 09:56:42 +08:00
aios c38c575d8e chore(release): bump baseline to V1R2C3 and create version record
- Update baseline/current.json: active_version=V1R2C3, active_tag=V1R2C3
- Create versions/V1R2C3.json with version record
- Update architecture/current.json: reflect V1R2C3
- Release reason: GPT Actions ClientResponseError compatibility recovery
- Preserve V1R2C2 rollback
- secret_plaintext_written=false
2026-07-08 01:09:02 +08:00
aios b1c1e49cc5 fix: add READABLE aliases for all API endpoints (V1R2C3)
Add descriptive, user-friendly aliases for the existing API routes to
improve gitops-agent API discoverability:

  GET  /api/baseline     → /api/base
  GET  /api/versions     → /api/ver
  GET  /api/runtime-status → /api/run
  GET  /api/patches      → /api/patch
  POST /api/create-patch  → /api/patch
  GET  /api/branches     → /api/branch
  POST /api/create-branch → /api/branch
  POST /api/commit-file   → /api/file
  POST /api/create-tag    → /api/tag

All aliases wire through the same handler functions, tested live.
2026-07-07 22:04:47 +08:00
aios b9e2358901 chore(test): add V1R2C2 autonomous recovery chain integration test
- integration_test_v1r2c2.py: full pipeline test covering ActionMonitor,
  ErrorClassifier, FailureRouter, RecoveryController end-to-end
- V1R2C2-autonomous-recovery-chain-acceptance-test-report.md: acceptance
  test report with all 6 test suites documented

All tests PASSED: module imports, monitor, classifier (10 patterns),
router (3 route types), controller (3 scenarios), API 500 scenario.
2026-07-07 20:57:27 +08:00
aios a1874c90ae chore(baseline): mark autonomous_recovery_loop feature enabled for V1R2C2 2026-07-07 19:47:35 +08:00
aios 19a2ce8439 feat(V1R2C2): complete autonomous recovery chain integration with 8/8 tests passing
- Integrate ActionMonitor into ChatGPT GitOps API for failure tracking
- Add ErrorClassifier with permission, git, runtime, system, secret violation error types
- Add FailureRouter to route errors to correct recovery agents (openclaw/hermes/windows_codex_app)
- Add RecoveryController orchestrating full pipeline: capture→classify→route→plan→execute→validate→record
- Update error_classifier to include ClientResponseError, rate limit, and secret_violation patterns
- All 8 integration tests passing (ActionMonitor, ErrorClassifier, FailureRouter, RecoveryController, API 500 scenario)
- Push baseline to V1R2C2 with tag v1r2c2-baseline
2026-07-07 19:45:51 +08:00
aios f8fc7ad73b fix(cgpt-actions): add sys.path fix for ActionMonitor import and fix JSON serialization
- Insert agent-os path into sys.path so orchestrator module is found
- Fix JSON serialization of ActionFailureEvent by converting dataclass to dict in the endpoint
- Add __init__.py for orchestrator package

This is a bugfix on the V1R2C2 feature already merged into main.
2026-07-07 19:31:32 +08:00
aios e2b9cc6849 chore(report): add V1R2C2 cgpt-actions action monitor execution report 2026-07-07 18:00:52 +08:00
aios 4f4c95ffce chore(version): bump baseline to V1R2C2 for action monitor & confirm guards 2026-07-07 18:00:08 +08:00
aios edd3bd239e feat(cgpt-actions): add ActionMonitor for failure tracking & confirm-guard for tag/model/model-priority 2026-07-07 17:58:11 +08:00
aios 7a1e862a03 feat(V1R2C2): add autonomous recovery action monitor 2026-07-07 16:11:47 +08:00
aios dc0ac18aea chore(patches): remove stale artifact files
Remove 4 stale patch artifact files that have outlived their purpose:
- patches/nl/*.json - ephemeral natural-language plan records (unknown, patch_create)
- patches/patch_collab-*.branch.json - collab test branch records for branches
  that have been merged and had no local residual branch

The self-evolution-agent-orchestration-policy.json patch record is retained
because patch/self-evolution-agent-orchestration-policy branch is still live.
2026-07-07 15:53:02 +08:00
aios 9e3dbdf146 chore(version): bump baseline to V1R1C6 for chatgpt-actions consequential governance 2026-07-07 15:22:24 +08:00
aios 87b1a5357c feat(cgpt-actions): add confirm protection to tag/model/model-priority 2026-07-07 15:12:00 +08:00
aios 128a7d9050 feat(agent-os): implement V1R1C5 conditional approval mechanism with consensus engine updates and integration tests 2026-07-07 12:19:34 +08:00
aios e8cf0f6348 feat(agent-instructions): add v1r1c5 main merge verification instruction 2026-07-07 12:12:04 +08:00
aios 786a3b6250 feat(agent-instructions): add openclaw hermes improvement execution instruction 2026-07-07 11:36:11 +08:00
aios b9a739fd95 docs(agent-os): add Custom GPT solution review result 2026-07-07 11:34:50 +08:00
aios 0fb71cd191 feat(agent-governance): add owner channel registry policy 2026-07-07 11:33:21 +08:00
aios 3b8e74bed4 feat(agent-instructions): add owner approval gate workflow 2026-07-07 11:31:02 +08:00
aios 9df07a5d7d feat(agent-instructions): add owner notification routing 2026-07-07 11:29:48 +08:00
aios 54356c0fcc fix(agent-instructions): correct openclaw hermes routing 2026-07-07 11:27:51 +08:00
aios 4b62b18a39 docs(agent-instructions): add openclaw instruction index 2026-07-07 11:23:13 +08:00
aios bb69d76a7b docs(agent-os): add Windows Codex recovery admin assistance result 2026-07-07 11:21:47 +08:00
aios 27eef25d12 feat(agent-instructions): add openclaw hermes coordination instruction 2026-07-07 11:17:18 +08:00
aios 7b563590b4 feat(agent-instructions): add windows codex v1r1c5 recovery instruction 2026-07-07 11:17:14 +08:00
aios 50bceb1d45 docs(agent-os): add hermes review result template 2026-07-07 11:15:18 +08:00
aios f89619f576 docs(agent-os): add V1R1C5 multi agent review request 2026-07-07 11:13:57 +08:00
aios 365ea5d06c docs(agent-os): add V1R1C5 release readiness report 2026-07-07 11:12:58 +08:00
aios f15a19d525 feat(agent-os): add V1R1C5 evolution proposal 2026-07-07 11:11:57 +08:00
aios 3bdb5f9d59 docs(agent-os): add self evolution acceptance report 2026-07-07 11:09:25 +08:00
aios 4a9f3fff1a docs(reports): add UCC WeChat owner test report 2026-07-07 11:02:43 +08:00
aios ffbc1e0e01 feat(agent-instructions): add codex reports permission fix instruction 2026-07-07 10:58:23 +08:00
aios 8365afbeb8 feat(agent-os): add custom gpt solution review instruction 2026-07-07 10:56:55 +08:00
aios d34ef72f5d feat(agent-os): add hermes architecture review instruction 2026-07-07 10:56:20 +08:00
aios 7f118fa205 feat(agent-os): add openclaw maintenance instruction 2026-07-07 10:55:37 +08:00
aios 97867386a3 feat(agent-os): add codex recovery instruction 2026-07-07 10:53:59 +08:00
aios e17abc12be feat(agent-os): add runtime integration test suite 2026-07-07 10:52:19 +08:00
aios bec612c67d feat(agent-os): add recovery manager runtime 2026-07-07 10:50:45 +08:00
aios 19bcb8aeed feat(agent-os): add escalation engine runtime 2026-07-07 10:50:05 +08:00
aios 79e00ef089 feat(agent-os): add agent instruction template 2026-07-07 10:49:06 +08:00
aios 4833b9f93a feat(agent-os): add versioned agent instruction center 2026-07-07 10:45:39 +08:00
aios fd715457da fix(agent-os): restore instruction center usage examples 2026-07-07 10:44:24 +08:00
aios 4325f0b7d2 feat(agent-os): add versioned agent instruction center 2026-07-07 10:41:21 +08:00
aios b2a4f67f93 chore(chatgpt): record nl plan patch_create 2026-07-07 10:37:29 +08:00
aios c2e4a5b078 feat(agent-os): add consensus engine runtime 2026-07-07 10:18:16 +08:00
aios 707cfe4d60 feat(agent-os): add agent evaluator runtime 2026-07-07 10:17:39 +08:00
aios cf97cf697d feat(agent-os): add skill loader runtime 2026-07-07 10:17:14 +08:00
aios 82eecc27b4 feat(agent-os): add memory service runtime 2026-07-07 10:16:49 +08:00
aios bea425052f feat(agent-os): add message bus runtime 2026-07-07 10:16:27 +08:00
aios 45c26a1a4a feat(agent-os): add task scheduler runtime 2026-07-07 10:16:07 +08:00
aios 6ea5047dbb feat(agent-os): add runtime lifecycle manager 2026-07-07 10:15:32 +08:00
aios a5fdfbf057 feat(agent-os): add agent registry runtime implementation 2026-07-07 10:15:05 +08:00
aios 8a5ce63069 feat(agent-os): implement runtime framework 2026-07-07 10:12:27 +08:00
aios bce254790c feat(agent-os): add release automation framework 2026-07-07 09:50:12 +08:00
aios 14a3b84fc6 feat(agent-os): add continuous evolution engine 2026-07-07 09:45:56 +08:00
aios 39f23b0891 feat(agent-os): add consensus engine framework 2026-07-07 09:42:42 +08:00
aios 034d5f0faa feat(agent-os): add acceptance pipeline framework 2026-07-07 09:42:07 +08:00
aios cb6851b751 feat(agent-os): add task scheduler policy 2026-07-07 09:41:42 +08:00
aios a79010f788 feat(agent-os): add skill package schema 2026-07-07 09:41:01 +08:00
aios 95b1d82738 feat(agent-os): add memory schema 2026-07-07 09:40:12 +08:00
aios afa5c0eb3e feat(agent-os): add agent protocol schema 2026-07-07 09:38:51 +08:00
aios ca9d7a7424 feat(agent-os): add runtime component definitions 2026-07-07 09:36:58 +08:00
aios 890699efaa feat(agent-os): add implementation roadmap for agent platform 2026-07-07 09:35:01 +08:00
aios 142f827feb feat(agent-os): add governance framework 2026-07-07 09:03:03 +08:00
aios 32949d9dac feat(agent-os): add platform roadmap modules 2026-07-07 08:59:52 +08:00
aios 4c505d2b6a feat(agent-os): add agent operating system blueprint 2026-07-07 08:48:45 +08:00
aios d2bcf4d5af feat(self-evolution): define server agent roles memory and skills 2026-07-07 08:40:36 +08:00
aios 76b33c0c6a feat(self-evolution): add agent orchestration policy config 2026-07-07 08:20:54 +08:00
aios 9babbcca43 feat(self-evolution): add multi-agent orchestration policy patch 2026-07-07 08:19:00 +08:00
aios c387c83c13 chore(chatgpt): record nl plan unknown 2026-07-07 08:03:34 +08:00
aios a9ef9197e6 fix-auth-gitops-owner-sync 2026-07-06 19:16:31 +08:00
aios 54870ed375 release: validate Codex and custom GPT collaboration 2026-07-06 18:42:34 +08:00
aios c850858973 merge(collab): include custom_gpt patch 20260706T104130Z 2026-07-06 18:42:33 +08:00
aios d08a7043d6 merge(collab): include local_codex patch 20260706T104130Z 2026-07-06 18:42:33 +08:00
aios c393931f64 test(collab): add custom_gpt evidence 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios a62d4ce2c1 test(collab): add local_codex evidence 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios 5a21bd5d7e test(collab): create custom_gpt branch 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios ed92b88fc0 test(collab): create local_codex branch 20260706T104130Z 2026-07-06 18:41:30 +08:00
aios a248142158 fix(api): sync Codex patch branches to online Git 2026-07-06 18:27:34 +08:00
aios ecf673500b release: configure Codex online GitOps versioning 2026-07-06 18:21:56 +08:00
aios 43ddb91568 fix(api): allow secret_ref model commits 2026-07-06 17:41:46 +08:00
aios 0b0505a645 test(api): include ChatGPT adapter contract checks 2026-07-06 17:37:30 +08:00
aios 96c00d81a9 feat(api): add ChatGPT GitOps Actions access 2026-07-06 17:36:09 +08:00
aios e7d36ad706 fix(auth): make cnnetmkt Casdoor primary 2026-07-06 17:07:09 +08:00
aios d6cb9d1529 fix(ingress): ensure cnnetmkt apex DNS record 2026-07-06 16:39:07 +08:00
aios c06a23f4f8 docs(ingress): archive cnnetmkt wildcard certificate rollout 2026-07-06 16:25:52 +08:00
aios 568c283aff fix(ingress): switch cnnetmkt to wildcard certificate 2026-07-06 16:24:28 +08:00
aios 87f9150806 chore(ingress): add SSL ingress parallel validator 2026-07-06 15:50:27 +08:00
aios 14fd6ee5c5 docs(ingress): record cnnetmkt SSL ingress status 2026-07-06 15:49:04 +08:00
aios b9328d4161 fix(ingress): add g.cnnetmkt.top GitOps alias 2026-07-06 15:48:12 +08:00
aios d8ae7ee6f6 docs(gitops): finalize V1R1C0 online baseline acceptance 2026-07-06 15:16:03 +08:00
aios 2ae60299b7 chore(gitops): archive final route and Gitea probes 2026-07-06 15:15:55 +08:00
aios c6b172dce7 chore(gitops): archive V1R1C0 migration operation scripts 2026-07-06 15:15:42 +08:00
aios d3cfef4147 docs(gitops): finalize V1R1C0 online baseline acceptance 2026-07-06 15:13:44 +08:00
195 changed files with 15385 additions and 47 deletions
+1 -1
View File
@@ -1 +1 @@
V1R1C0
V1R1C4
+93
View File
@@ -0,0 +1,93 @@
"""Adaptive policy engine for self-loop optimization.
Optimizes retry, cooldown, and low-risk automation choices from failure history.
It never disables owner gates or security controls.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any
from .failure_memory import FailureMemory
@dataclass(frozen=True)
class AdaptiveDecision:
retry_limit: int
cooldown_seconds: int
automation_level: str
risk_level: str
owner_approval_required: bool
reasons: tuple[str, ...]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class AdaptivePolicy:
def __init__(self, default_retry_limit: int = 3, default_cooldown_seconds: int = 30):
self.default_retry_limit = default_retry_limit
self.default_cooldown_seconds = default_cooldown_seconds
def evaluate(
self,
memory: FailureMemory,
runtime: dict[str, Any],
baseline: dict[str, Any],
goal: str,
) -> AdaptiveDecision:
reasons: list[str] = []
retry_limit = self.default_retry_limit
cooldown = self.default_cooldown_seconds
risk = "low"
automation = "safe"
top = memory.top_failures(1)
if top:
score = float(top[0].get("recurrence_score", 0))
if score >= 0.7:
retry_limit = 1
cooldown = 300
risk = "high"
automation = "analysis_only"
reasons.append("recurrent failure detected; reduce automatic retries")
elif score >= 0.4:
cooldown = 120
reasons.append("failure recurrence increasing; slow down loop")
if runtime.get("status") != "active":
risk = "high"
automation = "analysis_only"
reasons.append("runtime not healthy")
if baseline.get("codex_gitops", {}).get("direct_main_push"):
risk = "high"
automation = "analysis_only"
reasons.append("direct main push policy drift")
owner_gate = True
reasons.append("owner gate preserved")
if not reasons:
reasons.append(f"optimize policy for goal: {goal}")
return AdaptiveDecision(
retry_limit=retry_limit,
cooldown_seconds=cooldown,
automation_level=automation,
risk_level=risk,
owner_approval_required=owner_gate,
reasons=tuple(reasons),
)
def apply_to_scheduler(self, decision: AdaptiveDecision) -> dict[str, Any]:
return {
"max_attempts": decision.retry_limit,
"cooldown_seconds": decision.cooldown_seconds,
"automation_level": decision.automation_level,
"owner_approval_required": decision.owner_approval_required,
}
__all__ = ["AdaptiveDecision", "AdaptivePolicy"]
@@ -0,0 +1,300 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(os.getenv('AIOS_GITOPS_WORKSPACE', '/data/aios-gitops/workspace')) / 'runtime' / 'agent-os'))
from orchestrator.action_monitor import ActionMonitor
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WS = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
SECRETS = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS / 'gitops.tokens')))
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios-v23.git')))
monitor = ActionMonitor()
BASE_MODELS = [
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display_name':'DeepSeek V4 Flash','scope':'global'},
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display_name':'Qwen 3.6 35B','scope':'global'},
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display_name':'GPT-5.5','scope':'global'},
]
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id') -> str: return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def safe(s: str, n=160) -> str: return re.sub(r'[^A-Za-z0-9_./@-]+', '-', str(s or 'unknown')).strip('/-')[:n] or 'unknown'
def sh(args, check=False):
p = subprocess.run(['git'] + args, cwd=str(WS), text=True, capture_output=True)
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git failed')
return {'code':p.returncode,'stdout':p.stdout.strip(),'stderr':p.stderr.strip(),'cmd':['git']+args}
def jload(p: Path, default):
try: return json.loads(p.read_text(encoding='utf-8'))
except Exception: return default
def relpath(path: str) -> str:
x = str(path or '').replace('\\','/').strip()
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
return str(Path(x))
def branch_name(name: str | None, prefix='patch/chatgpt') -> str:
b = safe(name or f'{prefix}-{uid("br")}', 180)
if b in ('main','master') or b.startswith('main/') or b.startswith('master/'): raise ValueError('direct main/master writes are forbidden')
return b
def _json_has_plain_secret(obj: Any) -> bool:
if isinstance(obj, dict):
for k, v in obj.items():
lk = str(k).lower()
if lk == 'secret_ref' or lk.endswith('_secret_ref'):
continue
if lk in {'api_key', 'access_key', 'secret_access_key', 'secret_key', 'password', 'token', 'bearer_token'}:
if isinstance(v, str) and v and not v.startswith('secret://') and v != 'REDACTED':
return True
if _json_has_plain_secret(v):
return True
return False
if isinstance(obj, list):
return any(_json_has_plain_secret(x) for x in obj)
if isinstance(obj, str):
return bool(obj and not obj.startswith('secret://') and obj != 'REDACTED' and SECRET_VAL_RE.search(obj))
return False
def content_has_secret(text: str) -> bool:
text = text or ''
try:
return _json_has_plain_secret(json.loads(text))
except Exception:
pass
if SECRET_VAL_RE.search(text):
return True
for m in SECRET_RE.finditer(text):
key = str(m.group(1)).lower()
value = str(m.group(2))
if 'secret_ref' in key or value.startswith('secret://') or value == 'REDACTED':
continue
return True
return False
def token_records():
if not TOKEN_FILE.exists(): return []
out=[]
for raw in TOKEN_FILE.read_text(encoding='utf-8').splitlines():
line=raw.strip()
if not line or line.startswith('#'): continue
if line.startswith('{'):
try: out.append(json.loads(line))
except Exception: pass
else:
out.append({'account':'legacy-token','role':'owner','scopes':['*'],'token_plain':line})
return out
def authenticate(token: str):
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1': return {'account':'dev-owner','role':'owner','scopes':['*']}
if not token: return None
digest=hashlib.sha256(token.encode()).hexdigest()
for r in token_records():
if r.get('disabled'): continue
if r.get('token_sha256') and hmac.compare_digest(str(r['token_sha256']), digest): return r
if r.get('token_plain') and hmac.compare_digest(str(r['token_plain']), token): return r
return None
def sanitize(obj: Any) -> Any:
if isinstance(obj, dict):
return {k: ('REDACTED' if re.search(r'(?i)(api[_-]?key|secret|password|token)', str(k)) else sanitize(v)) for k,v in obj.items()}
if isinstance(obj, list): return [sanitize(x) for x in obj]
if isinstance(obj, str): return SECRET_VAL_RE.sub('REDACTED', obj)
return obj
def ensure_git():
if not sh(['config','user.email'])['stdout']: sh(['config','user.email','chatgpt-gitops-bot@cnnetmkt.top'], True)
if not sh(['config','user.name'])['stdout']: sh(['config','user.name','chatgpt-gitops-bot'], True)
def checkout(branch, base='main', create=False):
ensure_git(); b=branch_name(branch)
if create or sh(['rev-parse','--verify',b])['code'] != 0:
sh(['checkout',base], True); sh(['checkout','-B',b], True)
else: sh(['checkout',b], True)
return b
def write_file(path, content):
if content_has_secret(content): raise ValueError('secret_rejected: use secret_ref; plaintext API keys must not enter Git')
r=relpath(path); t=WS/r; t.parent.mkdir(parents=True, exist_ok=True); t.write_text(content, encoding='utf-8'); return r
def write_json(path, data): return write_file(path, json.dumps(data, ensure_ascii=False, indent=2) + '\n')
def sync_branch(branch: str) -> dict:
if not branch or branch in ('main','master') or branch.startswith('main/') or branch.startswith('master/'):
return {'status':'skipped','reason':'protected_branch','branch':branch}
if BARE_REPO.exists():
ref=f'refs/heads/{branch}:refs/heads/{branch}'
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git sync failed for {branch}')
return {'status':'synced','target':'bare','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
p=subprocess.run(['git','push','origin',f'HEAD:refs/heads/{branch}'], cwd=str(WS), text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git push failed for {branch}')
return {'status':'synced','target':'origin','branch':branch,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def sync_tag(tag: str) -> dict:
if not tag: return {'status':'skipped','reason':'empty_tag'}
if BARE_REPO.exists():
ref=f'refs/tags/{tag}:refs/tags/{tag}'
p=subprocess.run(['git','--git-dir',str(BARE_REPO),'fetch',str(WS),ref], text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag sync failed for {tag}')
return {'status':'synced','target':'bare','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
p=subprocess.run(['git','push','origin',tag], cwd=str(WS), text=True, capture_output=True)
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def commit(paths, msg):
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']
if not sh(['status','--short'])['stdout']:
return {'status':'no_changes','branch':branch,'sync':sync_branch(branch)}
out=sh(['commit','-m',msg], True); branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']; sync=sync_branch(branch); return {'status':'committed','branch':branch,'rev':sh(['rev-parse','--short','HEAD'], True)['stdout'],'git':out,'sync':sync}
def audit(actor, action, target, payload):
p=ROOT/'artifacts'/'chatgpt-gitops-audit.jsonl'; p.parent.mkdir(parents=True, exist_ok=True)
with p.open('a', encoding='utf-8') as f: f.write(json.dumps({'at':now(),'actor':actor,'action':action,'target':target,'payload':sanitize(payload)}, ensure_ascii=False)+'\n')
def model_docs():
out=[]
for p in sorted((WS/'config'/'model_registry').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
return out
def patches():
out=[]
for p in sorted((WS/'patches').glob('**/*.json')): out.append(jload(p, {'path':str(p.relative_to(WS))}))
return out
class H(BaseHTTPRequestHandler):
server_version='AIOSChatGPTGitOps/V1R1C0'
def sendj(self, data, code=200):
b=json.dumps(data, ensure_ascii=False, indent=2).encode(); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
def sendt(self, text, ctype='text/plain; charset=utf-8', code=200):
b=text.encode(); self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
def body(self):
n=int(self.headers.get('Content-Length','0') or '0'); raw=self.rfile.read(n).decode() if n else '{}'
try: return json.loads(raw or '{}')
except Exception: return {'_raw':raw}
def actor(self):
auth=self.headers.get('Authorization',''); tok=auth[7:].strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
rec=authenticate(tok)
if rec: return {'ok':True,'account':safe(rec.get('account') or rec.get('name') or 'token-user'),'role':rec.get('role','user'),'scopes':set(rec.get('scopes') or [])}
return {'ok':False,'account':'anonymous','role':'anonymous','scopes':set()}
def need(self, scope):
a=self.actor()
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
return a
def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path; q=urllib.parse.parse_qs(u.query)
try:
if path in ('/health','/api/health'): return self.sendj({'status':'ok','service':'chatgpt-gitops-adapter','workspace':str(WS)})
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','/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','/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','/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','/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'):
latest=monitor.latest()
ldict={'event_id':latest.event_id,'action':latest.action,'error':latest.error,'context':latest.context,'created_at':latest.created_at} if latest else None
return self.sendj({'events':[{"event_id":e.event_id,"action":e.action,"error":e.error,"context":e.context,"created_at":e.created_at} for e in monitor.list_events()],'latest':ldict})
return self.sendj({'error':'not_found','path':path},404)
except Exception as e:
monitor.record_failure('do_GET', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
def do_PUT(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
if path == '/api/model-priority': return self.model_priority(data)
return self.sendj({'error':'not_found','path':path},404)
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
try:
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 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)
if path == '/api/nl': return self.nl(data)
return self.sendj({'error':'not_found','path':path},404)
except Exception as e:
monitor.record_failure('do_POST', str(e), {'path':path})
return self.sendj({'error':str(e)},500)
def branch(self,data):
a=self.need('branch:create');
if not a: return
b=checkout(data.get('branch'), data.get('base','main'), True); meta={'type':'branch','branch':b,'base':data.get('base','main'),'created_by':a['account'],'created_at':now(),'direct_main_push':False,'owner_approval_required':True}
r=write_json(f'patches/{safe(b).replace("/","_")}.branch.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create {b}'); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c})
def file(self,data):
a=self.need('file:write');
if not a: return
b=checkout(data.get('branch'), data.get('base','main'), bool(data.get('create_branch'))); r=write_file(data.get('path'), data.get('content','')); c=commit([r], data.get('message') or f'chore(chatgpt): update {r}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c})
def commit(self,data):
a=self.need('commit:create');
if not a: return
b=checkout(data.get('branch') or sh(['rev-parse','--abbrev-ref','HEAD'])['stdout'], data.get('base','main'), False); paths=data.get('paths') or ['.']; paths=[paths] if isinstance(paths,str) else paths; c=commit(paths, data.get('message') or 'chore(chatgpt): commit changes'); audit(a['account'],'commit',b,{'paths':paths,'commit':c}); self.sendj({'status':c['status'],'branch':b,'commit':c})
def patch(self,data):
a=self.need('patch:create');
if not a: return
name=data.get('patch_name') or data.get('name') or uid('patch'); b=checkout(data.get('branch') or f'patch/{safe(name)}', data.get('base','main'), True); meta={'id':uid('patch'),'patch_name':name,'branch':b,'risk':data.get('risk','medium'),'description':data.get('description',''),'created_by':a['account'],'created_at':now()}
r=write_json(f'patches/{safe(name)}.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create patch {name}'); audit(a['account'],'patch',name,meta); self.sendj({'status':'patch_branch_created','patch':meta,'commit':c})
def tag(self,data):
a=self.need('tag:create');
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
if a['role'] != 'owner' or not data.get('execute'):
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
def rollback(self,data):
a=self.need('rollback:request');
if not a: return
target=data.get('target_version') or data.get('version') or data.get('target')
if not target: return self.sendj({'error':'target_version required'},400)
b=checkout(data.get('branch') or f'request/rollback-{safe(target)}-{uid("rb")}', 'main', True); req={'id':uid('rollback'),'target_version':target,'reason':data.get('reason',''),'requested_by':a['account'],'approval_required':True,'execute':False,'created_at':now()}; r=write_json(f'baseline/rollback_requests/{safe(req["id"])}.json', req); c=commit([r], f'chore(chatgpt): request rollback {target}'); self.sendj({'status':'rollback_request_committed','branch':b,'request':req,'commit':c})
def model(self,data):
a=self.need('model:write');
if not a: return
name=data.get('model_name') or data.get('model'); base=(data.get('base_url') or '').rstrip('/'); prov=data.get('provider_name') or 'custom-provider'
if not name or not base: return self.sendj({'error':'provider_name/base_url/model_name required'},400)
owner=safe(data.get('username') or a['account']); secret_ref=data.get('api_key_secret_ref') or data.get('secret_ref')
if data.get('api_key'):
d=SECRETS/'llm'/owner; d.mkdir(parents=True, exist_ok=True); sid=uid('llmkey'); p=d/f'{sid}.secret'; p.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(p,0o600); secret_ref=f'secret://llm/{owner}/{sid}'
if not secret_ref: return self.sendj({'error':'api_key_secret_ref or api_key required'},400)
mid=data.get('model_id') or f'custom.{owner}.{hashlib.sha1((prov+base+name).encode()).hexdigest()[:10]}'; b=checkout(data.get('branch') or f'patch/model-{safe(mid)}','main',True)
doc=sanitize({k:v for k,v in data.items() if k!='api_key'}); doc.update({'model_id':mid,'provider_name':prov,'base_url':base,'model_name':name,'api_key_secret_ref':secret_ref,'created_by':a['account'],'created_at':now()})
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'model','hint':'This operation registers a new model. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
r=write_json(f'config/model_registry/chatgpt/{safe(mid)}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): register model {name}'); audit(a['account'],'model',mid,doc); self.sendj({'status':'model_registered','branch':b,'model_id':mid,'secret_ref':secret_ref,'commit':c})
def model_priority(self,data):
a=self.need('model:priority');
if not a: return
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'model-priority','hint':'This operation updates model priority. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
user=safe(data.get('username') or a['account']); b=checkout(data.get('branch') or f'patch/model-priority-{user}', 'main', True); doc={'username':user,'items':data.get('items') or data.get('priority') or [],'updated_by':a['account'],'updated_at':now()}; r=write_json(f'config/model_priority/{user}.json', doc); c=commit([r], data.get('message') or f'chore(chatgpt): update model priority {user}'); audit(a['account'],'model-priority',user,doc); self.sendj({'status':'updated','branch':b,'priority':doc,'commit':c})
def nl(self,data):
a=self.need('nl:plan');
if not a: return
text=data.get('text',''); low=text.lower(); intent='unknown'; plan=[]
if 'rollback' in low or '回滚' in text: intent='rollback_request'; plan=['commit rollback request','wait owner/admin approval']
elif 'diff' in low or '对比' in text: intent='diff'; plan=['call GET /api/diff']
elif 'model' in low or '模型' in text: intent='model_change'; plan=['use POST /api/model or PUT /api/model-priority','commit metadata']
elif 'patch' in low or '补丁' in text: intent='patch_create'; plan=['call POST /api/patch']
b=checkout(data.get('branch') or f'patch/nl-{uid("nl")}', 'main', True); doc={'id':uid('nl'),'text':text,'intent':intent,'action_plan':plan,'execute':False,'owner_approval_required':True,'created_by':a['account'],'created_at':now()}; r=write_json(f'patches/nl/{safe(doc["id"])}.json', doc); c=commit([r], f'chore(chatgpt): record nl plan {intent}'); self.sendj({'status':'planned','branch':b,'request':doc,'commit':c})
def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
+108
View File
@@ -0,0 +1,108 @@
"""Autonomous self-loop orchestration primitives.
Goal: detect gaps, classify risk, plan remediation, run checks, and emit an
approval-aware report without storing secrets or bypassing owner governance.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SelfLoopSignal:
name: str
status: str
severity: str = "info"
detail: str = ""
def ok(self) -> bool:
return self.status in {"ok", "done", "pass"}
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class RemediationStep:
id: str
title: str
action: str
risk: str = "low"
requires_owner_approval: bool = False
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class SelfLoopReport:
id: str
status: str
signals: list[SelfLoopSignal] = field(default_factory=list)
remediation: list[RemediationStep] = field(default_factory=list)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"status": self.status,
"signals": [s.to_dict() for s in self.signals],
"remediation": [r.to_dict() for r in self.remediation],
"created_at": self.created_at,
}
class AutonomousSelfLoop:
def __init__(self, owner_approval_required: bool = True):
self.owner_approval_required = owner_approval_required
def probe(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> list[SelfLoopSignal]:
return [
SelfLoopSignal("runtime_active", "ok" if runtime.get("status") == "active" else "fail", "critical"),
SelfLoopSignal("git_patch_only", "ok" if baseline.get("update_mode") == "git_patch_only" else "fail", "high"),
SelfLoopSignal("no_direct_main_push", "ok" if not baseline.get("codex_gitops", {}).get("direct_main_push", True) else "fail", "high"),
SelfLoopSignal("autonomous_recovery_loop", "ok" if baseline.get("features", {}).get("autonomous_recovery_loop") else "fail", "high"),
SelfLoopSignal("portal_gitops_dashboard", "ok" if baseline.get("features", {}).get("portal_gitops_dashboard") else "fail", "medium"),
]
def plan(self, signals: list[SelfLoopSignal]) -> list[RemediationStep]:
steps: list[RemediationStep] = []
failed = {s.name for s in signals if not s.ok()}
if "runtime_active" in failed:
steps.append(RemediationStep("restart-runtime", "恢复 GitOps Runtime", "request-runtime-recovery", "high", True))
if "git_patch_only" in failed:
steps.append(RemediationStep("enforce-git-patch", "恢复 git_patch_only 更新模式", "commit-policy-patch", "high", True))
if "no_direct_main_push" in failed:
steps.append(RemediationStep("disable-direct-main", "禁止直接推送 main", "commit-security-policy", "high", True))
if "autonomous_recovery_loop" in failed:
steps.append(RemediationStep("enable-recovery-loop", "启用自动恢复闭环", "commit-runtime-config", "medium", True))
if "portal_gitops_dashboard" in failed:
steps.append(RemediationStep("enable-portal-dashboard", "启用 Portal GitOps 驾驶舱", "commit-portal-config", "medium", True))
if not steps:
steps.append(RemediationStep("continue-implementation", "继续补齐 Router/UI/OpenAPI/验收", "create-low-risk-patch", "low", False))
return steps
def evaluate(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> SelfLoopReport:
signals = self.probe(runtime, baseline)
remediation = self.plan(signals)
status = "done" if all(s.ok() for s in signals) and not any(r.requires_owner_approval for r in remediation) else "partial"
return SelfLoopReport("self-loop-report", status, signals, remediation)
def acceptance(self, report: SelfLoopReport) -> dict[str, Any]:
approval_blocked = any(step.requires_owner_approval for step in report.remediation)
return {
"status": report.status,
"can_unattended_continue": report.status == "done" and not approval_blocked,
"owner_approval_required": approval_blocked or self.owner_approval_required,
"report": report.to_dict(),
}
__all__ = ["SelfLoopSignal", "RemediationStep", "SelfLoopReport", "AutonomousSelfLoop"]
+192
View File
@@ -0,0 +1,192 @@
"""Failure memory for autonomous self-loop learning.
The memory keeps sanitized failure fingerprints, recurrence counters, and
remediation hints. It is intentionally in-memory and serializable; production
adapters can persist the exported dict to SQL/object storage without changing the
learning contract. Secrets and raw tokens must never be stored here.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from hashlib import sha256
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def _clean(value: str | None) -> str:
if not value:
return "unknown"
cleaned = value.replace("\n", " ").replace("\r", " ").strip().lower()
secret_markers = ("token", "password", "secret", "api_key", "private key")
for marker in secret_markers:
cleaned = cleaned.replace(marker, "redacted")
return cleaned[:500]
@dataclass(frozen=True)
class FailureSignature:
component: str
reason: str
fingerprint: str
severity: str = "medium"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class FailureRecord:
signature: FailureSignature
count: int = 0
first_seen: str = field(default_factory=utc_now)
last_seen: str = field(default_factory=utc_now)
last_job_id: str | None = None
examples: list[str] = field(default_factory=list)
remediation_hints: list[str] = field(default_factory=list)
resolved_count: int = 0
def to_dict(self) -> dict[str, Any]:
return {
"signature": self.signature.to_dict(),
"count": self.count,
"first_seen": self.first_seen,
"last_seen": self.last_seen,
"last_job_id": self.last_job_id,
"examples": list(self.examples),
"remediation_hints": list(self.remediation_hints),
"resolved_count": self.resolved_count,
"recurrence_score": self.recurrence_score(),
}
def recurrence_score(self) -> float:
if self.count <= 1:
return 0.0
unresolved = max(self.count - self.resolved_count, 0)
return min(1.0, unresolved / max(self.count, 1))
class FailureMemory:
def __init__(self) -> None:
self.records: dict[str, FailureRecord] = {}
def fingerprint(self, component: str, reason: str, detail: str | None = None) -> str:
base = "::".join([_clean(component), _clean(reason), _clean(detail)])
return sha256(base.encode("utf-8")).hexdigest()[:16]
def signature(self, component: str, reason: str, detail: str | None = None, severity: str = "medium") -> FailureSignature:
return FailureSignature(
component=_clean(component),
reason=_clean(reason),
fingerprint=self.fingerprint(component, reason, detail),
severity=severity,
)
def record_failure(
self,
component: str,
reason: str,
detail: str | None = None,
severity: str = "medium",
job_id: str | None = None,
remediation_hint: str | None = None,
) -> FailureRecord:
sig = self.signature(component, reason, detail, severity)
record = self.records.get(sig.fingerprint)
if record is None:
record = FailureRecord(signature=sig)
self.records[sig.fingerprint] = record
record.count += 1
record.last_seen = utc_now()
record.last_job_id = job_id
if detail:
example = _clean(detail)
if example not in record.examples:
record.examples.append(example)
record.examples = record.examples[-5:]
if remediation_hint and remediation_hint not in record.remediation_hints:
record.remediation_hints.append(remediation_hint)
record.remediation_hints = record.remediation_hints[-5:]
return record
def record_from_job(self, job: dict[str, Any]) -> FailureRecord | None:
status = job.get("status")
if status in {"completed", "waiting_owner"}:
return None
reason = job.get("last_error") or status or "unknown_failure"
return self.record_failure(
component="self_loop_job",
reason=reason,
detail=str(job.get("result") or job.get("last_error") or ""),
severity="high" if status == "failed" else "medium",
job_id=job.get("id"),
remediation_hint="inspect acceptance report and regenerate patch plan",
)
def mark_resolved(self, fingerprint: str) -> bool:
record = self.records.get(fingerprint)
if not record:
return False
record.resolved_count += 1
record.last_seen = utc_now()
return True
def recurrence_score(self, fingerprint: str) -> float:
record = self.records.get(fingerprint)
return record.recurrence_score() if record else 0.0
def top_failures(self, limit: int = 5) -> list[dict[str, Any]]:
ranked = sorted(
self.records.values(),
key=lambda record: (record.recurrence_score(), record.count, record.last_seen),
reverse=True,
)
return [record.to_dict() for record in ranked[:limit]]
def suggest_remediation(self, fingerprint: str) -> list[str]:
record = self.records.get(fingerprint)
if not record:
return []
if record.remediation_hints:
return list(record.remediation_hints)
reason = record.signature.reason
if "runtime" in reason:
return ["run runtime health probe", "request owner-gated runtime recovery if probe fails"]
if "acceptance" in reason or "test" in reason:
return ["rerun acceptance pipeline", "generate targeted low-risk patch"]
if "permission" in reason or "forbidden" in reason:
return ["verify GitOps branch policy", "request owner review for permission drift"]
return ["classify failure", "generate remediation plan", "keep owner gate for non-low-risk changes"]
def export(self) -> dict[str, Any]:
return {
"records": {fingerprint: record.to_dict() for fingerprint, record in self.records.items()},
"top_failures": self.top_failures(),
}
def load(self, data: dict[str, Any]) -> None:
self.records.clear()
for fingerprint, raw in data.get("records", {}).items():
sig_raw = raw.get("signature", {})
signature = FailureSignature(
component=sig_raw.get("component", "unknown"),
reason=sig_raw.get("reason", "unknown"),
fingerprint=sig_raw.get("fingerprint", fingerprint),
severity=sig_raw.get("severity", "medium"),
)
record = FailureRecord(signature=signature)
record.count = int(raw.get("count", 0))
record.first_seen = raw.get("first_seen", utc_now())
record.last_seen = raw.get("last_seen", utc_now())
record.last_job_id = raw.get("last_job_id")
record.examples = list(raw.get("examples", []))[-5:]
record.remediation_hints = list(raw.get("remediation_hints", []))[-5:]
record.resolved_count = int(raw.get("resolved_count", 0))
self.records[fingerprint] = record
__all__ = ["FailureSignature", "FailureRecord", "FailureMemory"]
@@ -0,0 +1,98 @@
"""Persistence adapters for FailureMemory.
Stores sanitized failure memory snapshots. The adapter only persists exported
FailureMemory dictionaries; it does not store secrets or raw runtime credentials.
"""
from __future__ import annotations
import json
import sqlite3
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .failure_memory import FailureMemory
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class MemoryStoreStatus:
status: str
path: str
records: int
updated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class JsonFailureMemoryStore:
def __init__(self, path: str | Path):
self.path = Path(path)
def save(self, memory: FailureMemory) -> MemoryStoreStatus:
self.path.parent.mkdir(parents=True, exist_ok=True)
data = memory.export()
self.path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return MemoryStoreStatus("saved", str(self.path), len(data.get("records", {})))
def load(self) -> FailureMemory:
memory = FailureMemory()
if not self.path.exists():
return memory
memory.load(json.loads(self.path.read_text(encoding="utf-8")))
return memory
class SQLiteFailureMemoryStore:
def __init__(self, path: str | Path):
self.path = Path(path)
def _connect(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(self.path))
conn.execute(
"""
CREATE TABLE IF NOT EXISTS self_loop_failure_memory (
fingerprint TEXT PRIMARY KEY,
payload_json TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
return conn
def save(self, memory: FailureMemory) -> MemoryStoreStatus:
data = memory.export().get("records", {})
with self._connect() as conn:
for fingerprint, payload in data.items():
conn.execute(
"""
INSERT INTO self_loop_failure_memory (fingerprint, payload_json, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(fingerprint) DO UPDATE SET
payload_json = excluded.payload_json,
updated_at = excluded.updated_at
""",
(fingerprint, json.dumps(payload, ensure_ascii=False), utc_now()),
)
return MemoryStoreStatus("saved", str(self.path), len(data))
def load(self) -> FailureMemory:
memory = FailureMemory()
if not self.path.exists():
return memory
records: dict[str, Any] = {}
with self._connect() as conn:
for fingerprint, payload_json in conn.execute("SELECT fingerprint, payload_json FROM self_loop_failure_memory"):
records[fingerprint] = json.loads(payload_json)
memory.load({"records": records})
return memory
__all__ = ["MemoryStoreStatus", "JsonFailureMemoryStore", "SQLiteFailureMemoryStore"]
@@ -0,0 +1,59 @@
"""GitOps commit planning adapter for self-loop patches.
Creates commit intents and validates governance. Actual repository writes remain
through the controlled GitOps Actions layer.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_patch_executor import PatchBundle
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class CommitIntent:
branch: str
message: str
files: tuple[str, ...]
direct_main_push: bool
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class GitOpsCommitExecutor:
"""Produces safe commit intents instead of bypassing GitOps controls."""
def prepare(self, bundle: PatchBundle) -> CommitIntent:
if bundle.direct_main_push:
raise ValueError("direct main push is forbidden")
if not bundle.branch.startswith("patch/"):
raise ValueError("only patch branches are allowed")
return CommitIntent(
branch=bundle.branch,
message=f"Apply self-loop patch: {bundle.goal}",
files=tuple(file.path for file in bundle.files),
direct_main_push=False,
owner_approval_required=bundle.owner_approval_required,
)
def commit_result(self, intent: CommitIntent, revision: str | None = None) -> dict[str, Any]:
return {
"status": "ready" if not intent.owner_approval_required else "waiting_owner",
"branch": intent.branch,
"revision": revision,
"commit": intent.to_dict(),
"sync_required": True,
}
__all__ = ["CommitIntent", "GitOpsCommitExecutor"]
+33
View File
@@ -0,0 +1,33 @@
"""Portal operations API domain layer.
No secrets. Designed for user/group_admin/owner portal integration.
"""
ROLES = ("user", "group_admin", "owner")
class PortalOpsAPI:
def dashboard(self, role):
if role not in ROLES:
raise ValueError("invalid role")
return {
"role": role,
"modules": self.modules(role),
"status": "ok"
}
def modules(self, role):
common = ["profile", "alerts", "approvals", "audit"]
if role == "user":
return common + ["tasks", "usage"]
if role == "group_admin":
return common + ["group_users", "group_quota"]
return common + ["system", "gitops", "runtime", "security", "models"]
def approval(self, request_id, action):
return {"request_id": request_id, "action": action, "state": "recorded"}
def audit_event(self, actor, action):
return {"actor": actor, "action": action, "recorded": True}
def alert(self, alert_id, level):
return {"id": alert_id, "level": level, "state": "open"}
+493
View File
@@ -0,0 +1,493 @@
"""Three-role portal RBAC, workflow, alert, audit, and owner cockpit services.
This module is intentionally framework-neutral so it can be used by FastAPI routes,
unit tests, CLI checks, and future Hermes/OpenClaw agents without importing runtime
secrets or web framework globals.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Iterable
from uuid import uuid4
ROLE_USER = "user"
ROLE_GROUP_ADMIN = "group_admin"
ROLE_OWNER = "owner"
ROLES = (ROLE_USER, ROLE_GROUP_ADMIN, ROLE_OWNER)
SCOPE_SELF = "self"
SCOPE_GROUP = "group"
SCOPE_GLOBAL = "global"
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class PortalContext:
user_id: str
role: str = ROLE_USER
group_id: str | None = None
display_name: str | None = None
def natural_scope(self) -> str:
if self.role == ROLE_OWNER:
return SCOPE_GLOBAL
if self.role == ROLE_GROUP_ADMIN:
return SCOPE_GROUP
return SCOPE_SELF
@dataclass(frozen=True)
class PortalResource:
scope: str
owner_user_id: str | None = None
group_id: str | None = None
resource_type: str = "portal"
resource_id: str | None = None
@dataclass(frozen=True)
class RBACDecision:
allowed: bool
reason: str
role: str
scope: str
action: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class ApprovalRequest:
id: str
request_type: str
requester_id: str
scope: str
group_id: str | None = None
target: str | None = None
payload: dict[str, Any] = field(default_factory=dict)
state: str = "pending"
decided_by: str | None = None
decision_reason: str | None = None
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class AlertRecord:
id: str
level: str
message: str
scope: str
group_id: str | None = None
owner_user_id: str | None = None
source: str = "portal"
state: str = "open"
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class AuditEvent:
id: str
actor_id: str
action: str
scope: str
group_id: str | None = None
resource_type: str = "portal"
resource_id: str | None = None
outcome: str = "success"
detail: dict[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class PortalModule:
key: str
title: str
route: str
permission: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
ROLE_PERMISSIONS: dict[str, set[str]] = {
ROLE_USER: {
"portal.dashboard.read:self",
"portal.profile.read:self",
"portal.tasks.read:self",
"portal.usage.read:self",
"portal.alerts.read:self",
"portal.approvals.create:self",
"portal.approvals.read:self",
"portal.audit.read:self",
},
ROLE_GROUP_ADMIN: {
"portal.dashboard.read:self",
"portal.dashboard.read:group",
"portal.profile.read:self",
"portal.tasks.read:self",
"portal.usage.read:self",
"portal.alerts.read:self",
"portal.alerts.read:group",
"portal.approvals.create:self",
"portal.approvals.read:self",
"portal.approvals.read:group",
"portal.approvals.decide:group",
"portal.audit.read:self",
"portal.audit.read:group",
"portal.group.users.read:group",
"portal.group.quota.read:group",
"portal.group.quota.write:group",
},
ROLE_OWNER: {
"portal.*:*",
},
}
ROLE_MODULES: dict[str, tuple[PortalModule, ...]] = {
ROLE_USER: (
PortalModule("profile", "个人中心", "/portal/user/profile", "portal.profile.read:self"),
PortalModule("tasks", "任务", "/portal/user/tasks", "portal.tasks.read:self"),
PortalModule("usage", "用量", "/portal/user/usage", "portal.usage.read:self"),
PortalModule("alerts", "告警", "/portal/user/alerts", "portal.alerts.read:self"),
PortalModule("approvals", "审批", "/portal/user/approvals", "portal.approvals.read:self"),
PortalModule("audit", "审计", "/portal/user/audit", "portal.audit.read:self"),
),
ROLE_GROUP_ADMIN: (
PortalModule("group_users", "组用户", "/portal/group_admin/users", "portal.group.users.read:group"),
PortalModule("group_quota", "组配额", "/portal/group_admin/quota", "portal.group.quota.read:group"),
PortalModule("alerts", "组告警", "/portal/group_admin/alerts", "portal.alerts.read:group"),
PortalModule("approvals", "组审批", "/portal/group_admin/approvals", "portal.approvals.read:group"),
PortalModule("audit", "组审计", "/portal/group_admin/audit", "portal.audit.read:group"),
),
ROLE_OWNER: (
PortalModule("system", "系统驾驶舱", "/portal/owner/system", "portal.system.read:global"),
PortalModule("gitops", "GitOps", "/portal/owner/gitops", "portal.gitops.read:global"),
PortalModule("runtime", "运行时", "/portal/owner/runtime", "portal.runtime.read:global"),
PortalModule("security", "安全", "/portal/owner/security", "portal.security.read:global"),
PortalModule("models", "模型", "/portal/owner/models", "portal.models.read:global"),
PortalModule("approvals", "全局审批", "/portal/owner/approvals", "portal.approvals.read:global"),
PortalModule("alerts", "全局告警", "/portal/owner/alerts", "portal.alerts.read:global"),
PortalModule("audit", "全局审计", "/portal/owner/audit", "portal.audit.read:global"),
),
}
class PortalRBACService:
"""Enforces user=self, group_admin=group, owner=global access."""
def __init__(self, permissions: dict[str, set[str]] | None = None):
self.permissions = permissions or ROLE_PERMISSIONS
def authorize(self, ctx: PortalContext, action: str, resource: PortalResource | None = None) -> RBACDecision:
if ctx.role not in ROLES:
return RBACDecision(False, "unknown_role", ctx.role, SCOPE_SELF, action)
target = resource or PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id)
if ctx.role == ROLE_OWNER:
return RBACDecision(True, "owner_global_allow", ctx.role, target.scope, action)
if target.scope == SCOPE_GLOBAL:
return RBACDecision(False, "global_scope_requires_owner", ctx.role, target.scope, action)
if ctx.role == ROLE_USER:
if target.scope != SCOPE_SELF or target.owner_user_id not in (None, ctx.user_id):
return RBACDecision(False, "user_limited_to_self", ctx.role, target.scope, action)
return self._permission_decision(ctx.role, action, SCOPE_SELF)
if ctx.role == ROLE_GROUP_ADMIN:
if target.scope == SCOPE_SELF and target.owner_user_id in (None, ctx.user_id):
return self._permission_decision(ctx.role, action, SCOPE_SELF)
if target.scope == SCOPE_GROUP and ctx.group_id and target.group_id == ctx.group_id:
return self._permission_decision(ctx.role, action, SCOPE_GROUP)
return RBACDecision(False, "group_admin_limited_to_own_group", ctx.role, target.scope, action)
return RBACDecision(False, "deny_by_default", ctx.role, target.scope, action)
def visible_modules(self, ctx: PortalContext) -> list[dict[str, Any]]:
modules = ROLE_MODULES.get(ctx.role, ())
visible: list[dict[str, Any]] = []
for module in modules:
action, scope = module.permission.split(":", 1)
resource = PortalResource(scope=scope, owner_user_id=ctx.user_id, group_id=ctx.group_id)
if self.authorize(ctx, action, resource).allowed:
visible.append(module.to_dict())
return visible
def _permission_decision(self, role: str, action: str, scope: str) -> RBACDecision:
permissions = self.permissions.get(role, set())
if "portal.*:*" in permissions or f"{action}:{scope}" in permissions:
return RBACDecision(True, "permission_match", role, scope, action)
return RBACDecision(False, "permission_missing", role, scope, action)
class PortalRepository:
"""Small deterministic repository used for domain tests and route integration.
Production deployments can replace this repository with a SQL-backed adapter while
preserving the service contract.
"""
def __init__(self) -> None:
self.approvals: dict[str, ApprovalRequest] = {}
self.alerts: dict[str, AlertRecord] = {}
self.audit_events: list[AuditEvent] = []
def create_approval(self, ctx: PortalContext, request_type: str, payload: dict[str, Any] | None = None, target: str | None = None) -> ApprovalRequest:
approval = ApprovalRequest(
id=f"apr_{uuid4().hex[:12]}",
request_type=request_type,
requester_id=ctx.user_id,
scope=ctx.natural_scope(),
group_id=ctx.group_id,
target=target,
payload=payload or {},
)
self.approvals[approval.id] = approval
self.append_audit(ctx, "portal.approvals.create", approval.scope, approval.group_id, "approval", approval.id)
return approval
def decide_approval(self, ctx: PortalContext, approval_id: str, decision: str, reason: str | None = None) -> ApprovalRequest:
if decision not in {"approved", "rejected"}:
raise ValueError("decision must be approved or rejected")
approval = self.approvals[approval_id]
approval.state = decision
approval.decided_by = ctx.user_id
approval.decision_reason = reason
approval.updated_at = utc_now()
self.append_audit(ctx, f"portal.approvals.{decision}", approval.scope, approval.group_id, "approval", approval.id)
return approval
def create_alert(self, ctx: PortalContext, level: str, message: str, scope: str | None = None, source: str = "portal") -> AlertRecord:
if level not in {"info", "warning", "high", "critical"}:
raise ValueError("invalid alert level")
alert_scope = scope or ctx.natural_scope()
alert = AlertRecord(
id=f"alt_{uuid4().hex[:12]}",
level=level,
message=message,
scope=alert_scope,
group_id=ctx.group_id if alert_scope == SCOPE_GROUP else None,
owner_user_id=ctx.user_id if alert_scope == SCOPE_SELF else None,
source=source,
)
self.alerts[alert.id] = alert
self.append_audit(ctx, "portal.alerts.create", alert.scope, alert.group_id, "alert", alert.id)
return alert
def resolve_alert(self, ctx: PortalContext, alert_id: str) -> AlertRecord:
alert = self.alerts[alert_id]
alert.state = "resolved"
alert.updated_at = utc_now()
self.append_audit(ctx, "portal.alerts.resolve", alert.scope, alert.group_id, "alert", alert.id)
return alert
def append_audit(
self,
ctx: PortalContext,
action: str,
scope: str | None = None,
group_id: str | None = None,
resource_type: str = "portal",
resource_id: str | None = None,
outcome: str = "success",
detail: dict[str, Any] | None = None,
) -> AuditEvent:
event = AuditEvent(
id=f"aud_{uuid4().hex[:12]}",
actor_id=ctx.user_id,
action=action,
scope=scope or ctx.natural_scope(),
group_id=group_id or ctx.group_id,
resource_type=resource_type,
resource_id=resource_id,
outcome=outcome,
detail=detail or {},
)
self.audit_events.append(event)
return event
def list_approvals(self, ctx: PortalContext) -> list[ApprovalRequest]:
return [a for a in self.approvals.values() if self._visible(ctx, a.scope, a.requester_id, a.group_id)]
def list_alerts(self, ctx: PortalContext) -> list[AlertRecord]:
return [a for a in self.alerts.values() if self._visible(ctx, a.scope, a.owner_user_id, a.group_id)]
def list_audit(self, ctx: PortalContext) -> list[AuditEvent]:
return [a for a in self.audit_events if self._visible(ctx, a.scope, a.actor_id, a.group_id)]
def _visible(self, ctx: PortalContext, scope: str, owner_user_id: str | None, group_id: str | None) -> bool:
if ctx.role == ROLE_OWNER:
return True
if scope == SCOPE_SELF:
return owner_user_id in (None, ctx.user_id)
if ctx.role == ROLE_GROUP_ADMIN and scope == SCOPE_GROUP:
return bool(ctx.group_id and group_id == ctx.group_id)
return False
class PortalDashboardService:
def __init__(self, repo: PortalRepository | None = None, rbac: PortalRBACService | None = None):
self.repo = repo or PortalRepository()
self.rbac = rbac or PortalRBACService()
def dashboard(self, ctx: PortalContext) -> dict[str, Any]:
resource = PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id)
decision = self.rbac.authorize(ctx, "portal.dashboard.read", resource)
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict(), "modules": []}
return {
"status": "ok",
"role": ctx.role,
"scope": ctx.natural_scope(),
"user_id": ctx.user_id,
"group_id": ctx.group_id,
"modules": self.rbac.visible_modules(ctx),
"counters": {
"approvals": len(self.repo.list_approvals(ctx)),
"alerts": len([a for a in self.repo.list_alerts(ctx) if a.state == "open"]),
"audit_events": len(self.repo.list_audit(ctx)),
},
}
def owner_cockpit(self, ctx: PortalContext, runtime: dict[str, Any] | None = None) -> dict[str, Any]:
decision = self.rbac.authorize(ctx, "portal.system.read", PortalResource(scope=SCOPE_GLOBAL))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
runtime = runtime or {}
return {
"status": "ok",
"scope": SCOPE_GLOBAL,
"runtime": {
"service": runtime.get("service", "unknown"),
"status": runtime.get("status", "unknown"),
"workspace_branch": runtime.get("workspace_branch"),
},
"gitops": {
"mode": runtime.get("mode", "git_patch_only"),
"sync_status": runtime.get("sync_status", "unknown"),
},
"security": {
"human_shortlinks": "login_required",
"secrets_in_git": False,
"direct_main_push": False,
},
"models": {
"custom_llm": bool(runtime.get("custom_llm", True)),
"user_priority": bool(runtime.get("user_model_priority", True)),
},
"ops": {
"open_alerts": len([a for a in self.repo.alerts.values() if a.state == "open"]),
"pending_approvals": len([a for a in self.repo.approvals.values() if a.state == "pending"]),
"audit_events": len(self.repo.audit_events),
},
}
class PortalOpsService:
def __init__(self, repo: PortalRepository | None = None, rbac: PortalRBACService | None = None):
self.repo = repo or PortalRepository()
self.rbac = rbac or PortalRBACService()
self.dashboard_service = PortalDashboardService(self.repo, self.rbac)
def check_rbac(self, ctx: PortalContext, action: str, resource: PortalResource | None = None) -> dict[str, Any]:
return self.rbac.authorize(ctx, action, resource).to_dict()
def dashboard(self, ctx: PortalContext) -> dict[str, Any]:
return self.dashboard_service.dashboard(ctx)
def owner_cockpit(self, ctx: PortalContext, runtime: dict[str, Any] | None = None) -> dict[str, Any]:
return self.dashboard_service.owner_cockpit(ctx, runtime)
def create_approval(self, ctx: PortalContext, request_type: str, payload: dict[str, Any] | None = None, target: str | None = None) -> dict[str, Any]:
decision = self.rbac.authorize(ctx, "portal.approvals.create", PortalResource(scope=ctx.natural_scope(), owner_user_id=ctx.user_id, group_id=ctx.group_id))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
approval = self.repo.create_approval(ctx, request_type, payload, target)
return {"status": "ok", "approval": approval.to_dict()}
def decide_approval(self, ctx: PortalContext, approval_id: str, decision_value: str, reason: str | None = None) -> dict[str, Any]:
approval = self.repo.approvals[approval_id]
decision = self.rbac.authorize(ctx, "portal.approvals.decide", PortalResource(scope=approval.scope, group_id=approval.group_id, owner_user_id=approval.requester_id))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
updated = self.repo.decide_approval(ctx, approval_id, decision_value, reason)
return {"status": "ok", "approval": updated.to_dict()}
def create_alert(self, ctx: PortalContext, level: str, message: str, scope: str | None = None, source: str = "portal") -> dict[str, Any]:
requested_scope = scope or ctx.natural_scope()
decision = self.rbac.authorize(ctx, "portal.alerts.read", PortalResource(scope=requested_scope, group_id=ctx.group_id, owner_user_id=ctx.user_id))
if not decision.allowed:
return {"status": "forbidden", "decision": decision.to_dict()}
alert = self.repo.create_alert(ctx, level, message, requested_scope, source)
return {"status": "ok", "alert": alert.to_dict()}
def list_alerts(self, ctx: PortalContext) -> dict[str, Any]:
return {"status": "ok", "alerts": [a.to_dict() for a in self.repo.list_alerts(ctx)]}
def list_approvals(self, ctx: PortalContext) -> dict[str, Any]:
return {"status": "ok", "approvals": [a.to_dict() for a in self.repo.list_approvals(ctx)]}
def list_audit(self, ctx: PortalContext) -> dict[str, Any]:
return {"status": "ok", "audit": [a.to_dict() for a in self.repo.list_audit(ctx)]}
def run_self_loop(self) -> dict[str, Any]:
owner = PortalContext("owner", ROLE_OWNER)
group_admin = PortalContext("admin", ROLE_GROUP_ADMIN, "g1")
user = PortalContext("user", ROLE_USER, "g1")
self.create_alert(owner, "info", "self-loop-started", SCOPE_GLOBAL, "self_loop")
approval = self.create_approval(user, "model_priority_change", {"model": "default"}, "model_priority")
approval_id = approval["approval"]["id"]
decision = self.decide_approval(group_admin, approval_id, "approved", "self-loop group approval")
return {
"status": "ok",
"checks": {
"user_dashboard": self.dashboard(user)["status"] == "ok",
"group_admin_dashboard": self.dashboard(group_admin)["status"] == "ok",
"owner_cockpit": self.owner_cockpit(owner)["status"] == "ok",
"approval_decision": decision["status"] == "ok",
"audit_written": len(self.repo.audit_events) >= 3,
},
}
def build_context(user_id: str, role: str = ROLE_USER, group_id: str | None = None, display_name: str | None = None) -> PortalContext:
return PortalContext(user_id=user_id, role=role, group_id=group_id, display_name=display_name)
__all__ = [
"ROLE_USER",
"ROLE_GROUP_ADMIN",
"ROLE_OWNER",
"ROLES",
"SCOPE_SELF",
"SCOPE_GROUP",
"SCOPE_GLOBAL",
"PortalContext",
"PortalResource",
"RBACDecision",
"ApprovalRequest",
"AlertRecord",
"AuditEvent",
"PortalRBACService",
"PortalRepository",
"PortalDashboardService",
"PortalOpsService",
"build_context",
]
+239
View File
@@ -0,0 +1,239 @@
"""HTTP routes for the three-role user portal.
The router is FastAPI-compatible but keeps handlers as plain functions so tests and
agent self-checks can exercise the contract without booting an ASGI server.
"""
from __future__ import annotations
from typing import Any
try: # pragma: no cover - optional runtime dependency
from fastapi import APIRouter, Header, HTTPException
except Exception: # pragma: no cover - test/runtime fallback
APIRouter = None
Header = None
HTTPException = RuntimeError
from .portal_rbac_ops import (
ROLE_GROUP_ADMIN,
ROLE_OWNER,
ROLE_USER,
SCOPE_GLOBAL,
PortalOpsService,
PortalResource,
build_context,
)
portal_ops_service = PortalOpsService()
def context_from_headers(
user_id: str | None = None,
role: str | None = None,
group_id: str | None = None,
display_name: str | None = None,
):
return build_context(
user_id=user_id or "anonymous",
role=role or ROLE_USER,
group_id=group_id,
display_name=display_name,
)
def get_portal_dashboard(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.dashboard(ctx)
def get_owner_system_cockpit(user_id: str = "owner", runtime: dict[str, Any] | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=ROLE_OWNER)
return portal_ops_service.owner_cockpit(ctx, runtime)
def check_portal_rbac(
role: str,
action: str,
user_id: str = "anonymous",
group_id: str | None = None,
scope: str | None = None,
owner_user_id: str | None = None,
resource_group_id: str | None = None,
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
resource = PortalResource(
scope=scope or ctx.natural_scope(),
owner_user_id=owner_user_id or user_id,
group_id=resource_group_id or group_id,
)
return portal_ops_service.check_rbac(ctx, action, resource)
def create_portal_approval(
role: str,
request_type: str,
user_id: str = "anonymous",
group_id: str | None = None,
payload: dict[str, Any] | None = None,
target: str | None = None,
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.create_approval(ctx, request_type, payload, target)
def decide_portal_approval(
approval_id: str,
decision: str,
role: str,
user_id: str,
group_id: str | None = None,
reason: str | None = None,
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.decide_approval(ctx, approval_id, decision, reason)
def list_portal_alerts(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.list_alerts(ctx)
def create_portal_alert(
role: str,
level: str,
message: str,
user_id: str = "anonymous",
group_id: str | None = None,
scope: str | None = None,
source: str = "portal",
) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.create_alert(ctx, level, message, scope, source)
def list_portal_approvals(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.list_approvals(ctx)
def list_portal_audit_events(role: str, user_id: str = "anonymous", group_id: str | None = None) -> dict[str, Any]:
ctx = context_from_headers(user_id=user_id, role=role, group_id=group_id)
return portal_ops_service.list_audit(ctx)
def run_portal_self_loop() -> dict[str, Any]:
return portal_ops_service.run_self_loop()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/portal", tags=["portal"])
@router.get("/dashboard", operation_id="getPortalDashboard")
def dashboard(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return get_portal_dashboard(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.post("/rbac/check", operation_id="checkPortalRBAC")
def rbac_check(payload: dict[str, Any]):
return check_portal_rbac(
role=payload.get("role", ROLE_USER),
action=payload["action"],
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
scope=payload.get("scope"),
owner_user_id=payload.get("owner_user_id"),
resource_group_id=payload.get("resource_group_id"),
)
@router.post("/approvals", operation_id="createPortalApproval")
def create_approval(payload: dict[str, Any]):
return create_portal_approval(
role=payload.get("role", ROLE_USER),
request_type=payload["request_type"],
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
payload=payload.get("payload"),
target=payload.get("target"),
)
@router.post("/approvals/{approval_id}/decision", operation_id="decidePortalApproval")
def decide_approval(approval_id: str, payload: dict[str, Any]):
return decide_portal_approval(
approval_id=approval_id,
decision=payload["decision"],
role=payload.get("role", ROLE_GROUP_ADMIN),
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
reason=payload.get("reason"),
)
@router.get("/approvals", operation_id="listPortalApprovals")
def approvals(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return list_portal_approvals(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.get("/alerts", operation_id="listPortalAlerts")
def alerts(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return list_portal_alerts(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.post("/alerts", operation_id="createPortalAlert")
def create_alert(payload: dict[str, Any]):
return create_portal_alert(
role=payload.get("role", ROLE_USER),
level=payload["level"],
message=payload["message"],
user_id=payload.get("user_id", "anonymous"),
group_id=payload.get("group_id"),
scope=payload.get("scope"),
source=payload.get("source", "portal"),
)
@router.get("/audit", operation_id="listPortalAuditEvents")
def audit(
x_user_id: str | None = Header(default=None),
x_portal_role: str | None = Header(default=None),
x_group_id: str | None = Header(default=None),
):
return list_portal_audit_events(x_portal_role or ROLE_USER, x_user_id or "anonymous", x_group_id)
@router.get("/owner/system", operation_id="getOwnerSystemCockpit")
def owner_system(x_user_id: str | None = Header(default=None)):
return get_owner_system_cockpit(x_user_id or "owner")
@router.post("/self-loop", operation_id="runPortalSelfLoop")
def self_loop():
return run_portal_self_loop()
return router
router = build_router()
__all__ = [
"router",
"build_router",
"context_from_headers",
"get_portal_dashboard",
"get_owner_system_cockpit",
"check_portal_rbac",
"create_portal_approval",
"decide_portal_approval",
"list_portal_alerts",
"list_portal_approvals",
"list_portal_audit_events",
"run_portal_self_loop",
]
@@ -0,0 +1,69 @@
"""Production acceptance gate for self-loop promotion.
The gate verifies runtime, GitOps policy, portal readiness, and security rules
before any promotion request can proceed.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GateCheck:
name: str
passed: bool
detail: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ProductionGateResult:
status: str
checks: tuple[GateCheck, ...]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"checks": [c.to_dict() for c in self.checks],
"owner_approval_required": self.owner_approval_required,
"created_at": self.created_at,
}
class ProductionAcceptanceGate:
def evaluate(
self,
runtime: dict[str, Any],
baseline: dict[str, Any],
acceptance: dict[str, Any],
owner_approval_required: bool = True,
) -> ProductionGateResult:
checks = (
GateCheck("runtime_active", runtime.get("status") == "active", "runtime status"),
GateCheck("git_patch_only", baseline.get("update_mode") == "git_patch_only", "patch-only policy"),
GateCheck("no_direct_main_push", baseline.get("codex_gitops", {}).get("direct_main_push") is False, "main push guard"),
GateCheck("acceptance_passed", acceptance.get("status") in {"pass", "waiting_owner_gate"}, "acceptance pipeline"),
GateCheck("no_secrets", baseline.get("codex_gitops", {}).get("api_keys_in_git") is False, "secret policy"),
)
passed = all(check.passed for check in checks)
if not passed:
status = "blocked"
elif owner_approval_required:
status = "waiting_owner_approval"
else:
status = "ready_for_promotion"
return ProductionGateResult(status, checks, owner_approval_required)
__all__ = ["GateCheck", "ProductionGateResult", "ProductionAcceptanceGate"]
@@ -0,0 +1,59 @@
"""Self-evolution reporting for release and owner review."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class EvolutionReport:
status: str
improvements: list[str]
risks: list[str]
metrics: dict[str, Any]
recommendations: list[str]
created_at: str = utc_now()
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfEvolutionReporter:
def generate(
self,
supervisor: dict[str, Any],
failure_memory: dict[str, Any],
adaptive_policy: dict[str, Any],
) -> EvolutionReport:
failures = failure_memory.get("top_failures", [])
risks = []
if failures:
risks.append("recurring failures require owner-visible review")
if adaptive_policy.get("automation_level") == "analysis_only":
risks.append("automation reduced due to recurrence")
return EvolutionReport(
status="ready_for_review",
improvements=[
"self-loop supervisor enabled",
"failure memory accumulated",
"adaptive policy active",
],
risks=risks,
metrics={
"failure_records": len(failure_memory.get("records", {})),
"supervisor_cycles": supervisor.get("state", {}).get("cycles", 0),
},
recommendations=[
"continue low-risk autonomous patches",
"review owner-gated promotion requests",
],
)
__all__ = ["EvolutionReport", "SelfEvolutionReporter"]
@@ -0,0 +1,89 @@
"""Event hooks that trigger autonomous self-loop jobs."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .adaptive_policy import AdaptivePolicy
from .failure_memory import FailureMemory
from .self_loop_scheduler import SelfLoopScheduler
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SelfLoopEvent:
source: str
event_type: str
goal: str
runtime: dict[str, Any] = field(default_factory=dict)
baseline: dict[str, Any] = field(default_factory=dict)
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopEventHook:
SUPPORTED_EVENTS = {
"runtime_unhealthy",
"gitops_drift",
"test_failed",
"portal_gap_detected",
"acceptance_failed",
}
def __init__(self, scheduler: SelfLoopScheduler | None = None, memory: FailureMemory | None = None, policy: AdaptivePolicy | None = None):
self.scheduler = scheduler or SelfLoopScheduler()
self.memory = memory or FailureMemory()
self.policy = policy or AdaptivePolicy()
def parse(self, payload: dict[str, Any]) -> SelfLoopEvent:
return SelfLoopEvent(
source=str(payload.get("source", "unknown")),
event_type=str(payload.get("event_type", "unknown")),
goal=str(payload.get("goal", "investigate self-loop event")),
runtime=dict(payload.get("runtime", {})),
baseline=dict(payload.get("baseline", {})),
detail=str(payload.get("detail", "")),
)
def handle(self, payload: dict[str, Any]) -> dict[str, Any]:
event = self.parse(payload)
if event.event_type not in self.SUPPORTED_EVENTS:
return {
"status": "ignored",
"reason": "unsupported_event",
"event": event.to_dict(),
}
if event.event_type in {"test_failed", "acceptance_failed", "runtime_unhealthy"}:
self.memory.record_failure(
component=event.source,
reason=event.event_type,
detail=event.detail,
severity="high",
remediation_hint="run self-loop diagnosis and generate patch plan",
)
decision = self.policy.evaluate(self.memory, event.runtime, event.baseline, event.goal)
options = self.policy.apply_to_scheduler(decision)
job = self.scheduler.enqueue(
goal=event.goal,
runtime=event.runtime,
baseline=event.baseline,
max_attempts=options["max_attempts"],
)
return {
"status": "queued",
"event": event.to_dict(),
"policy": decision.to_dict(),
"job": job.to_dict(),
}
__all__ = ["SelfLoopEvent", "SelfLoopEventHook"]
+222
View File
@@ -0,0 +1,222 @@
"""Autonomous self-loop executor.
This layer turns probe/plan reports into safe execution outcomes. It does not
bypass owner governance: high-risk or restricted actions are blocked at the owner
approval gate and emitted as explicit pending work.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Callable
from .autonomous_self_loop import AutonomousSelfLoop, RemediationStep, SelfLoopReport
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ExecutionResult:
step_id: str
action: str
status: str
reason: str
risk: str
requires_owner_approval: bool
output: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class TestResult:
name: str
status: str
detail: str = ""
def passed(self) -> bool:
return self.status == "pass"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass
class ExecutionReport:
status: str
report_id: str
actions: list[ExecutionResult]
tests: list[TestResult]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"report_id": self.report_id,
"actions": [a.to_dict() for a in self.actions],
"tests": [t.to_dict() for t in self.tests],
"owner_approval_required": self.owner_approval_required,
"created_at": self.created_at,
}
class SelfLoopTestRunner:
"""Deterministic self-loop checks that avoid shell execution and secrets."""
def run(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> list[TestResult]:
return [
self._runtime_active(runtime),
self._patch_only_policy(baseline),
self._owner_gate_policy(baseline),
self._portal_domain_smoke(),
]
def _runtime_active(self, runtime: dict[str, Any]) -> TestResult:
if runtime.get("status") == "active":
return TestResult("runtime_active", "pass", "runtime reports active")
return TestResult("runtime_active", "fail", "runtime is not active")
def _patch_only_policy(self, baseline: dict[str, Any]) -> TestResult:
if baseline.get("update_mode") == "git_patch_only":
return TestResult("git_patch_only", "pass", "patch-only updates enforced")
return TestResult("git_patch_only", "fail", "update mode is not git_patch_only")
def _owner_gate_policy(self, baseline: dict[str, Any]) -> TestResult:
codex_gitops = baseline.get("codex_gitops", {})
if codex_gitops.get("direct_main_push") is False:
return TestResult("owner_gate", "pass", "direct main push disabled")
return TestResult("owner_gate", "fail", "direct main push must remain disabled")
def _portal_domain_smoke(self) -> TestResult:
try:
from .portal_rbac_ops import ROLE_OWNER, PortalOpsService, build_context
service = PortalOpsService()
result = service.owner_cockpit(build_context("owner", ROLE_OWNER))
if result.get("status") == "ok":
return TestResult("portal_domain_smoke", "pass", "owner cockpit domain service responds")
return TestResult("portal_domain_smoke", "fail", "owner cockpit returned non-ok")
except Exception as exc: # pragma: no cover - defensive smoke output
return TestResult("portal_domain_smoke", "fail", f"portal smoke failed: {type(exc).__name__}")
class SelfLoopAgentExecutor:
"""Executes safe self-loop steps and gates restricted ones."""
SAFE_ACTIONS = {
"health_probe",
"test_execution",
"failure_classification",
"patch_plan_generation",
"report_generation",
"create-low-risk-patch",
}
RESTRICTED_ACTIONS = {
"request-runtime-recovery",
"commit-policy-patch",
"commit-security-policy",
"commit-runtime-config",
"commit-portal-config",
"production-promotion",
"permission-escalation",
"secret-rotation",
}
def __init__(self, loop: AutonomousSelfLoop | None = None, test_runner: SelfLoopTestRunner | None = None):
self.loop = loop or AutonomousSelfLoop(owner_approval_required=True)
self.test_runner = test_runner or SelfLoopTestRunner()
self.handlers: dict[str, Callable[[RemediationStep], dict[str, Any]]] = {
"create-low-risk-patch": self._create_low_risk_patch_manifest,
"patch_plan_generation": self._patch_plan_manifest,
"report_generation": self._report_manifest,
}
def execute_step(self, step: RemediationStep) -> ExecutionResult:
if step.requires_owner_approval or step.action in self.RESTRICTED_ACTIONS:
return ExecutionResult(
step.id,
step.action,
"blocked_owner_gate",
"restricted action requires owner approval",
step.risk,
True,
{"title": step.title},
)
if step.risk not in {"low"}:
return ExecutionResult(
step.id,
step.action,
"blocked_risk_gate",
"only low-risk autonomous actions are allowed",
step.risk,
True,
{"title": step.title},
)
handler = self.handlers.get(step.action, self._safe_noop_manifest)
return ExecutionResult(
step.id,
step.action,
"executed",
"safe autonomous action executed",
step.risk,
False,
handler(step),
)
def execute_report(self, report: SelfLoopReport, runtime: dict[str, Any], baseline: dict[str, Any]) -> ExecutionReport:
actions = [self.execute_step(step) for step in report.remediation]
tests = self.test_runner.run(runtime, baseline)
owner_approval_required = self.loop.owner_approval_required or any(a.requires_owner_approval for a in actions)
failed_tests = [t for t in tests if not t.passed()]
if failed_tests:
status = "failed_tests"
elif owner_approval_required:
status = "waiting_owner_gate"
else:
status = "completed"
return ExecutionReport(status, report.id, actions, tests, owner_approval_required)
def run_once(self, runtime: dict[str, Any], baseline: dict[str, Any]) -> dict[str, Any]:
report = self.loop.evaluate(runtime, baseline)
execution = self.execute_report(report, runtime, baseline)
acceptance = self.loop.acceptance(report)
owner_approval_required = execution.owner_approval_required or acceptance["owner_approval_required"]
return {
"status": execution.status,
"can_unattended_continue": execution.status == "completed" and not owner_approval_required,
"owner_approval_required": owner_approval_required,
"plan": report.to_dict(),
"execution": execution.to_dict(),
}
def _create_low_risk_patch_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {
"patch_intent": step.title,
"branch_policy": "patch-only",
"direct_main_push": False,
"secrets_in_git": False,
}
def _patch_plan_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {"plan": step.title, "action": step.action}
def _report_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {"report": step.title, "generated": True}
def _safe_noop_manifest(self, step: RemediationStep) -> dict[str, Any]:
return {"noop": True, "title": step.title}
__all__ = [
"ExecutionResult",
"TestResult",
"ExecutionReport",
"SelfLoopTestRunner",
"SelfLoopAgentExecutor",
]
@@ -0,0 +1,58 @@
"""Adapter manifest for controlled GitOps Actions execution.
The adapter produces an action plan consumed by the GitOps bridge. It does not
embed credentials and does not perform direct repository writes.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_patch_executor import PatchBundle
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GitOpsActionPlan:
action: str
branch: str
operations: tuple[dict[str, Any], ...]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopGitOpsAdapter:
def build_action_plan(self, bundle: PatchBundle) -> GitOpsActionPlan:
operations = tuple(
{
"operation": "commitFile",
"path": patch_file.path,
"checksum": patch_file.checksum(),
}
for patch_file in bundle.files
)
return GitOpsActionPlan(
action="gitops_patch_apply",
branch=bundle.branch,
operations=operations,
owner_approval_required=bundle.owner_approval_required,
)
def validate_action_plan(self, plan: GitOpsActionPlan) -> dict[str, Any]:
return {
"valid": plan.branch.startswith("patch/") and plan.action == "gitops_patch_apply",
"branch": plan.branch,
"owner_approval_required": plan.owner_approval_required,
"operations": len(plan.operations),
}
__all__ = ["GitOpsActionPlan", "SelfLoopGitOpsAdapter"]
@@ -0,0 +1,142 @@
"""Multi-agent self-loop orchestration.
Coordinates planning, implementation proposals, and validation feedback from
Hermes/OpenClaw/CodexBridge-style agents without storing credentials or bypassing
GitOps governance.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_executor import SelfLoopAgentExecutor
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class AgentProfile:
name: str
role: str
weight: int
capabilities: tuple[str, ...]
@dataclass(frozen=True)
class AgentTask:
task_id: str
goal: str
task_type: str
risk: str = "low"
payload: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class AgentResponse:
agent: str
status: str
proposal: dict[str, Any]
confidence: float
@dataclass(frozen=True)
class AgentVote:
agent: str
approve: bool
weight: int
reason: str
@dataclass
class PatchManifest:
branch_policy: str
files: list[str]
tests: list[str]
approvals_required: bool
generated_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopAgentOrchestrator:
AGENTS = (
AgentProfile("Hermes", "planner", 2, ("analysis", "planning", "risk")),
AgentProfile("OpenClaw", "implementation", 2, ("code", "patch", "test")),
AgentProfile("CodexBridge", "validation", 2, ("gitops", "validation", "acceptance")),
)
def __init__(self, executor: SelfLoopAgentExecutor | None = None):
self.executor = executor or SelfLoopAgentExecutor()
def dispatch(self, task: AgentTask) -> list[AgentResponse]:
responses = []
for agent in self.AGENTS:
if agent.role == "planner":
proposal = {"risk": task.risk, "plan": "classify issue and produce remediation steps"}
elif agent.role == "implementation":
proposal = {"patch": True, "branch_policy": "patch-only", "goal": task.goal}
else:
proposal = {"validation": ["tests", "gitops_policy", "acceptance_report"]}
responses.append(AgentResponse(agent.name, "ready", proposal, 0.9))
return responses
def vote(self, responses: list[AgentResponse]) -> dict[str, Any]:
votes = []
total = 0
approved = 0
for response in responses:
profile = next(a for a in self.AGENTS if a.name == response.agent)
approve = response.status == "ready"
votes.append(AgentVote(response.agent, approve, profile.weight, "ready" if approve else "rejected"))
total += profile.weight
approved += profile.weight if approve else 0
return {
"approved": approved >= total / 2,
"approval_weight": approved,
"total_weight": total,
"votes": [asdict(v) for v in votes],
}
def create_patch_manifest(self, task: AgentTask, requires_approval: bool) -> PatchManifest:
return PatchManifest(
branch_policy="patch-only",
files=[
"active-runtime/app",
"openapi",
"tests",
],
tests=[
"self_loop_executor",
"portal_self_loop",
"acceptance_pipeline",
],
approvals_required=requires_approval or task.risk != "low",
)
def run_cycle(self, runtime: dict[str, Any], baseline: dict[str, Any], goal: str) -> dict[str, Any]:
task = AgentTask("self-loop-cycle", goal, "remediation")
executor_result = self.executor.run_once(runtime, baseline)
responses = self.dispatch(task)
decision = self.vote(responses)
manifest = self.create_patch_manifest(task, executor_result["owner_approval_required"])
return {
"status": "ready_for_patch" if decision["approved"] else "blocked_agent_vote",
"executor": executor_result,
"agents": [asdict(r) for r in responses],
"vote": decision,
"patch_manifest": manifest.to_dict(),
}
__all__ = [
"AgentProfile",
"AgentTask",
"AgentResponse",
"SelfLoopAgentOrchestrator",
"PatchManifest",
]
@@ -0,0 +1,284 @@
"""Self-loop patch executor and acceptance pipeline.
This module converts multi-agent self-loop decisions into GitOps-safe patch
bundles, validates policy, runs deterministic acceptance checks, and prepares a
promotion request. It intentionally does not push main, rotate secrets, or bypass
owner approval.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from hashlib import sha256
from typing import Any
from .self_loop_orchestrator import SelfLoopAgentOrchestrator
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def stable_id(*parts: str) -> str:
digest = sha256("::".join(parts).encode("utf-8")).hexdigest()[:12]
return f"slp_{digest}"
@dataclass(frozen=True)
class PatchFile:
path: str
content: str
purpose: str
def checksum(self) -> str:
return sha256(self.content.encode("utf-8")).hexdigest()
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["sha256"] = self.checksum()
return data
@dataclass(frozen=True)
class PatchBundle:
id: str
branch: str
risk: str
goal: str
files: tuple[PatchFile, ...]
owner_approval_required: bool
direct_main_push: bool = False
secrets_in_git: bool = False
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"branch": self.branch,
"risk": self.risk,
"goal": self.goal,
"files": [f.to_dict() for f in self.files],
"owner_approval_required": self.owner_approval_required,
"direct_main_push": self.direct_main_push,
"secrets_in_git": self.secrets_in_git,
"created_at": self.created_at,
}
@dataclass(frozen=True)
class AcceptanceCheck:
name: str
status: str
detail: str
required: bool = True
def passed(self) -> bool:
return self.status == "pass" or not self.required
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceReport:
status: str
patch_id: str
checks: tuple[AcceptanceCheck, ...]
owner_approval_required: bool
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"patch_id": self.patch_id,
"checks": [c.to_dict() for c in self.checks],
"owner_approval_required": self.owner_approval_required,
"created_at": self.created_at,
}
@dataclass(frozen=True)
class PromotionRequest:
status: str
patch_id: str
branch: str
reason: str
owner_approval_required: bool
acceptance: AcceptanceReport
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"patch_id": self.patch_id,
"branch": self.branch,
"reason": self.reason,
"owner_approval_required": self.owner_approval_required,
"acceptance": self.acceptance.to_dict(),
}
class PatchPolicyValidator:
"""Validates GitOps safety constraints before any patch is promoted."""
FORBIDDEN_PATH_PREFIXES = (
".git/",
"secrets/",
"private/",
)
SECRET_MARKERS = (
"BEGIN PRIVATE KEY",
"api_key=",
"password=",
"token=",
"secret=",
)
def validate(self, bundle: PatchBundle) -> tuple[AcceptanceCheck, ...]:
checks: list[AcceptanceCheck] = []
checks.append(self._patch_only(bundle))
checks.append(self._no_direct_main_push(bundle))
checks.append(self._no_secret_files(bundle))
checks.append(self._file_paths_safe(bundle))
checks.append(self._content_safe(bundle))
return tuple(checks)
def _patch_only(self, bundle: PatchBundle) -> AcceptanceCheck:
if bundle.branch.startswith("patch/"):
return AcceptanceCheck("patch_branch_policy", "pass", "branch is patch-scoped")
return AcceptanceCheck("patch_branch_policy", "fail", "branch must start with patch/")
def _no_direct_main_push(self, bundle: PatchBundle) -> AcceptanceCheck:
if bundle.direct_main_push is False:
return AcceptanceCheck("no_direct_main_push", "pass", "direct main push disabled")
return AcceptanceCheck("no_direct_main_push", "fail", "direct main push is not allowed")
def _no_secret_files(self, bundle: PatchBundle) -> AcceptanceCheck:
if bundle.secrets_in_git is False:
return AcceptanceCheck("no_secrets_in_git", "pass", "bundle declares no secrets")
return AcceptanceCheck("no_secrets_in_git", "fail", "secrets must not be stored in Git")
def _file_paths_safe(self, bundle: PatchBundle) -> AcceptanceCheck:
unsafe = [f.path for f in bundle.files if f.path.startswith(self.FORBIDDEN_PATH_PREFIXES)]
if not unsafe:
return AcceptanceCheck("safe_file_paths", "pass", "all file paths are allowed")
return AcceptanceCheck("safe_file_paths", "fail", f"unsafe paths: {unsafe}")
def _content_safe(self, bundle: PatchBundle) -> AcceptanceCheck:
flagged: list[str] = []
for patch_file in bundle.files:
content_lower = patch_file.content.lower()
for marker in self.SECRET_MARKERS:
if marker.lower() in content_lower:
flagged.append(patch_file.path)
break
if not flagged:
return AcceptanceCheck("safe_file_content", "pass", "no secret markers detected")
return AcceptanceCheck("safe_file_content", "fail", f"secret-like markers detected: {flagged}")
class AcceptancePipeline:
def __init__(self, validator: PatchPolicyValidator | None = None):
self.validator = validator or PatchPolicyValidator()
def run(self, bundle: PatchBundle, executor_result: dict[str, Any]) -> AcceptanceReport:
checks = list(self.validator.validate(bundle))
checks.extend(self._executor_checks(executor_result))
status = "pass" if all(c.passed() for c in checks) else "fail"
if status == "pass" and bundle.owner_approval_required:
status = "waiting_owner_gate"
return AcceptanceReport(status, bundle.id, tuple(checks), bundle.owner_approval_required)
def _executor_checks(self, executor_result: dict[str, Any]) -> tuple[AcceptanceCheck, ...]:
execution = executor_result.get("execution", {})
tests = execution.get("tests", [])
checks: list[AcceptanceCheck] = []
for test in tests:
checks.append(AcceptanceCheck(
name=f"executor_{test.get('name', 'unknown')}",
status="pass" if test.get("status") == "pass" else "fail",
detail=test.get("detail", ""),
))
if not checks:
checks.append(AcceptanceCheck("executor_tests_present", "fail", "no executor tests were reported"))
return tuple(checks)
class SelfLoopPatchExecutor:
def __init__(self, orchestrator: SelfLoopAgentOrchestrator | None = None, pipeline: AcceptancePipeline | None = None):
self.orchestrator = orchestrator or SelfLoopAgentOrchestrator()
self.pipeline = pipeline or AcceptancePipeline()
def build_patch_bundle(self, cycle: dict[str, Any], goal: str, branch: str = "patch/autonomous-self-loop") -> PatchBundle:
executor = cycle.get("executor", {})
owner_approval_required = bool(executor.get("owner_approval_required"))
manifest = cycle.get("patch_manifest", {})
risk = "medium" if owner_approval_required else "low"
patch_id = stable_id(branch, goal, cycle.get("status", "unknown"))
files = (
PatchFile(
path=f"patches/{patch_id}.self-loop.json",
purpose="self-loop patch manifest",
content=self._manifest_content(patch_id, goal, cycle, manifest),
),
)
return PatchBundle(
id=patch_id,
branch=branch,
risk=risk,
goal=goal,
files=files,
owner_approval_required=owner_approval_required or bool(manifest.get("approvals_required")),
)
def run_cycle_to_promotion(
self,
runtime: dict[str, Any],
baseline: dict[str, Any],
goal: str,
branch: str = "patch/autonomous-self-loop",
) -> dict[str, Any]:
cycle = self.orchestrator.run_cycle(runtime, baseline, goal)
bundle = self.build_patch_bundle(cycle, goal, branch)
acceptance = self.pipeline.run(bundle, cycle.get("executor", {}))
promotion = self.prepare_promotion(bundle, acceptance)
return {
"status": promotion.status,
"cycle": cycle,
"patch_bundle": bundle.to_dict(),
"acceptance": acceptance.to_dict(),
"promotion": promotion.to_dict(),
}
def prepare_promotion(self, bundle: PatchBundle, acceptance: AcceptanceReport) -> PromotionRequest:
if acceptance.status == "fail":
return PromotionRequest("blocked_failed_acceptance", bundle.id, bundle.branch, "acceptance failed", True, acceptance)
if acceptance.owner_approval_required:
return PromotionRequest("waiting_owner_approval", bundle.id, bundle.branch, "owner approval required", True, acceptance)
return PromotionRequest("ready_for_auto_merge", bundle.id, bundle.branch, "low-risk patch passed acceptance", False, acceptance)
def _manifest_content(self, patch_id: str, goal: str, cycle: dict[str, Any], manifest: dict[str, Any]) -> str:
lines = [
"{",
f' "id": "{patch_id}",',
f' "goal": "{goal}",',
f' "status": "{cycle.get("status", "unknown")}",',
f' "branch_policy": "{manifest.get("branch_policy", "patch-only")}",',
' "direct_main_push": false,',
' "secrets_in_git": false,',
f' "owner_approval_required": {str(bool(manifest.get("approvals_required"))).lower()}',
"}",
]
return "\n".join(lines)
__all__ = [
"PatchFile",
"PatchBundle",
"AcceptanceCheck",
"AcceptanceReport",
"PromotionRequest",
"PatchPolicyValidator",
"AcceptancePipeline",
"SelfLoopPatchExecutor",
]
+140
View File
@@ -0,0 +1,140 @@
"""Runtime API wiring for autonomous self-loop jobs.
This module exposes plain functions and an optional FastAPI router for portal,
webhook, and worker integrations. It keeps all writes behind patch-only GitOps
plans and never stores secrets.
"""
from __future__ import annotations
from typing import Any
try: # pragma: no cover - optional runtime dependency
from fastapi import APIRouter, Header
except Exception: # pragma: no cover - fallback for unit tests
APIRouter = None
Header = None
from .production_acceptance_gate import ProductionAcceptanceGate
from .self_loop_scheduler import SelfLoopScheduler, TriggerType
scheduler = SelfLoopScheduler()
production_gate = ProductionAcceptanceGate()
def enqueue_self_loop_job(
goal: str,
runtime: dict[str, Any],
baseline: dict[str, Any],
trigger: str = "event",
branch: str = "patch/autonomous-self-loop",
) -> dict[str, Any]:
job = scheduler.enqueue(goal, runtime, baseline, TriggerType(trigger), branch)
return {"status": "queued", "job": job.to_dict()}
def run_self_loop_job(job_id: str) -> dict[str, Any]:
return {"status": "ok", "job": scheduler.run_job(job_id)}
def tick_self_loop_jobs(limit: int = 1) -> dict[str, Any]:
return {"status": "ok", "jobs": scheduler.tick(limit), "snapshot": scheduler.snapshot().to_dict()}
def list_self_loop_jobs() -> dict[str, Any]:
return {"status": "ok", "jobs": [job.to_dict() for job in scheduler.jobs.values()]}
def get_self_loop_job(job_id: str) -> dict[str, Any]:
job = scheduler.jobs.get(job_id)
if not job:
return {"status": "not_found", "job_id": job_id}
return {"status": "ok", "job": job.to_dict()}
def run_self_loop_once(
goal: str,
runtime: dict[str, Any],
baseline: dict[str, Any],
branch: str = "patch/autonomous-self-loop",
) -> dict[str, Any]:
job = scheduler.enqueue(goal, runtime, baseline, TriggerType.MANUAL, branch, max_attempts=1)
return run_self_loop_job(job.id)
def evaluate_self_loop_production_gate(
runtime: dict[str, Any],
baseline: dict[str, Any],
acceptance: dict[str, Any],
owner_approval_required: bool = True,
) -> dict[str, Any]:
result = production_gate.evaluate(runtime, baseline, acceptance, owner_approval_required)
return {"status": "ok", "gate": result.to_dict()}
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/self-loop", tags=["self-loop"])
@router.post("/jobs", operation_id="enqueueSelfLoopJob")
def enqueue_job(payload: dict[str, Any]):
return enqueue_self_loop_job(
goal=payload["goal"],
runtime=payload.get("runtime", {}),
baseline=payload.get("baseline", {}),
trigger=payload.get("trigger", "event"),
branch=payload.get("branch", "patch/autonomous-self-loop"),
)
@router.get("/jobs", operation_id="listSelfLoopJobs")
def jobs():
return list_self_loop_jobs()
@router.get("/jobs/{job_id}", operation_id="getSelfLoopJob")
def get_job(job_id: str):
return get_self_loop_job(job_id)
@router.post("/jobs/{job_id}/run", operation_id="runSelfLoopJob")
def run_job(job_id: str):
return run_self_loop_job(job_id)
@router.post("/tick", operation_id="tickSelfLoopJobs")
def tick(payload: dict[str, Any] | None = None):
return tick_self_loop_jobs((payload or {}).get("limit", 1))
@router.post("/run-once", operation_id="runSelfLoopOnce")
def run_once(payload: dict[str, Any]):
return run_self_loop_once(
goal=payload["goal"],
runtime=payload.get("runtime", {}),
baseline=payload.get("baseline", {}),
branch=payload.get("branch", "patch/autonomous-self-loop"),
)
@router.post("/production-gate", operation_id="evaluateSelfLoopProductionGate")
def production_gate_route(payload: dict[str, Any]):
return evaluate_self_loop_production_gate(
runtime=payload.get("runtime", {}),
baseline=payload.get("baseline", {}),
acceptance=payload.get("acceptance", {}),
owner_approval_required=payload.get("owner_approval_required", True),
)
return router
router = build_router()
__all__ = [
"router",
"build_router",
"enqueue_self_loop_job",
"run_self_loop_job",
"tick_self_loop_jobs",
"list_self_loop_jobs",
"get_self_loop_job",
"run_self_loop_once",
"evaluate_self_loop_production_gate",
]
+163
View File
@@ -0,0 +1,163 @@
"""Autonomous self-loop scheduler.
Schedules event-triggered and interval-style self-loop cycles. The scheduler is
framework-neutral and deterministic: production code can invoke tick() from a
cron, worker, or webhook handler without giving the module direct access to
secrets, shell commands, or main-branch writes.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any
from uuid import uuid4
from .self_loop_patch_executor import SelfLoopPatchExecutor
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class TriggerType(str, Enum):
INTERVAL = "interval"
EVENT = "event"
MANUAL = "manual"
class JobStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
WAITING_OWNER = "waiting_owner"
FAILED = "failed"
RETRY_PENDING = "retry_pending"
@dataclass
class SelfLoopJob:
id: str
goal: str
trigger: TriggerType
runtime: dict[str, Any]
baseline: dict[str, Any]
branch: str = "patch/autonomous-self-loop"
status: JobStatus = JobStatus.PENDING
attempts: int = 0
max_attempts: int = 3
last_error: str | None = None
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
result: dict[str, Any] | None = None
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["trigger"] = self.trigger.value
data["status"] = self.status.value
return data
@dataclass(frozen=True)
class SchedulerSnapshot:
pending: int
running: int
completed: int
waiting_owner: int
failed: int
retry_pending: int
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopScheduler:
"""Small in-process scheduler used by runtime workers and tests."""
def __init__(self, executor: SelfLoopPatchExecutor | None = None):
self.executor = executor or SelfLoopPatchExecutor()
self.jobs: dict[str, SelfLoopJob] = {}
def enqueue(
self,
goal: str,
runtime: dict[str, Any],
baseline: dict[str, Any],
trigger: TriggerType | str = TriggerType.EVENT,
branch: str = "patch/autonomous-self-loop",
max_attempts: int = 3,
) -> SelfLoopJob:
trigger_value = trigger if isinstance(trigger, TriggerType) else TriggerType(trigger)
job = SelfLoopJob(
id=f"job_{uuid4().hex[:12]}",
goal=goal,
trigger=trigger_value,
runtime=runtime,
baseline=baseline,
branch=branch,
max_attempts=max_attempts,
)
self.jobs[job.id] = job
return job
def tick(self, limit: int = 1) -> list[dict[str, Any]]:
runnable = [j for j in self.jobs.values() if j.status in {JobStatus.PENDING, JobStatus.RETRY_PENDING}]
results: list[dict[str, Any]] = []
for job in runnable[:limit]:
results.append(self.run_job(job.id))
return results
def run_job(self, job_id: str) -> dict[str, Any]:
job = self.jobs[job_id]
job.status = JobStatus.RUNNING
job.attempts += 1
job.updated_at = utc_now()
try:
result = self.executor.run_cycle_to_promotion(job.runtime, job.baseline, job.goal, job.branch)
job.result = result
promotion_status = result.get("promotion", {}).get("status")
if promotion_status == "waiting_owner_approval":
job.status = JobStatus.WAITING_OWNER
elif promotion_status == "ready_for_auto_merge":
job.status = JobStatus.COMPLETED
elif job.attempts < job.max_attempts:
job.status = JobStatus.RETRY_PENDING
job.last_error = promotion_status or "promotion_not_ready"
else:
job.status = JobStatus.FAILED
job.last_error = promotion_status or "promotion_failed"
except Exception as exc: # pragma: no cover - defensive runtime guard
job.last_error = f"{type(exc).__name__}: {exc}"
job.status = JobStatus.RETRY_PENDING if job.attempts < job.max_attempts else JobStatus.FAILED
job.updated_at = utc_now()
return job.to_dict()
def retry_failed(self) -> list[str]:
retry_ids: list[str] = []
for job in self.jobs.values():
if job.status == JobStatus.FAILED and job.attempts < job.max_attempts:
job.status = JobStatus.RETRY_PENDING
job.updated_at = utc_now()
retry_ids.append(job.id)
return retry_ids
def snapshot(self) -> SchedulerSnapshot:
statuses = [job.status for job in self.jobs.values()]
return SchedulerSnapshot(
pending=statuses.count(JobStatus.PENDING),
running=statuses.count(JobStatus.RUNNING),
completed=statuses.count(JobStatus.COMPLETED),
waiting_owner=statuses.count(JobStatus.WAITING_OWNER),
failed=statuses.count(JobStatus.FAILED),
retry_pending=statuses.count(JobStatus.RETRY_PENDING),
)
__all__ = [
"TriggerType",
"JobStatus",
"SelfLoopJob",
"SchedulerSnapshot",
"SelfLoopScheduler",
]
@@ -0,0 +1,77 @@
"""Long-running supervisor model for autonomous self-loop runtime."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_event_hooks import SelfLoopEventHook
from .failure_memory import FailureMemory
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass
class SupervisorState:
running: bool = False
cycles: int = 0
last_cycle: str | None = None
last_status: str = "idle"
heartbeat: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopSupervisor:
def __init__(self, hook: SelfLoopEventHook | None = None, memory: FailureMemory | None = None):
self.memory = memory or FailureMemory()
self.hook = hook or SelfLoopEventHook(memory=self.memory)
self.state = SupervisorState()
def start(self) -> dict[str, Any]:
self.state.running = True
self.state.last_status = "running"
self.state.heartbeat = utc_now()
return self.status()
def stop(self) -> dict[str, Any]:
self.state.running = False
self.state.last_status = "stopped"
self.state.heartbeat = utc_now()
return self.status()
def heartbeat(self) -> dict[str, Any]:
self.state.heartbeat = utc_now()
return self.status()
def handle_event(self, payload: dict[str, Any]) -> dict[str, Any]:
if not self.state.running:
self.start()
self.state.cycles += 1
self.state.last_cycle = utc_now()
result = self.hook.handle(payload)
self.state.last_status = result.get("status", "unknown")
self.state.heartbeat = utc_now()
return result
def tick(self, events: list[dict[str, Any]]) -> dict[str, Any]:
results = [self.handle_event(event) for event in events]
return {
"status": "ok",
"supervisor": self.status(),
"events": results,
"failure_memory": self.memory.export(),
}
def status(self) -> dict[str, Any]:
return {
"state": self.state.to_dict(),
"failure_memory_size": len(self.memory.records),
}
__all__ = ["SupervisorState", "SelfLoopSupervisor"]
+70
View File
@@ -0,0 +1,70 @@
"""Resident worker model for self-loop supervision.
Provides a daemon-friendly lifecycle without forcing an infinite loop inside the
application. A process manager can call start/run_cycle/stop and persist state.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .self_loop_supervisor import SelfLoopSupervisor
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass
class WorkerState:
running: bool = False
cycles: int = 0
last_error: str | None = None
started_at: str | None = None
heartbeat: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SelfLoopWorker:
def __init__(self, supervisor: SelfLoopSupervisor | None = None):
self.supervisor = supervisor or SelfLoopSupervisor()
self.state = WorkerState()
def start(self) -> dict[str, Any]:
self.state.running = True
self.state.started_at = utc_now()
self.state.heartbeat = utc_now()
self.supervisor.start()
return self.status()
def run_cycle(self, events: list[dict[str, Any]]) -> dict[str, Any]:
if not self.state.running:
self.start()
try:
result = self.supervisor.tick(events)
self.state.cycles += 1
self.state.heartbeat = utc_now()
return result
except Exception as exc:
self.state.last_error = f"{type(exc).__name__}: {exc}"
self.state.heartbeat = utc_now()
raise
def stop(self) -> dict[str, Any]:
self.state.running = False
self.supervisor.stop()
self.state.heartbeat = utc_now()
return self.status()
def status(self) -> dict[str, Any]:
return {
"worker": self.state.to_dict(),
"supervisor": self.supervisor.status(),
}
__all__ = ["WorkerState", "SelfLoopWorker"]
@@ -0,0 +1,43 @@
CREATE TABLE IF NOT EXISTS portal_roles (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
role TEXT NOT NULL,
group_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS portal_approvals (
id TEXT PRIMARY KEY,
request_type TEXT NOT NULL,
requester_id TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
scope TEXT NOT NULL,
group_id TEXT,
payload_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS portal_alerts (
id TEXT PRIMARY KEY,
level TEXT NOT NULL,
message TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'open',
scope TEXT NOT NULL,
group_id TEXT,
owner_user_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS portal_audit_events (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
action TEXT NOT NULL,
scope TEXT NOT NULL,
resource_type TEXT,
resource_id TEXT,
outcome TEXT NOT NULL DEFAULT 'success',
detail_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+29
View File
@@ -0,0 +1,29 @@
# Agent Instruction Center
Version-controlled execution instructions for AIOS agents.
## Supported agents
- codex
- openclaw
- hermes
- windows_codex_app
- custom_gpt_review
## Workflow
1. Create versioned instruction file.
2. Target agent executes instruction.
3. Result is recorded with GitOps audit information.
## Naming
agent-instructions/<agent>/<instruction-name>-v<version>.md
## Rules
- GitOps managed only.
- Patch-first workflow.
- No secrets, tokens, passwords or credentials.
- Production changes require approval.
- Main branch changes require approval.
+34
View File
@@ -0,0 +1,34 @@
# Agent Execution Instruction
Instruction ID:
Version:
Target Agent:
## Objective
## Context
## Execution Steps
## Constraints
- GitOps only
- Patch-first
- No secrets
- No force push
- Production changes require approval
## Validation
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
+1
View File
@@ -0,0 +1 @@
# Placeholder for versioned Codex instructions.
@@ -0,0 +1,65 @@
# Codex Fix Patches Artifact Lifecycle Instruction
Instruction ID: fix-patches-artifact-lifecycle-v1
Version: v1
Target Agent: codex
## Objective
Manage the lifecycle of artifacts in the `patches/` directory — specifically, clean up stale or committed artifacts (`.branch.json`, `nl/*.json`, standalone `.json` patch files) that are no longer needed.
## Background
The ChatGPT Actions API writes several types of artifacts into `patches/`:
1. **Branch metadata** (`patches/patch_*.branch.json`) — created when the `branch()` action writes ahead. These are summary records of created branches.
2. **Patch files** (`patches/*.json`, top level) — created when the `patch()` action saves a JSON patch proposal. Example: `patches/self-evolution-agent-orchestration-policy.json`.
3. **Natural language plan files** (`patches/nl/nl-*.json`) — created when `nl()` action records a natural-language plan. These are transient planning artifacts.
None of these files represent durable long-term state. Once committed, they are snapshots of ephemeral decisions. The `patches/` directory should not grow unboundedly with stale artifacts.
## Preconditions
- GitOps authentication healthy.
- Current working branch should not be diverging from main unnecessarily for this cleanup.
- Run from the workspace root.
## Steps
### Phase 1: Assess Current State
1. List all files under `patches/`.
2. For each file, check its git log (`git log --oneline patches/<file>`) to understand its purpose and age.
3. Identify which files are:
- Stale `.branch.json` files describing branches that have already been merged.
- Stale `nl/` plan files that were experimental, incomplete, or superseded.
- Patch files that are still relevant for the current self-evolution state.
### Phase 2: Clean Up Stale Artifacts
For each stale artifact file:
1. Remove it with `git rm`.
2. Create a single commit message describing the batch cleanup, e.g.:
`chore(patches): remove stale artifact files [nl/*.json|branch.json]`
### Phase 3: Housekeeping (Optional)
If the file `self-evolution-agent-orchestration-policy.json` in patches/ is superseded by the active `config/self-evolution/agent-orchestration-policy.json`, evaluate whether it should also be removed or kept as reference. Document this decision.
## Constraints
- No force push.
- No direct production publish.
- No secret output.
- Preserve audit trail.
- Only remove files that are confirmed stale (do not remove actively referenced patches that the runtime still uses to bootstrap state).
## Return Format
Status:
Workspace:
Branch:
Files Removed:
Files Kept:
Commit:
Issues:
@@ -0,0 +1,66 @@
# Codex Execution Instruction
Instruction ID: fix-reports-permission-v1
Version: v1
Target Agent: codex
## Objective
Fix the GitOps workspace reports directory permission issue so the Agent OS acceptance report can be committed.
## Context
Current blocked path:
/data/aios-gitops/workspace/reports/
Error:
Permission denied
## Execution Steps
1. Enter workspace:
cd /data/aios-gitops/workspace
2. Check current user and permissions:
whoami
id
ls -ld reports
3. Create directory if missing:
mkdir -p reports
4. Fix ownership and write permission for this directory only:
chown -R <gitops-user>:<gitops-group> reports
chmod -R u+rwX reports
5. Verify write access:
touch reports/.write-test
rm reports/.write-test
6. Commit pending acceptance report changes after verification.
## Constraints
- GitOps only.
- No force push.
- No chmod 777.
- Do not modify unrelated directories.
- Do not write secrets.
- Production changes require approval.
## Validation
Return:
Status:
Branch:
Commit:
Rev:
Sync Status:
Permission Result:
Issues:
@@ -0,0 +1,31 @@
# Codex System Recovery Instruction
Version: v1
Target Agent: codex
## Objective
Recover AIOS system issues requiring deep diagnosis or administrator assistance.
## Execution
1. Inspect runtime status.
2. Inspect GitOps state.
3. Identify root cause.
4. Propose safe patch.
5. Apply only approved GitOps changes.
## Escalation
Use when server agents cannot resolve issues.
## Constraints
- GitOps only.
- No secrets.
- No force push.
- Production changes require approval.
## Return
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
@@ -0,0 +1,48 @@
# Codex Main Merge Verification Instruction
Instruction ID: v1r1c5-main-merge-verify-v1
Version: v1
Target Agent: codex
## Objective
Verify whether V1R1C5 patch branch has been merged into main.
## Preconditions
Ensure GitOps authentication is healthy.
If Gitea account password state blocks access, resolve account activation through approved admin procedure before continuing.
## Verification Steps
1. Check repository status.
2. Fetch remote branches.
3. Compare:
source:
patch/self-evolution-agent-orchestration-policy
target:
main
4. Confirm merge commit.
5. Confirm main HEAD.
6. Confirm no divergence.
## Constraints
- No force push.
- No direct production publish.
- No secret output.
- Preserve audit trail.
## Return Format
Status:
Source Branch:
Target Branch:
Merge Commit:
Rev:
Sync Status:
Conflicts:
Main HEAD:
Issues:
@@ -0,0 +1,59 @@
# Codex V1R2C2 Branch Sync Validation Instruction
Instruction ID: v1r2c2-branch-sync-validation-v1
Version: v1
Target Agent: codex
## Objective
Validate synchronization state of all git branches between local, origin (Gitea), and gitops remote. Identify divergent branches that need reconciliation.
## Preconditions
- GitOps authentication healthy.
- Run from workspace root (`/data/aios-gitops/workspace`).
## Validation Scope
1. **main branch sync**: local main == origin/main == gitops/main
2. **patch branch sync**: for each local branch, compare HEAD against origin and gitops remotes
3. **Current branch**: verify the active working branch is on the correct base
4. **Untracked files**: check for files that should be committed vs ignored
## Steps
1. Fetch all remotes.
2. Capture local branch HEAD hashes.
3. Capture remote origin branch HEAD hashes.
4. Capture remote gitops branch HEAD hashes.
5. For each branch, report:
- Fully synced: local == origin == gitops
- Divergent: local != origin or local != gitops
- Missing remote: branch exists locally but not in remote
6. Check if current branch is based on main or is diverging.
7. List untracked files that may need governance.
## Return Format
```
Status: <synced|divergence_found>
Workspace: /data/aios-gitops/workspace
Main Sync: <yes|no>
Main HEAD: <sha>
Origin/main HEAD: <sha>
Gitops/main HEAD: <sha>
Divergent Branches:
- <branch name>: <local sha> vs <remote sha> [origin|gitops] (<reason>)
Missing Remote Branches:
- <branch name>: <local sha>
Untracked File Count: <count>
Issues:
- <issue description>
```
## Constraints
- No force push.
- No destructive actions.
- No secret output.
- Report only; do not auto-resolve discrepancies.
@@ -0,0 +1 @@
# Placeholder for versioned custom GPT review instructions.
@@ -0,0 +1,31 @@
# Custom GPT Solution Review Instruction
Version: v1
Target Agent: custom_gpt_review
## Objective
Review difficult architecture decisions and implementation proposals from server agents.
## Role
Act as reviewer and advisor for OpenClaw, Hermes and other AIOS agents.
## Execution
1. Receive complete problem context.
2. Review baseline, branch and proposed changes.
3. Analyze alternative solutions.
4. Recommend implementation approach.
5. Identify risks and validation requirements.
## Constraints
- Advisory role only.
- No direct production changes.
- No secrets.
- GitOps workflow required.
- Release requires approval.
## Return
Status:
Recommendation:
Risk:
Validation:
Next Step:
+1
View File
@@ -0,0 +1 @@
# Placeholder for versioned Hermes instructions.
@@ -0,0 +1,32 @@
# Hermes Architecture Review Instruction
Version: v1
Target Agent: hermes
## Objective
Review AIOS architecture, policies and implementation plans.
## Execution
1. Review baseline and branch diff.
2. Analyze architecture impact.
3. Review OpenClaw execution proposals.
4. Validate acceptance criteria.
5. Provide improvement recommendations.
## Escalation
For unresolved architecture choices, prepare complete context for external review.
## Constraints
- GitOps only.
- No secrets.
- No direct production changes.
- Approval required for release.
## Return
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
Recommendations:
+1
View File
@@ -0,0 +1 @@
# Placeholder for versioned OpenClaw instructions.
+24
View File
@@ -0,0 +1,24 @@
# OpenClaw Agent Instructions Index
## Available Instructions
### v1r1c5-hermes-coordination-v1
Instruction file:
agent-instructions/openclaw/v1r1c5-hermes-coordination-v1.md
Purpose:
Coordinate Hermes architecture review through OpenClaw controlled communication channel.
Invocation:
Execute openclaw/v1r1c5-hermes-coordination-v1 instruction
Requirements:
- Read the referenced markdown file from the GitOps workspace.
- Do not treat the identifier as a local shell command.
- Do not search external skill marketplaces unless the file is missing.
- Follow GitOps workflow rules defined in the instruction.
@@ -0,0 +1,32 @@
# OpenClaw Runtime Maintenance Instruction
Version: v1
Target Agent: openclaw
## Objective
Maintain server-side AIOS runtime and perform safe optimization tasks.
## Execution
1. Check runtime health.
2. Review logs and GitOps status.
3. Identify optimization opportunities.
4. Create patch proposals.
5. Execute approved changes.
## Escalation
When solution selection or implementation is uncertain, prepare full context and escalate for review.
## Constraints
- GitOps only.
- Patch-first workflow.
- No secrets.
- No force push.
- Production release requires approval.
## Return
Status:
Branch:
Commit:
Rev:
Sync Status:
Issues:
@@ -0,0 +1,51 @@
# OpenClaw Execution Instruction: Hermes Coordination
Instruction ID: v1r1c5-hermes-coordination-v1
Version: v1
Target Agent: openclaw
## Objective
Coordinate Hermes architecture review through OpenClaw-controlled communication channel.
## Architecture Rule
Hermes is not directly connected to external control channels.
All Hermes requests and responses must be routed through OpenClaw personal WeChat channel.
## Workflow
1. Receive AIOS task or review request.
2. Send required context to Hermes through OpenClaw WeChat.
3. Collect Hermes architecture analysis.
4. Record review result in GitOps reports.
5. Continue execution or escalate when required.
## Context Package
Include:
- Baseline version
- Branch
- Diff summary
- Problem description
- Proposed solution
- Validation requirements
## Constraints
- GitOps only.
- No secrets.
- No direct production changes.
- Maintain audit records.
- Use escalation flow for unresolved decisions.
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Hermes Result:
Issues:
@@ -0,0 +1,119 @@
# OpenClaw Execution Instruction
Instruction ID: v1r1c5-hermes-review-improvement-v1
Version: v1
Target Agent: openclaw
## Objective
Implement V1R1C5 improvements based on Hermes Conditional Approval review.
## Context
Baseline:
V1R1C4
Branch:
patch/self-evolution-agent-orchestration-policy
Proposal:
V1R1C5-agent-os-evolution-proposal
## Required Changes
1. Add Agent Resource Governance:
Create:
config/self-evolution/agent-resource-manager.json
Support:
- resource limits
- parallel task limits
- agent priority
- runtime isolation
- overload handling
2. Enhance Consensus Engine:
Add dynamic weighting:
Final Weight = Base Weight * Quality Score * Historical Reliability * Domain Expertise
Base weights:
- custom_gpt: 2
- windows_codex: 2
- server_agent: 1
Add risk gates:
- Production Release
- V-Level Change
- Security Policy Change
Require:
Consensus Approval + Owner Approval
3. Enhance Message Bus:
Update runtime/agent-os/message_bus.py
Add:
- task_id
- correlation_id
- retry_state
- delivery_status
Support:
- message tracking
- retry
- recovery
- audit
4. Add Agent Health Score:
Create:
runtime/agent-os/agent_health_score.py
Calculate from:
- quality score
- acceptance rate
- historical reliability
Use for:
- scheduler selection
- consensus weighting
## Validation
Run:
python3 -m compileall runtime/agent-os
Run:
runtime/agent-os/integration_test.py
Validate:
- scheduler
- consensus
- message bus
- escalation
- recovery
## Constraints
- GitOps only
- Patch-first
- No secrets
- No force push
- No direct main write
- Production changes require approval
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Changed Files:
Tests:
Hermes Review Items Completed:
Remaining Issues:
@@ -0,0 +1,61 @@
# OpenClaw Hermes Routing Correction Instruction
Instruction ID: v1r1c5-hermes-routing-correction-v1
Version: v1
Target Agent: openclaw
## Objective
Correct Hermes coordination routing. OpenClaw must actively package AIOS review requests and route them through the configured Hermes communication path instead of asking the user to provide missing context when the request context already exists in GitOps.
## Current Architecture
OpenClaw is the coordination entry point.
Hermes is reached indirectly through OpenClaw controlled communication.
## Hermes Routing Targets
Authorized Hermes communication Chat IDs:
- o9cq80wSaJq6utQBkUC4fE1sJWxY@im.wechat
- o9cq80x9RoA6ljoFZDxg71mS0gDA@im.wechat
## Execution Flow
1. Read current AIOS GitOps context.
2. Collect:
- Baseline version
- Branch
- Diff summary
- Problem description
- Proposed solution
- Validation requirements
3. Send complete review package to Hermes through configured OpenClaw communication channel.
4. Receive Hermes analysis.
5. Record review result in GitOps reports.
## Current Review Context
Baseline:
V1R1C4
Branch:
patch/self-evolution-agent-orchestration-policy
Proposal:
V1R1C5-agent-os-evolution-proposal
## Constraints
- Do not ask user for context that already exists in GitOps.
- Do not treat instruction ID as external skill package.
- GitOps audit required.
- No secrets.
- No direct production changes.
## Return
Status:
Hermes Routing:
Review Context:
Result:
Issues:
@@ -0,0 +1,52 @@
# OpenClaw Owner Approval Gate Instruction
Instruction ID: v1r1c5-owner-approval-gate-v1
Version: v1
Target Agent: openclaw
## Objective
Manage owner approval workflow for high-risk AIOS operations.
## Approval Required For
- V-level version changes.
- Production release.
- Tag creation.
- Runtime changes with production impact.
- Administrator authorization actions.
- Unresolved multi-agent decisions.
## Workflow
1. Collect approval context.
2. Include baseline version, branch, diff summary and risk assessment.
3. Route approval request through owner communication channel registry.
4. Wait for approval decision.
5. Record decision in GitOps audit.
## Approval Context
Must include:
- Request reason.
- Affected components.
- Validation results.
- Rollback considerations.
- Risk level.
## Constraints
- No direct production action without approval.
- No secrets in messages or reports.
- Use configured owner communication channels only.
- Maintain audit records.
## Return Format
Status:
Approval Status:
Channel Used:
Decision:
Audit Record:
Issues:
@@ -0,0 +1,43 @@
# OpenClaw Owner Notification Routing Instruction
Instruction ID: v1r1c5-owner-notification-routing-v1
Version: v1
Target Agent: openclaw
## Objective
Implement owner notification routing through configured owner communication channels.
## Routing Rules
1. OpenClaw must first inspect the owner-bound communication channels already configured in the system.
2. Select the primary owner communication channel according to system priority configuration.
3. If the primary channel is unavailable, unhealthy, or delivery fails, automatically fallback to the configured backup communication channel.
4. Record routing decision and delivery status in GitOps audit records.
## Notification Scenarios
Notify owner for:
- High risk architecture decisions.
- Agent consensus deadlock.
- Production-impacting approval requests.
- Required administrator authorization.
- Release approval gates.
## Constraints
- Do not hardcode personal communication identifiers in instruction files.
- Use configured channel registry only.
- Do not expose private contact information in logs.
- Maintain audit trail.
- GitOps workflow required.
## Return Format
Status:
Primary Channel Status:
Fallback Channel Status:
Delivery Result:
Audit Record:
Issues:
@@ -0,0 +1 @@
# Placeholder for versioned Windows Codex App instructions.
@@ -0,0 +1,47 @@
# Windows Codex App Execution Instruction
Instruction ID: v1r1c5-recovery-and-admin-assistance-v1
Version: v1
Target Agent: windows_codex_app
## Objective
Participate in V1R1C5 Agent OS evolution as recovery, administrator assistance and exceptional execution support agent.
## Responsibilities
1. Assist when server OpenClaw agents have runtime failures.
2. Assist when Hermes review cannot be completed through OpenClaw.
3. Handle cases requiring administrator authorization workflow.
4. Participate in multi-agent voting when decision conflicts occur.
5. Validate recovery results after server agent restoration.
## Execution Rules
- Work from online GitOps versions.
- Use patch-first workflow.
- Do not bypass approval gates.
- Do not write secrets.
- Do not force push.
- Do not directly publish production versions.
## Recovery Flow
Server agent failure:
OpenClaw/Hermes issue
-> Windows Codex diagnosis
-> propose fix
-> GitOps patch
-> validation
-> approval
## Return Format
Status:
Branch:
Commit:
Rev:
Sync Status:
Recovery Result:
Issues:
@@ -0,0 +1,27 @@
{
"approval_id": "owner-approve-V1R1C5-migration-20260707",
"created_at": "2026-07-07T11:46:17+08:00",
"approved_by_role": "owner",
"approved_via": "openclaw-weixin",
"approved_at": "2026-07-07T11:46:17+08:00",
"target_branch": "patch/self-evolution-agent-orchestration-policy",
"target_base": "main",
"target_version": "V1R1C5",
"rollback_target": "V1R1C4",
"allowed_actions": [
"merge_to_main",
"create_release_tag",
"activate_new_baseline"
],
"risk_acceptance": "Owner approved V1R1C5 Agent OS evolution migration to main. Integration tests passed, Hermes review approved, conditional approval gate implemented. Rollback target V1R1C4.",
"expires_at": "2026-07-14T23:59:59Z",
"constraints": {
"direct_main_push": false,
"force_push": false,
"secrets_in_git": false,
"destructive_operations": false,
"validation_required": true,
"owner_notification_required": true,
"web_ui_audit_required": true
}
}
@@ -0,0 +1,25 @@
{
"approval_id": "owner-preapprove-V1R1C5-main-entry-20260707",
"created_at": "2026-07-07T00:00:00Z",
"approved_by_role": "owner",
"target_branch": "patch/web-ui-complete-gitops-dashboard-20260706",
"target_base": "main",
"target_version": "V1R1C5",
"rollback_target": "V1R1C4",
"allowed_actions": [
"merge_to_main",
"create_release_tag",
"activate_new_baseline"
],
"risk_acceptance": "Owner approved promotion of autonomous GitOps platform control plane to main, subject to validation, OpenClaw supervision, no secret leakage, no direct force push, and rollback target V1R1C4.",
"expires_at": "2026-07-14T23:59:59Z",
"constraints": {
"direct_main_push": false,
"force_push": false,
"secrets_in_git": false,
"destructive_operations": false,
"validation_required": true,
"owner_notification_required": true,
"web_ui_audit_required": true
}
}
+10 -20
View File
@@ -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"
}
+62 -10
View File
@@ -1,20 +1,20 @@
{
"active_version": "V1R1C0",
"active_tag": "V1R1C0-baseline-20260706",
"active_version": "V1R2C3",
"active_tag": "V1R2C3",
"mode": "gitops-online",
"version_policy": "VxRxCx",
"update_mode": "git_patch_only",
"zip_update_deprecated": true,
"entry": "https://cnnetmkt.top",
"links": {
"git": "/git",
"git": "https://g.cnnetmkt.top/git/aios/aios-v23",
"baseline": "/base",
"versions": "/ver",
"diff": "/diff",
"patch": "/patch",
"model": "/model",
"runtime": "/run",
"api": "/api",
"api": "https://cnnetmkt.top/api",
"hook": "/hook"
},
"features": {
@@ -23,16 +23,19 @@
"custom_llm": true,
"user_model_priority": true,
"chatgpt_rw": true,
"codex_rw": true
"codex_rw": true,
"portal_gitops_dashboard": true,
"human_shortlinks_login_required": true,
"autonomous_recovery_loop": true
},
"version": "V1R1C0",
"baseline_tag": "V1R1C0-baseline-20260706",
"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",
"chatgpt_access": true,
"codex_access": true,
"git_url": "https://cnnetmkt.top/git/aios/aios-v23.git",
"git_url": "https://g.cnnetmkt.top/git/aios/aios-v23.git",
"short_links": [
"/git",
"/base",
@@ -44,5 +47,54 @@
"/api",
"/hook"
],
"updated_at": "2026-07-06T07:13:01.298789+00:00"
}
"updated_at": "2026-07-08T00:00:00Z",
"auth": {
"primary_domain": "https://auth.cnnetmkt.top",
"backup_domain": "https://auth.h.imruyi.top:8443",
"primary_user_entry": "https://cnnetmkt.top/",
"primary_callback": "https://cnnetmkt.top/casdoor/callback/",
"casdoor_application": "admin/aios-portal",
"backup_domain_role": "standby-only"
},
"previous_version": "V1R2C2",
"codex_gitops": {
"config": "config/codex/gitops-online.json",
"token_account": "codex-gitops-bot",
"token_ref": "secret_ref://aios-gitops/runtime/codex-gitops-bot",
"direct_main_push": false,
"all_writes_require_git_commit": true,
"api_keys_in_git": false,
"online_git_sync": true,
"bare_repo": "/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git"
},
"last_collaboration_test": {
"timestamp": "20260706T104130Z",
"local_codex_branch": "patch/collab-local-codex-20260706T104130Z",
"custom_gpt_branch": "patch/collab-custom-gpt-20260706T104130Z",
"custom_gpt_url": "https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai",
"result": "merged_to_main"
},
"security": {
"human_shortlinks": {
"status": "login_required",
"routes": [
"/base",
"/ver",
"/diff",
"/patch",
"/model",
"/run"
],
"target": "https://cnnetmkt.top/portal/me/#gitops",
"machine_api": "https://cnnetmkt.top/api"
},
"gitea": {
"web": "https://g.cnnetmkt.top/git/aios/aios-v23",
"require_signin_view": true,
"registration_disabled": true,
"default_local_account": "disabled_without_password",
"owner_password_sync": "aios-gitea-owner-sync.timer",
"owner_password_source": "secret_ref://aios/user-control-center/UCC_OWNER_PASSWORD"
}
}
}
@@ -0,0 +1,36 @@
# 20260706 Auth GitOps Portal Hardening
Version: V1R1C4
Previous: V1R1C3
Timestamp: 2026-07-06T11:05:30Z
## User-visible fixes
- Human shortlinks no longer expose baseline/model/run JSON without portal login.
- The cnnetmkt portal has a visible GitOps section for owner/admin users.
- Gitea is served from `https://g.cnnetmkt.top/git/` and repository browsing requires login.
- Gitea `owner` password is synchronized from the cnnetmkt owner account secret.
- The default local Gitea account password is removed and direct login is disabled.
## Files
- `deploy/nginx/aios-gitops-locations.conf`
- `deploy/gitea-binary/app.ini.template`
- `deploy/systemd/aios-gitea-owner-sync.service`
- `deploy/systemd/aios-gitea-owner-sync.timer`
- `ops/gitops-migration/sync_gitea_owner_password.sh`
- `ops/gitops-migration/apply_user_portal_gitops_overlay.sh`
- `overlays/user-control-center/control/portal_views.py`
- `overlays/user-control-center/templates/portal/me.html`
- `baseline/current.json`
- `config/codex/gitops-online.json`
- `versions/V1R1C4.json`
- `docs/release/AIOS_V1R1C4_AUTH_GITOPS_PORTAL.md`
## Notes
Docker image rebuild was not required for the live fix. The runtime source and running container were patched, and the exact overlay has been added to GitOps so a later image rebuild can reproduce the change.
## Secret Log Hygiene
The Gitea owner password sync script uses `runuser` instead of `sudo -u ... --password ...`, strips CRLF from sourced owner fields, and does not print passwords or tokens. Archived journal files were rotated/vacuumed after the live script was corrected.
@@ -0,0 +1,25 @@
# 20260706 ChatGPT Actions GitOps Access
## Summary
Implemented a controlled ChatGPT Actions adapter for the AIOS online GitOps baseline under https://cnnetmkt.top/api.
## Access Model
- Public read APIs: /api/base, /api/ver, /api/diff, /api/patch, /api/model, /api/run.
- Bearer-token write APIs: /api/branch, /api/file, /api/commit, /api/patch, /api/tag, /api/rollback, /api/model, /api/model-priority, /api/nl.
- chatgpt-gitops-bot is scoped for branch/file/commit/patch/model/rollback-request/NL plan operations.
- Direct main writes are rejected by the adapter.
- V-level owner/admin actions are represented as approval request commits unless an owner executes them.
- Custom LLM API keys are stored outside Git under runtime secrets; committed model records store only secret_ref.
## Files
- ctive-runtime/app/aios_chatgpt_actions_api.py
- docs/gitops/openapi-chatgpt-actions.yaml
- deploy/systemd/aios-gitops-model-api.service
- scripts/package_contract_check_v1r1c0.sh
## Verification
See /data/aios-gitops/artifacts/CHATGPT_ACCESS_ACCEPTANCE.md after live deployment.
@@ -0,0 +1,34 @@
# 20260706 cnnetmkt auth primary route fix
## Problem
`https://cnnetmkt.top/` could be opened, but login redirected users to `https://auth.h.imruyi.top:8443`. Casdoor then rejected the OAuth request because `https://cnnetmkt.top/casdoor/callback/` was not in the application allow-list.
`*.h.imruyi.top:8443` is retained as a backup domain only and must not be exposed as the primary public login URL.
## Runtime Changes
- Added `auth.cnnetmkt.top` Nginx server block on `hw.imruyi.top` using the wildcard `cnnetmkt.top` certificate.
- Proxied `auth.cnnetmkt.top` to the existing backup/home Casdoor upstream `auth.h.imruyi.top:8443` while preserving the public host as `auth.cnnetmkt.top`.
- Recreated `user-control-web`, `user-control-worker`, and `user-control-beat` with:
- `CASDOOR_PUBLIC_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INTERNAL_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INVITE_SYNC_BASE_URL=https://auth.cnnetmkt.top`
- `UCC_PUBLIC_BASE_URL=https://cnnetmkt.top`
- Updated Casdoor application `admin/aios-portal` on `aihost` to include `https://cnnetmkt.top/casdoor/callback/` and keep user/portal/backup callbacks.
## Backups
- Casdoor DB application table backup: `/data/aios/backups/casdoor-cnnetmkt-primary-20260706T085445Z/application-before.sql`
- HW UCC/Nginx backup directory: `/data/aios-gitops/backups/auth-cnnetmkt-primary-*`
## Verification
- `https://cnnetmkt.top/` redirects to `/portal/login/` and then to `https://auth.cnnetmkt.top/login/oauth/authorize?...redirect_uri=https%3A%2F%2Fcnnetmkt.top%2Fcasdoor%2Fcallback%2F...`.
- `https://auth.cnnetmkt.top/login/oauth/authorize?...` returns HTTP 200 from the Casdoor SPA through the HW Nginx proxy.
- `user-control-web` health returned `healthy` after recreation.
- Casdoor DB readback contains `https://cnnetmkt.top/casdoor/callback/` in `application.redirect_uris`.
## Follow-up
Run `ops/gitops-migration/check_cnnetmkt_auth_primary.sh` after any domain, Nginx, or Casdoor application change.
@@ -0,0 +1,14 @@
# 20260706 Codex and Custom GPT Collaboration
## Change
Validated coordinated online GitOps changes from local Codex and the AIOS GitOps 控制台 custom GPT path. Promoted the merged result to `V1R1C3`.
## Evidence Branches
- `patch/collab-local-codex-20260706T104130Z` -> `a62d4ce` (codex-gitops-bot, sync=synced)
- `patch/collab-custom-gpt-20260706T104130Z` -> `c393931` (chatgpt-gitops-bot, sync=synced)
## Result
`/api/base` should report `V1R1C3` after this release commit is synchronized.
@@ -0,0 +1,16 @@
# 20260706 Codex Online Git Branch Sync
## Change
Promoted the online GitOps configuration to `V1R1C2` and enabled automatic synchronization of committed non-main API/Codex branches to the Gitea bare repository.
## Reason
Codex writes must be visible through the online Git service, not only in the local workspace clone.
## Guardrails
- `main` and `master` are skipped by API branch sync.
- Direct main writes remain forbidden by branch validation.
- Tags are synchronized only in the owner direct tag execution path.
- Tokens and LLM API keys remain outside Git.
@@ -0,0 +1,23 @@
# 20260706 Codex Online GitOps Version Management
## Change
Configured Codex to use the AIOS online Git baseline for version management and promoted the current controlled access changes to `V1R1C1`.
## Files
- `config/codex/gitops-online.json`
- `docs/gitops/CODEX_ONLINE_VERSION_MANAGEMENT.md`
- `docs/gitops/CHATGPT_CODEX_ACCESS.md`
- `docs/release/AIOS_V1R1C1_CODEX_GITOPS_RELEASE.md`
- `versions/V1R1C1.json`
- `baseline/current.json`
- `scripts/codex_gitops_online_check.sh`
## Merge Policy
Production `main` includes the ChatGPT/Codex GitOps API, schema, service configuration, contract checks, and secret-ref fix. Acceptance dummy model branches are retained as audit branches and not merged into production.
## Secret Policy
`codex-gitops-bot` plaintext token is stored only under `/data/aios-gitops/runtime/secrets/`; Git stores only `secret_ref` and token account metadata.
+24
View File
@@ -0,0 +1,24 @@
self_loop:
enabled: true
mode: evaluate_plan_verify
secrets_in_git: false
direct_main_push: false
git_patch_only: true
owner_gate:
enabled: true
required_for:
- runtime_recovery
- security_policy
- mainline_promotion
- release_tag
automatic_actions:
allowed:
- health_probe
- test_execution
- failure_classification
- patch_plan_generation
- report_generation
restricted:
- production_promotion
- permission_escalation
- secret_rotation
+78
View File
@@ -0,0 +1,78 @@
{
"profile": "aios-v23-online-gitops",
"mode": "gitops-online",
"version": "V1R1C4",
"previous_version": "V1R1C3",
"repository": {
"workspace": "/data/aios-gitops/workspace",
"remote": "origin",
"url": "https://g.cnnetmkt.top/git/aios/aios-v23.git",
"web": "https://g.cnnetmkt.top/git/aios/aios-v23",
"protected_branches": [
"main"
],
"patch_branch_prefixes": [
"patch/",
"codex/patch/",
"request/"
],
"bare_repo": "/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git",
"sync_mode": "workspace_commit_to_online_bare_repo",
"public_portal": "https://cnnetmkt.top/portal/me/#gitops"
},
"api": {
"base_url": "https://cnnetmkt.top/api",
"openapi_schema": "https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml",
"local_schema": "/data/aios-gitops/workspace/docs/gitops/openapi-chatgpt-actions.yaml",
"token_account": "codex-gitops-bot",
"token_ref": "secret_ref://aios-gitops/runtime/codex-gitops-bot"
},
"policy": {
"zip_update_deprecated": true,
"update_mode": "git_patch_only",
"all_writes_require_git_commit": true,
"direct_main_push": false,
"delete_tag": false,
"v_level_owner_admin_approval_required": true,
"api_keys_in_git": false,
"api_keys_in_logs": false,
"custom_llm_secret_storage": "runtime_secret_ref_only",
"online_git_sync": true,
"human_shortlinks_login_required": true,
"gitea_require_signin_view": true,
"gitea_default_local_account_disabled": true,
"owner_password_sync_secret_ref_only": true
},
"workflow": [
"create patch branch from main",
"commit every file/config/script change",
"push or sync branch to online Git service",
"review diff and run smoke checks",
"merge to main only after owner/admin approval",
"tag VxRxCx release and update baseline/current.json"
],
"created_at": "2026-07-06T10:21:08Z",
"base_commit_before_release": "43ddb91",
"updated_at": "2026-07-06T11:16:23Z",
"last_collaboration_test": {
"timestamp": "20260706T104130Z",
"result": "local_codex_and_custom_gpt_patches_merged"
},
"last_auth_portal_fix": {
"version": "V1R1C4",
"timestamp": "2026-07-06T11:16:23Z",
"shortlink_routes": [
"/base",
"/ver",
"/diff",
"/patch",
"/model",
"/run"
],
"gitea_sync_service": "aios-gitea-owner-sync.service",
"gitea_sync_timer": "aios-gitea-owner-sync.timer",
"portal_overlay": "overlays/user-control-center",
"portal_baseline_cache": "/app/runtime/gitops/current.json",
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
}
}
+4
View File
@@ -1,4 +1,8 @@
AIOS_PUBLIC_DOMAIN=https://cnnetmkt.top
AIOS_USER_DOMAIN=https://user.cnnetmkt.top
AIOS_AUTH_DOMAIN=https://auth.cnnetmkt.top
AIOS_AUTH_BACKUP_DOMAIN=https://auth.h.imruyi.top:8443
AIOS_CASDOOR_PRIMARY_CALLBACK=https://cnnetmkt.top/casdoor/callback/
AIOS_WEBOPS_DOMAIN=https://ops.cnnetmkt.top
AIOS_API_DOMAIN=https://api.cnnetmkt.top
AIOS_WECHAT_CALLBACK_DOMAIN=https://wechat.cnnetmkt.top
@@ -0,0 +1 @@
{"schema":"acceptance-pipeline-v1","status":"proposal","pipeline":{"stages":[{"name":"code_review","agent_role":"review_agent"},{"name":"automated_test","agent_role":"test_agent"},{"name":"security_check","agent_role":"security_agent"},{"name":"performance_check","agent_role":"performance_agent"},{"name":"regression_check","agent_role":"regression_agent"},{"name":"release_readiness","agent_role":"release_readiness_agent"}],"parallel_validation":true,"failure_blocks_release":true},"outputs":{"quality_score":true,"risk_report":true,"approval_request":true},"safety":{"production_release_requires_owner":true,"credentials_scan":true,"audit_log":true}}
@@ -0,0 +1 @@
{"schema":"agent-governance-v1","status":"proposal","governance":{"identity":true,"reputation":true,"capability_discovery":true,"memory_service":true,"shared_knowledge":true,"skill_packages":true,"peer_review":true,"acceptance_pipeline":["optimization","implementation","acceptance","security","release_readiness"],"dynamic_voting":{"enabled":true,"base_weights":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"quality_score_modifier":true},"audit":{"full_trace":true,"decision_log":true}},"release":{"patch_only":true,"owner_approval_required":true}}
@@ -0,0 +1,63 @@
{
"policy_id": "self-evolution-agent-orchestration-policy",
"version": 1,
"status": "proposal",
"scope": "self-evolution-execution-orchestration",
"execution_subjects": {
"primary": [
"server.openclaw_agent",
"server.hermes_agent"
],
"collaborators": [
"windows.codex_app",
"custom_gpt.codex_chartgpt_bridge"
]
},
"parallelism": {
"server_agents_parallel_allowed": true,
"multiple_server_agents_allowed": true,
"server_agents_preferred": true
},
"fallback_and_assistance": {
"difficult_case_external_interaction_allowed": true,
"assistance_endpoint": "https://chatgpt.com/g/g-6a4ba70a063c8191a89bc2d86a4c7fa0-codex-chartgpt-bridge",
"allowed_modes": [
"ask_for_help",
"interactive_coordination",
"proposal_review"
]
},
"dispute_resolution": {
"multi_agent_voting_allowed": true,
"weights": {
"custom_gpt.codex_chartgpt_bridge": 2,
"windows.codex_app": 2,
"server.openclaw_agent": 1,
"server.hermes_agent": 1,
"server.additional_agent_default": 1
},
"tie_breaking": "require_owner_or_admin_review"
},
"safety": {
"credentials_in_git_allowed": false,
"force_push_allowed": false,
"destructive_bulk_delete_allowed": false,
"v_level_change_requires_owner_approval": true,
"production_switch_requires_owner_approval": true,
"tag_creation_requires_owner_approval": true,
"v_level_tag_or_production_switch_requires_owner_approval": true,
"direct_main_push_allowed": false,
"direct_main_merge_allowed": false
},
"gitops": {
"audit_required": true,
"writes_require_commit": true,
"default_target_branch": "patch/self-evolution-agent-orchestration-policy",
"direct_main_push_default": false,
"patch_branch_only": true,
"main_merge_requires_owner_admin_approval": true
},
"created_by": "codex-chartgpt-bridge",
"created_at": "2026-07-07T00:00:00Z",
"branch": "patch/self-evolution-agent-orchestration-policy"
}
@@ -0,0 +1 @@
{"schema":"agent-os-v1","status":"proposal","agents":{"openclaw":{"memory":true,"skills":true,"tools":true,"planner":true,"evaluator":true,"self_reflection":true,"learning":true},"hermes":{"memory":true,"skills":true,"tools":true,"planner":true,"evaluator":true,"self_reflection":true,"learning":true}},"coordination":{"voting":true,"weighted_voting":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"owner_review_on_tie":true},"safety":{"gitops_only":true,"patch_first":true,"no_credentials":true,"no_direct_main":true}}
@@ -0,0 +1 @@
{"schema":"agent-platform-v1","status":"proposal","modules":[{"name":"memory-manager","enabled":true},{"name":"skill-registry","enabled":true},{"name":"agent-registry","enabled":true},{"name":"task-scheduler","enabled":true},{"name":"consensus-engine","enabled":true},{"name":"continuous-self-evolution","enabled":true}],"release_policy":{"patch_first":true,"owner_approval":true}}
@@ -0,0 +1 @@
{"schema":"agent-protocol-v1","status":"proposal","agent_identity":{"id":"string","name":"string","family":"openclaw|hermes|custom","version":"string","capabilities":[],"skills":[],"health":"unknown|healthy|degraded|offline"},"task_protocol":{"task_id":"string","requester":"agent_id","executor":"agent_id","objective":"string","priority":"low|normal|high|critical","status":"queued|running|completed|failed|review","result":"object"},"memory_protocol":{"memory_id":"string","type":"experience|knowledge|decision|reflection","scope":"agent|shared","audit":true},"voting_protocol":{"enabled":true,"proposal":"string","votes":[],"weighted":true,"tie":"owner_review"},"security":{"credentials":false,"direct_main_write":false,"production_action_approval":true}}
@@ -0,0 +1,43 @@
{
"$schema": "schema/self-evolution.schema.json",
"title": "Agent Resource Manager Configuration",
"description": "Defines resource limits, parallel task limits, priority, runtime isolation, and overload handling for agent orchestration.",
"version": "1.0.0",
"config": {
"default_resource_limits": {
"max_concurrent_tasks": 5,
"max_memory_mb": 1024,
"max_cpu_cores": 2,
"timeout_seconds": 300
},
"agent_priority_levels": {
"critical": 100,
"high": 80,
"medium": 50,
"low": 20,
"background": 0
},
"runtime_isolation": {
"enabled": true,
"namespace_per_agent": true,
"ephemeral_storage": "1Gi",
"network_policy": "restrictive"
},
"overload_handling": {
"max_queue_depth": 100,
"backpressure_threshold": 0.9,
"cooldown_seconds": 30,
"scaling_strategy": "queue"
},
"agent_pools": {
"server_agents": {
"max_instances": 3,
"priority": "high"
},
"external_agents": {
"max_instances": 5,
"priority": "medium"
}
}
}
}
@@ -0,0 +1 @@
{"schema":"agent-runtime-components-v1","status":"proposal","components":{"agent-runtime-manager":{"enabled":true,"responsibilities":["lifecycle_management","health_check","recovery","resource_tracking"]},"agent-registry":{"enabled":true,"responsibilities":["identity","capability_discovery","status_tracking","version_tracking"]},"message-bus":{"enabled":true,"responsibilities":["task_routing","event_delivery","agent_coordination"]},"memory-service":{"enabled":true,"responsibilities":["long_term_memory","working_memory","knowledge_retrieval","audit"]},"skill-loader":{"enabled":true,"responsibilities":["skill_installation","skill_validation","skill_versioning"]},"agent-evaluator":{"enabled":true,"responsibilities":["quality_score","acceptance_check","performance_feedback"]}},"security":{"credential_storage":false,"gitops_audit":true,"patch_only":true}}
@@ -0,0 +1 @@
{"schema":"consensus-engine-v1","status":"proposal","consensus":{"enabled":true,"modes":["weighted_vote","review_board","owner_escalation"],"participants":["openclaw_agent","hermes_agent","windows_codex","custom_gpt"],"base_weights":{"custom_gpt":2,"windows_codex":2,"server_agents":1},"dynamic_adjustment":{"enabled":true,"factors":["quality_score","acceptance_rate","historical_reliability"]},"conflict_detection":true,"tie_breaking":"owner_admin_review"},"decision_log":{"required":true,"audit":true},"safety":{"production_decision_requires_approval":true}}
@@ -0,0 +1 @@
{"schema":"continuous-evolution-engine-v1","status":"proposal","pipeline":{"monitor":true,"detect":true,"plan":true,"schedule":true,"execute":true,"learn":true},"workflow":{"detect_sources":["runtime_metrics","logs","acceptance_results","agent_feedback","user_requests"],"proposal_generation":true,"patch_generation":true,"validation_required":true,"consensus_required":true,"approval_gate":true,"release_feedback":true},"agents":{"optimizer":"system_optimization_agent","planner":"planning_agent","executor":"patch_execution_agent","validator":"acceptance_validation_agent","reviewer":"consensus_engine"},"learning":{"store_experience":true,"update_quality_score":true,"reflection_required":true},"safety":{"gitops_only":true,"patch_first":true,"credentials":false,"production_requires_approval":true}}
@@ -0,0 +1 @@
{"schema":"agent-platform-implementation-v1","status":"proposal","objective":"evolve-agent-platform-from-configuration-to-operational-framework","implementation_phases":[{"phase":1,"name":"agent-runtime-manager","scope":["agent lifecycle","health check","startup shutdown","recovery"]},{"phase":2,"name":"agent-message-bus","scope":["agent communication","task routing","event coordination"]},{"phase":3,"name":"memory-service","scope":["long term memory","working memory","shared knowledge retrieval","audit"]},{"phase":4,"name":"skill-framework","scope":["skill packages","versioning","installation","validation"]},{"phase":5,"name":"agent-api","scope":["agent registration","capability discovery","task submission","status query"]},{"phase":6,"name":"continuous-evolution-pipeline","scope":["proposal generation","patch creation","automated validation","approval workflow"]}],"constraints":{"gitops_only":true,"patch_first":true,"no_credentials":true,"no_force_push":true,"production_release_requires_approval":true}}
+1
View File
@@ -0,0 +1 @@
{"schema":"agent-memory-v1","status":"proposal","memory_types":{"working_memory":{"ttl":"task_scope","audit":true},"long_term_memory":{"persistent":true,"scope":"agent"},"shared_knowledge":{"persistent":true,"scope":"platform"},"reflection_memory":{"persistent":true,"purpose":"self_improvement"}},"memory_entry":{"id":"string","agent_id":"string","type":"working|experience|knowledge|decision|reflection","content":"object","source_task":"string","created_at":"timestamp","quality_score":0},"safety":{"credentials":false,"secrets":false,"audit_required":true}}
@@ -0,0 +1,26 @@
{
"schema": "owner-channel-registry-v1",
"status": "proposal",
"purpose": "Owner notification channel routing and fallback management",
"routing": {
"primary_selection": "configured_priority",
"fallback_enabled": true,
"health_check_required": true,
"delivery_failure_fallback": true
},
"channel_policy": {
"identifiers_managed_externally": true,
"no_hardcoded_personal_identifiers": true,
"audit_delivery_result": true
},
"notification_priority": [
"production_approval",
"administrator_authorization",
"high_risk_decision",
"agent_consensus_failure"
],
"security": {
"credentials_in_git": false,
"private_contact_data_in_logs": false
}
}
@@ -0,0 +1 @@
{"schema":"release-automation-v1","status":"proposal","release_flow":{"patch_review":true,"acceptance_gate":true,"consensus_gate":true,"approval_request":true,"version_proposal":true,"tag_request":true},"agents":{"release_manager":"release_readiness_agent","reviewers":["openclaw_agent","hermes_agent","custom_gpt","windows_codex_app"]},"controls":{"auto_tag":false,"auto_production_publish":false,"owner_approval_required":true,"audit_log":true},"rollback":{"request_supported":true,"automatic_destructive_rollback":false}}
@@ -0,0 +1,119 @@
{
"policy_id": "server-agent-roles-and-capabilities",
"version": 1,
"status": "proposal",
"scope": "self-evolution-server-agents",
"agent_families": {
"openclaw": {
"enabled": true,
"runtime": "server",
"priority": "primary",
"roles": [
"system_optimization_agent",
"acceptance_validation_agent",
"patch_execution_agent",
"runtime_diagnosis_agent",
"regression_review_agent"
],
"persistent_memory": {
"enabled": true,
"scope": "agent_family_and_role",
"storage": "runtime_or_gitops_managed_memory",
"credentials_in_memory_allowed": false,
"audit_required": true
},
"skills": {
"enabled": true,
"skill_registry": "config/self-evolution/skills/openclaw-skills.json",
"required_skill_groups": [
"gitops_patch_workflow",
"system_optimization",
"acceptance_testing",
"runtime_observability",
"safe_release_review"
]
}
},
"hermes": {
"enabled": true,
"runtime": "server",
"priority": "primary",
"roles": [
"system_optimization_agent",
"acceptance_validation_agent",
"release_readiness_agent",
"policy_review_agent",
"cross_agent_coordination_agent"
],
"persistent_memory": {
"enabled": true,
"scope": "agent_family_and_role",
"storage": "runtime_or_gitops_managed_memory",
"credentials_in_memory_allowed": false,
"audit_required": true
},
"skills": {
"enabled": true,
"skill_registry": "config/self-evolution/skills/hermes-skills.json",
"required_skill_groups": [
"gitops_patch_workflow",
"acceptance_testing",
"release_readiness",
"policy_governance",
"multi_agent_coordination"
]
}
}
},
"role_contracts": {
"system_optimization_agent": {
"purpose": "Identify and propose safe system optimization patches.",
"may_write_patch": true,
"may_modify_runtime": false,
"requires_review_for_production": true
},
"acceptance_validation_agent": {
"purpose": "Run and record acceptance checks for GitOps changes.",
"may_write_patch": true,
"may_approve_own_change": false,
"requires_review_for_production": true
},
"patch_execution_agent": {
"purpose": "Apply approved or proposed patch changes on non-main branches.",
"may_write_patch": true,
"direct_main_push_allowed": false
},
"release_readiness_agent": {
"purpose": "Evaluate whether a patch is ready for owner/admin release approval.",
"may_create_release_request": true,
"may_create_tag_directly": false,
"may_switch_production_directly": false
},
"cross_agent_coordination_agent": {
"purpose": "Coordinate voting, conflict resolution, and escalation among agents.",
"voting_allowed": true,
"tie_breaking": "owner_or_admin_review"
}
},
"online_capabilities": {
"read_online_baseline": true,
"read_online_versions": true,
"read_online_diff": true,
"read_online_runtime": true,
"create_patch_branch": true,
"commit_patch_changes": true,
"sync_online_git": true,
"create_release_request": true,
"direct_tag_creation": false,
"direct_production_publish": false
},
"safety": {
"credentials_in_git_allowed": false,
"credentials_in_memory_allowed": false,
"force_push_allowed": false,
"destructive_bulk_delete_allowed": false,
"owner_admin_approval_required_for_release": true
},
"created_by": "codex-chartgpt-bridge",
"created_at": "2026-07-07T00:00:00Z"
}
@@ -0,0 +1 @@
{"schema":"agent-skill-package-v1","status":"proposal","skill_package":{"id":"string","name":"string","version":"string","description":"string","owner_agent":"agent_id","dependencies":[],"capabilities":[],"entrypoints":[],"validation":{"required":true,"tests":[],"quality_score":0}},"lifecycle":{"install":true,"upgrade":true,"rollback":true,"disable":true},"sharing":{"agent_scope":true,"platform_scope":true},"security":{"credentials":false,"secrets":false,"audit_required":true,"production_change_requires_approval":true}}
@@ -0,0 +1 @@
{"schema":"task-scheduler-v1","status":"proposal","scheduler":{"enabled":true,"modes":["capability_match","priority_queue","parallel_execution","failure_recovery"],"selection":{"factors":["capability","skill_match","health","quality_score","load"]},"priority":{"levels":["critical","high","normal","low"]}},"execution":{"parallel_agents":true,"retry":true,"handoff":true,"timeout_control":true},"safety":{"patch_first":true,"direct_main":false,"production_requires_approval":true}}
+28
View File
@@ -0,0 +1,28 @@
self_loop_runtime:
enabled: true
scheduler:
triggers:
- event
- interval
- manual
retry:
enabled: true
max_attempts: 3
api:
prefix: /self-loop
operations:
- enqueueSelfLoopJob
- listSelfLoopJobs
- getSelfLoopJob
- runSelfLoopJob
- tickSelfLoopJobs
- runSelfLoopOnce
- evaluateSelfLoopProductionGate
gitops:
adapter: controlled_actions
branch_policy: patch-only
direct_main_push: false
secrets_in_git: false
promotion:
acceptance_gate_required: true
owner_approval_required: true
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
CREATE TABLE IF NOT EXISTS message_outbox (
id TEXT PRIMARY KEY,
channel TEXT NOT NULL,
recipient_user_id TEXT NOT NULL,
recipient_role TEXT,
message_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT,
content TEXT NOT NULL,
payload TEXT,
status TEXT NOT NULL DEFAULT 'queued',
retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TEXT NOT NULL,
sent_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_message_outbox_dispatch
ON message_outbox(channel, status, priority, created_at);
CREATE TABLE IF NOT EXISTS channel_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
channel_type TEXT NOT NULL,
external_chat_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_channel_bindings_lookup
ON channel_bindings(user_id, channel_type, status, updated_at);
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
actor TEXT NOT NULL,
action TEXT NOT NULL,
target TEXT,
detail TEXT,
created_at TEXT NOT NULL
);
+4 -3
View File
@@ -6,8 +6,8 @@ WORK_PATH = /data/aios-gitops/runtime/gitea-bin
PROTOCOL = http
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3000
DOMAIN = cnnetmkt.top
ROOT_URL = https://cnnetmkt.top/git/
DOMAIN = g.cnnetmkt.top
ROOT_URL = https://g.cnnetmkt.top/git/
SERVE_FROM_SUB_PATH = true
DISABLE_SSH = true
OFFLINE_MODE = false
@@ -27,7 +27,8 @@ INTERNAL_TOKEN = __INTERNAL_TOKEN__
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
REQUIRE_SIGNIN_VIEW = true
SHOW_REGISTRATION_BUTTON = false
[webhook]
ALLOWED_HOST_LIST = *
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[AIOS] Installing Autonomous GitOps System..."
WORKDIR=/data/aios-gitops/workspace
ETC=/etc/aios
RUNTIME=/data/aios-gitops/runtime
SERVICE_USER=aios
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
echo "[AIOS] Creating system user: $SERVICE_USER"
useradd --system --home /data/aios-gitops --shell /usr/sbin/nologin "$SERVICE_USER"
fi
mkdir -p "$ETC"
cp deploy/systemd/aios-autonomous-daemon.env.template "$ETC/aios-autonomous-daemon.env"
mkdir -p \
"$RUNTIME/events" \
"$RUNTIME/audit" \
"$RUNTIME/requests" \
"$RUNTIME/reports/codex" \
"$RUNTIME/approvals/preapproved" \
"$RUNTIME/integrations/v1r1-personal-wechat/outbox" \
"$RUNTIME/integrations/v1r1-personal-wechat/inbox" \
"$RUNTIME/integrations/v1r1-personal-wechat/status"
chown -R "$SERVICE_USER:$SERVICE_USER" /data/aios-gitops/runtime || true
chown -R "$SERVICE_USER:$SERVICE_USER" "$WORKDIR" || true
cp deploy/systemd/aios-autonomous-daemon.service /etc/systemd/system/
cp deploy/systemd/aios-v1r1-wechat-worker.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable aios-autonomous-daemon.service || true
systemctl enable aios-v1r1-wechat-worker.service || true
systemctl restart aios-autonomous-daemon.service || true
systemctl restart aios-v1r1-wechat-worker.service || true
sleep 2
systemctl status aios-autonomous-daemon.service --no-pager || true
systemctl status aios-v1r1-wechat-worker.service --no-pager || true
echo "[AIOS] Installation complete"
+6 -6
View File
@@ -24,12 +24,12 @@
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /base { proxy_pass http://127.0.0.1:18890/base; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /ver { proxy_pass http://127.0.0.1:18890/ver; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /diff { proxy_pass http://127.0.0.1:18890/diff; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /patch { proxy_pass http://127.0.0.1:18890/patch; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /model { proxy_pass http://127.0.0.1:18890/model; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /run { proxy_pass http://127.0.0.1:18890/run; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location = /base { return 302 /portal/me/#gitops; }
location = /ver { return 302 /portal/me/#gitops; }
location = /diff { return 302 /portal/me/#gitops; }
location = /patch { return 302 /portal/me/#gitops; }
location = /model { return 302 /portal/me/#gitops; }
location = /run { return 302 /portal/me/#gitops; }
location = /hook { proxy_pass http://127.0.0.1:18890/hook; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
location /hook/ {
proxy_pass http://127.0.0.1:18890/hook/;
+40
View File
@@ -0,0 +1,40 @@
# AIOS primary public Casdoor entry for cnnetmkt.top.
# The upstream remains the existing backup/home Casdoor service; public clients should see auth.cnnetmkt.top only.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name auth.cnnetmkt.top;
access_log /var/log/nginx/aios-auth-cnnetmkt.access.log;
error_log /var/log/nginx/aios-auth-cnnetmkt.error.log;
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass https://auth.h.imruyi.top:8443;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_ssl_name auth.h.imruyi.top;
proxy_ssl_verify off;
proxy_set_header Host auth.h.imruyi.top;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect https://auth.h.imruyi.top:8443/ https://auth.cnnetmkt.top/;
proxy_redirect https://auth.h.imruyi.top/ https://auth.cnnetmkt.top/;
}
}
server {
listen 80;
listen [::]:80;
server_name auth.cnnetmkt.top;
return 301 https://$host$request_uri;
}
@@ -0,0 +1,12 @@
# cnnetmkt wildcard certificate
generated_at=2026-07-06T16:24:28+08:00
issuer=Let's Encrypt via lego DNS-01 Huawei Cloud
domains=cnnetmkt.top,*.cnnetmkt.top
live_dir=/etc/letsencrypt/live/cnnetmkt-wildcard
nginx_sites=/etc/nginx/sites-available/aios-user-portal.conf,/etc/nginx/sites-available/g-cnnetmkt-gitea.conf
renew_service=cnnetmkt-wildcard-renew.service
renew_timer=cnnetmkt-wildcard-renew.timer
secret_env=/etc/letsencrypt/huaweicloud-cnnetmkt.env
backup_dir=/data/aios-gitops/backups/nginx-wildcard-20260706-162202
status=issued
+52
View File
@@ -0,0 +1,52 @@
# AIOS GitOps alias for https://g.cnnetmkt.top/
# Note: this server block intentionally keeps /git/ as the Gitea app prefix,
# matching the current Gitea ROOT_URL and avoiding a disruptive app.ini rewrite.
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name g.cnnetmkt.top;
access_log /var/log/nginx/aios-g-cnnetmkt.access.log;
error_log /var/log/nginx/aios-g-cnnetmkt.error.log;
ssl_certificate /etc/letsencrypt/live/cnnetmkt-wildcard/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cnnetmkt-wildcard/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location = / {
return 302 /git/;
}
location = /git {
return 302 /git/;
}
location /git/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /git;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
}
}
server {
listen 80;
listen [::]:80;
server_name g.cnnetmkt.top;
location = / {
return 302 https://$host/git/;
}
location / {
return 301 https://$host$request_uri;
}
}
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=AIOS Self-loop Supervisor
After=network.target
[Service]
Type=simple
WorkingDirectory=/data/aios-gitops/workspace
ExecStart=/usr/bin/python3 -m active_runtime.app.self_loop_worker
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,7 @@
[Unit]
Description=Sync AIOS Gitea owner password from UCC owner account
After=aios-gitea.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/aios-sync-gitea-owner-password.sh
@@ -0,0 +1,10 @@
[Unit]
Description=Periodic AIOS Gitea owner password sync
[Timer]
OnBootSec=2min
OnUnitActiveSec=15min
Persistent=true
[Install]
WantedBy=timers.target
+2 -1
View File
@@ -10,9 +10,10 @@ Environment=AIOS_GITOPS_ROOT=/data/aios-gitops
Environment=AIOS_GITOPS_WORKSPACE=/data/aios-gitops/workspace
Environment=AIOS_GITOPS_DB=/data/aios-gitops/runtime/gitops_model.sqlite3
Environment=AIOS_GITOPS_TOKEN_FILE=/data/aios-gitops/runtime/secrets/gitops.tokens
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18890
ExecStart=/usr/bin/python3 /data/aios-gitops/workspace/active-runtime/app/aios_gitops_model_api.py
ExecStart=/usr/bin/python3 /data/aios-gitops/workspace/active-runtime/app/aios_chatgpt_actions_api.py
Restart=always
RestartSec=3
@@ -0,0 +1,154 @@
# Codex 验收手册:GitOps Platform UI
## 目标
本次变更的目标是把 `cnnetmkt.top` AIOS GitOps Web UI 从基础 dashboard 升级为平台级控制台,形成可审计、可审批、可灰度、可回滚请求的 GitOps 操作入口。
## 验证分支
- Branch: `patch/web-ui-complete-gitops-dashboard-20260706`
- Base: `main`
- 当前目标基线: `V1R1C4`
- 合并后建议新基线: `V1R1C5`
## 主线差异点
与 `main` 相比,本分支新增或修改以下直接内容:
1. `overlays/user-control-center/portal/gitops-dashboard/index.html`
- 新增平台级 GitOps 控制台页面。
- 包含 Overview、RBAC、Approval、Diff Tree、Patch、Model、Release、Audit 模块。
2. `overlays/user-control-center/portal/gitops-dashboard/app.js`
- 新增浏览器运行时 Platform Control Plane。
- 包含 API-first client、RBAC guard、approval fallback、audit local store、diff tree parser、model graph、SSE event hook。
3. `overlays/user-control-center/portal/gitops-dashboard/styles.css`
- 控制台样式。
4. `overlays/user-control-center/portal/gitops-dashboard/manifest.json`
- 控制台 manifest。
5. `overlays/user-control-center/portal/gitops-dashboard/gitops-ui-policy.json`
- UI 侧策略声明。
6. `overlays/user-control-center/portal/gitops-dashboard/gitops-enterprise-api.contract.json`
- 后端 API 契约说明。
7. `patches/patch_web-ui-complete-gitops-dashboard-20260706.branch.json`
- GitOps branch 元数据。
## 间接影响区域
Codex 验证时必须确认以下间接影响:
1. Portal 登录入口不应被破坏。
- `/portal/me/#gitops`
- `/portal/me/#gitops-dashboard`
2. 现有短链仍应保持登录保护或正常跳转。
- `/base`
- `/ver`
- `/diff`
- `/patch`
- `/model`
- `/run`
3. 机器 API 不应被前端变更破坏。
- `/api/base`
- `/api/ver`
- `/api/diff`
- `/api/patch`
- `/api/model`
- `/api/run`
4. 敏感信息不得进入 Git、日志、前端源码或测试产物。
- 禁止真实 API key。
- 禁止 Bearer token。
- 禁止 password / private key。
- 允许 `secret_ref://...`。
5. 高风险操作不得绕过审批。
- rollback 只能 request。
- tag/release 默认只能 request。
- runtime 写入只能 request。
- UI 不提供 direct main write。
## Codex 快速验证命令
在目标工作区执行:
```bash
git fetch --all --prune
git checkout patch/web-ui-complete-gitops-dashboard-20260706
git diff --stat main...HEAD
bash scripts/validate-gitops-platform-ui.sh patch/web-ui-complete-gitops-dashboard-20260706 main
```
## 灰度验证外的必测项目
除正常灰度页面点击验证外,Codex 必须按 `tests/acceptance/gitops-platform-ui.acceptance.json` 执行以下测试组:
1. Static Files
- 必须存在 index/html/js/css/manifest/policy/contract 文件。
2. Security
- 源码中不得出现真实 secret/token/password。
- 模型注册必须只允许 `secret_ref://`。
- rollback/tag/runtime/main write 必须 request-only 或 disabled。
3. UI Direct Impact
- 所有 tab id 必须能在 HTML 中找到。
- app.js 必须绑定 `DOMContentLoaded`。
- app.js 必须包含 `guardAction`、`audit`、`createApproval`、`renderDiffTree`。
4. API Contract Compatibility
- 读接口保持 `/api/base`、`/api/ver`、`/api/run`、`/api/patch`、`/api/model`。
- 写接口必须包含 branch、patch/plan、model、model-priority、rollback-request、tag-request。
5. Indirect Regression
- 不得修改 auth/casdoor 配置。
- 不得修改 Gitea owner password sync 配置。
- 不得修改 baseline active_version。
- 不得删除任何版本/tag/patch 历史。
## 验收判定
- PASS: 所有 required 测试项通过,且没有 secret 泄漏、没有 direct main write、没有破坏短链/API。
- FAIL: 任一 required 测试项失败。
- BLOCKED: 需要真实后端或浏览器环境,但静态检查通过;必须列出 blocked 项,不得合并 main。
## 失败回流规则
如果验证失败,Codex 必须输出:
1. 失败测试 ID。
2. 失败文件路径。
3. 具体失败原因。
4. 最小修改建议。
5. 是否阻止合并。
然后把问题交给自定义 GPT 继续修改同一 patch 分支。修复后重复执行完整验收,直到 PASS。
## 合并规则
只有满足以下条件才能合并 main:
1. `scripts/validate-gitops-platform-ui.sh` PASS。
2. acceptance JSON 中所有 `required: true` 项 PASS。
3. 灰度验证通过。
4. 无 secret 泄漏。
5. 无 direct main write。
6. 已创建或确认合并后基线提案 `V1R1C5`。
## 合并后基线要求
合并进入 main 后,必须定义新基线:
- Version: `V1R1C5`
- Purpose: `Promote AIOS GitOps Platform Control Plane UI with Codex validation workflow, acceptance tests, RBAC/approval/audit/release/model control-plane scaffolding.`
- Risk: `high`
- Approval: `owner/admin required`
- Rollback target: `V1R1C4`
合并后再创建 tag/version,不允许在未合并或未验收通过前直接创建生产 tag。
+15
View File
@@ -12,3 +12,18 @@ Forbidden:
- API keys in Git
- API keys in plaintext logs
- bypassing owner/admin approval for V-level changes
## Codex Online Version Management
Codex uses `/data/aios-gitops/workspace` and `https://cnnetmkt.top/git/aios/aios-v23.git` as the online Git version baseline. Runtime write access uses `codex-gitops-bot`; its token is stored outside Git and referenced as `secret_ref://aios-gitops/runtime/codex-gitops-bot`.
Current production release after this merge: `V1R1C3`. Acceptance-only branches are retained for audit and are not automatically merged into `main`.
## Online Git Sync
Starting in `V1R1C3`, committed API/Codex patch branches are synchronized from the workspace clone into the online Gitea bare repository. Protected production branches remain excluded from API sync.
## Codex and Custom GPT Collaboration Test
`V1R1C3` confirms local Codex and the custom GPT Actions path can independently create patch branches, commit files, sync to online Git, and then merge into the latest baseline version.
@@ -0,0 +1,38 @@
# Codex Online GitOps Version Management
Codex is configured to treat the online Git baseline as the version source of truth.
- Workspace: `/data/aios-gitops/workspace`
- Git web: `https://cnnetmkt.top/git/aios/aios-v23`
- API base: `https://cnnetmkt.top/api`
- OpenAPI schema: `https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
- Runtime token account: `codex-gitops-bot`
- Runtime token ref: `secret_ref://aios-gitops/runtime/codex-gitops-bot`
## Rules
1. Codex updates must use Git patch branches; ZIP upload is deprecated.
2. Every Codex write must become a Git commit.
3. Codex must not push directly to `main`; merge to `main` requires owner/admin approval.
4. V-level release/tag/rollback requests must not bypass owner/admin approval.
5. User custom LLM API keys must stay in runtime secrets and enter Git only as `secret_ref`.
6. Test or acceptance branches can remain as audit evidence, but dummy model/provider configs must not be merged into production `main`.
## Standard Flow
```text
main -> patch/<name> -> commit -> diff review -> smoke test -> owner/admin approval -> merge main -> VxRxCx tag -> baseline/current.json
```
## Current Merge
`V1R1C1` merges the current GitOps/ChatGPT/Codex access changes into the latest online baseline while excluding acceptance dummy model configuration.
## Online Git Sync
Starting in `V1R1C3`, committed API/Codex patch branches are synchronized from the workspace clone into the online Gitea bare repository. Protected production branches remain excluded from API sync.
## Codex and Custom GPT Collaboration Test
`V1R1C3` confirms local Codex and the custom GPT Actions path can independently create patch branches, commit files, sync to online Git, and then merge into the latest baseline version.
@@ -0,0 +1,7 @@
# Custom GPT Collaboration Patch
Timestamp: 20260706T104130Z
Actor: chatgpt-gitops-bot
Created through chatgpt-gitops-bot to simulate the AIOS GitOps 控制台 GPT Actions path: https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai
@@ -0,0 +1,7 @@
# Local Codex Collaboration Patch
Timestamp: 20260706T104130Z
Actor: codex-gitops-bot
Created by local Codex through codex-gitops-bot to validate coordinated online GitOps changes.
+240
View File
@@ -0,0 +1,240 @@
openapi: 3.1.0
info:
title: AIOS GitOps Controlled Access API
version: V1R1C0-chatgpt-actions
description: Controlled ChatGPT Actions API for AIOS online GitOps baseline. Read endpoints are public. Write endpoints require Bearer token and create Git commits on non-main branches.
servers:
- url: https://cnnetmkt.top
security: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: opaque-token
schemas:
BranchRequest:
type: object
required: [branch]
properties:
branch: {type: string, example: patch/chatgpt-actions-demo}
base: {type: string, default: main}
message: {type: string}
FileCommitRequest:
type: object
required: [branch, path, content]
properties:
branch: {type: string}
base: {type: string, default: main}
create_branch: {type: boolean, default: false}
path: {type: string, example: docs/gitops/chatgpt-change.md}
content: {type: string}
message: {type: string}
CommitRequest:
type: object
properties:
branch: {type: string}
paths:
type: array
items: {type: string}
message: {type: string}
PatchRequest:
type: object
required: [patch_name]
properties:
patch_name: {type: string}
branch: {type: string}
base: {type: string, default: main}
risk: {type: string, enum: [low, medium, high]}
description: {type: string}
message: {type: string}
TagRequest:
type: object
required: [version]
properties:
version: {type: string, example: V1R1C1}
message: {type: string}
execute:
type: boolean
description: Owner-approved calls only. Otherwise an approval request commit is created.
RollbackRequest:
type: object
required: [target_version]
properties:
target_version: {type: string}
reason: {type: string}
branch: {type: string}
ModelRequest:
type: object
required: [provider_name, base_url, model_name]
properties:
username: {type: string}
provider_name: {type: string}
base_url: {type: string}
model_name: {type: string}
display_name: {type: string}
api_key_secret_ref:
type: string
description: Preferred. Git stores this secret_ref only.
api_key:
type: string
writeOnly: true
description: Accepted only for runtime secret storage; never committed to Git or logs.
branch: {type: string}
message: {type: string}
ModelPriorityRequest:
type: object
required: [items]
properties:
username: {type: string}
branch: {type: string}
items:
type: array
items:
type: object
properties:
model_id: {type: string}
priority: {type: integer}
enabled: {type: boolean}
fallback_policy: {type: string}
NLRequest:
type: object
required: [text]
properties:
text: {type: string}
branch: {type: string}
paths:
/api/base:
get:
operationId: getBaseline
summary: Read current AIOS baseline
responses: {'200': {description: OK}}
/api/ver:
get:
operationId: listVersions
summary: List GitOps versions
responses: {'200': {description: OK}}
/api/diff:
get:
operationId: getDiff
summary: Read Git diff between refs
parameters:
- in: query
name: from
schema: {type: string, default: HEAD~1}
- in: query
name: to
schema: {type: string, default: HEAD}
responses: {'200': {description: OK}}
/api/patch:
get:
operationId: listPatches
summary: List patch records
responses: {'200': {description: OK}}
post:
operationId: createPatch
summary: Create a patch branch and commit metadata
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/PatchRequest'}
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
/api/model:
get:
operationId: readModelRegistry
summary: Read visible model registry and priority
responses: {'200': {description: OK}}
post:
operationId: registerModel
summary: Register a custom model; API keys are stored as runtime secrets and Git receives secret_ref only
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ModelRequest'}
responses: {'200': {description: Model registered and committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
/api/run:
get:
operationId: getRuntimeStatus
summary: Read runtime status
responses: {'200': {description: OK}}
/api/branch:
post:
operationId: createBranch
summary: Create a non-main branch and commit branch metadata
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BranchRequest'}
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
/api/file:
post:
operationId: commitFile
summary: Write a file and create a Git commit on a non-main branch
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/FileCommitRequest'}
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
/api/commit:
post:
operationId: commitChanges
summary: Commit existing workspace changes on a non-main branch
security: [{bearerAuth: []}]
requestBody:
required: false
content:
application/json:
schema: {$ref: '#/components/schemas/CommitRequest'}
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
/api/tag:
post:
operationId: requestOrCreateTag
summary: Request tag creation; owner execution is required for direct tag
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TagRequest'}
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
/api/rollback:
post:
operationId: requestRollback
summary: Commit a rollback request requiring owner/admin approval
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/RollbackRequest'}
responses: {'200': {description: Rollback request committed}, '401': {description: Unauthorized}}
/api/model-priority:
put:
operationId: setModelPriority
summary: Set model priority and commit policy file
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ModelPriorityRequest'}
responses: {'200': {description: Priority updated and committed}, '401': {description: Unauthorized}}
/api/nl:
post:
operationId: planNaturalLanguageGitOpsAction
summary: Convert natural language to a safe committed plan without executing V-level changes
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/NLRequest'}
responses: {'200': {description: Plan committed}, '401': {description: Unauthorized}}
+17
View File
@@ -0,0 +1,17 @@
# CNNETMKT apex DNS status
generated_at=2026-07-06T16:39:07+08:00
provider=Huawei Cloud DNS
zone=cnnetmkt.top.
record=cnnetmkt.top. A 1.95.164.183
status=created_and_resolved
credential_storage=/etc/letsencrypt/huaweicloud-cnnetmkt.env
secret_policy=not_tracked_not_logged
## verification
- authoritative NS returns 1.95.164.183
- system resolver returns 1.95.164.183
- https://cnnetmkt.top/ returns 302 /portal/login/
- https://cnnetmkt.top/git/ returns 200 AIOS GitOps
- wildcard certificate SAN covers cnnetmkt.top and *.cnnetmkt.top
+53
View File
@@ -0,0 +1,53 @@
# CNNETMKT Auth Primary Status
更新时间:2026-07-06T09:00:06Z
## 结论
`https://cnnetmkt.top/` 是用户主入口。登录授权主域为 `https://auth.cnnetmkt.top`。
`https://auth.h.imruyi.top:8443` 只作为备份/上游兼容入口保留,不应出现在普通用户的主登录跳转链路里。
## 当前链路
1. 用户访问 `https://cnnetmkt.top/`。
2. UCC 返回 `/portal/login/`。
3. UCC 生成 Casdoor 授权地址:
`https://auth.cnnetmkt.top/login/oauth/authorize?...redirect_uri=https%3A%2F%2Fcnnetmkt.top%2Fcasdoor%2Fcallback%2F...`
4. 华为云 Nginx `auth.cnnetmkt.top` server block 反代到现有 Casdoor 上游。
5. Casdoor 允许 `https://cnnetmkt.top/casdoor/callback/` 作为 `admin/aios-portal` 的 OAuth callback。
## 已调整配置
### 华为云 UCC 运行时
- `CASDOOR_PUBLIC_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INTERNAL_BASE_URL=https://auth.cnnetmkt.top`
- `CASDOOR_INVITE_SYNC_BASE_URL=https://auth.cnnetmkt.top`
- `UCC_PUBLIC_BASE_URL=https://cnnetmkt.top`
- `ALLOWED_HOSTS` 增加 `auth.cnnetmkt.top`
- `CSRF_TRUSTED_ORIGINS` 增加 `https://cnnetmkt.top` 和 `https://auth.cnnetmkt.top`
### Casdoor 应用
应用:`admin/aios-portal`
新增/保留 callback:
- `https://cnnetmkt.top/casdoor/callback/`
- `https://cnnetmkt.top/api/auth/callback/casdoor`
- `https://user.cnnetmkt.top/casdoor/callback/`
- `https://portal.cnnetmkt.top/casdoor/callback/`
- `https://portal.h.imruyi.top:8443/casdoor/callback/`(备份)
## 验证命令
```bash
ops/gitops-migration/check_cnnetmkt_auth_primary.sh
```
关键验收点:
- 主链路不再出现 `auth.h.imruyi.top:8443`。
- `user-control-web` 为 `healthy`。
- Casdoor DB `application.redirect_uris` 包含 `https://cnnetmkt.top/casdoor/callback/`。
+83
View File
@@ -0,0 +1,83 @@
# cnnetmkt SSL ingress status
generated_at=2026-07-06T15:48:12+08:00
g_route_status=installed
certbot_status=issued-g-user-portal
apex_dns_status=blocked_until_cnnetmkt.top_has_A_record_to_1.95.164.183
wildcard_status=blocked_until_dns01_provider_credentials_available
backup_dir=/data/aios-gitops/backups/nginx-g-cnnetmkt-20260706-154743
tracked_nginx=/data/aios-gitops/workspace/deploy/nginx/g-cnnetmkt-gitea-server.conf
## validation
### g.cnnetmkt.top
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.24.0 (Ubuntu)
Date: Mon, 06 Jul 2026 07:48:12 GMT
Content-Type: text/html
Content-Length: 154
Location: https://g.cnnetmkt.top/git/
Connection: keep-alive
<title>302 Found
subject=CN = user.cnnetmkt.top
issuer=C = US, O = Let's Encrypt, CN = YE2
notBefore=Jul 6 06:49:39 2026 GMT
notAfter=Oct 4 06:49:38 2026 GMT
X509v3 Subject Alternative Name:
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
### cnnetmkt.top
HTTP/1.1 302 Found
Server: nginx/1.24.0 (Ubuntu)
Date: Mon, 06 Jul 2026 07:48:12 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Location: /portal/login/
X-Request-ID: 7c649abe-a2de-4b3e-87fe-325f4297738f
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
subject=CN = user.cnnetmkt.top
issuer=C = US, O = Let's Encrypt, CN = YE2
notBefore=Jul 6 06:49:39 2026 GMT
notAfter=Oct 4 06:49:38 2026 GMT
X509v3 Subject Alternative Name:
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
### user.cnnetmkt.top
HTTP/1.1 302 Found
Server: nginx/1.24.0 (Ubuntu)
Date: Mon, 06 Jul 2026 07:48:12 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Location: /portal/login/
X-Request-ID: e3d70907-1ab7-456c-a725-7cb60647514d
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
subject=CN = user.cnnetmkt.top
issuer=C = US, O = Let's Encrypt, CN = YE2
notBefore=Jul 6 06:49:39 2026 GMT
notAfter=Oct 4 06:49:38 2026 GMT
X509v3 Subject Alternative Name:
DNS:g.cnnetmkt.top, DNS:portal.cnnetmkt.top, DNS:user.cnnetmkt.top
## certbot
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: user.cnnetmkt.top
Serial Number: 529feebc03b37f2866c25b1465f37e14565
Key Type: ECDSA
Domains: user.cnnetmkt.top g.cnnetmkt.top portal.cnnetmkt.top
Expiry Date: 2026-10-04 06:49:38+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/user.cnnetmkt.top/fullchain.pem
Private Key Path: /etc/letsencrypt/live/user.cnnetmkt.top/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+28
View File
@@ -0,0 +1,28 @@
# CNNETMKT wildcard certificate status
generated_at=2026-07-06T16:24:28+08:00
status=issued
staging_status=ok
dns_provider=huaweicloud
secret_storage=/etc/letsencrypt/huaweicloud-cnnetmkt.env
certificate_live_dir=/etc/letsencrypt/live/cnnetmkt-wildcard
nginx_status=loaded
backup_dir=/data/aios-gitops/backups/nginx-wildcard-20260706-162202
renew_timer=cnnetmkt-wildcard-renew.timer
## certificate
subject=CN = *.cnnetmkt.top
issuer=C = US, O = Let's Encrypt, CN = YE2
notBefore=Jul 6 07:25:55 2026 GMT
notAfter=Oct 4 07:25:54 2026 GMT
X509v3 Subject Alternative Name:
DNS:*.cnnetmkt.top, DNS:cnnetmkt.top
## routes
- https://cnnetmkt.top/
- https://cnnetmkt.top/git/
- https://g.cnnetmkt.top/
- https://g.cnnetmkt.top/git/
- https://user.cnnetmkt.top/
@@ -0,0 +1,27 @@
# AIOS V1R1C1 Codex GitOps Online Release
Date: 2026-07-06T10:21:08Z
## Purpose
Merge the current controlled ChatGPT/Codex GitOps access changes into the latest online baseline and make Git the operating version manager for Codex changes.
## Included
- ChatGPT Actions API adapter and OpenAPI schema.
- Secret-safe model registration using `secret_ref`.
- `codex-gitops-bot` runtime token account with scoped API permissions.
- Codex online GitOps configuration under `config/codex/gitops-online.json`.
- Baseline metadata updated from `V1R1C0` to `V1R1C1`.
## Excluded
- Acceptance dummy model registry entries from `patch/chatgpt-model-acceptance-*`.
- Plaintext API keys or bearer tokens.
## Verification
- `curl https://cnnetmkt.top/api/base` returns 200.
- `curl https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml` returns 200.
- `bash scripts/package_contract_check_v1r1c0.sh` passes.
- `bash scripts/codex_gitops_online_check.sh` passes after tag publication.
@@ -0,0 +1,18 @@
# AIOS V1R1C2 Codex GitOps Online Sync Release
Date: 2026-07-06T10:26:26Z
## Purpose
Make Codex/API writes visible in the online Git service by syncing non-main patch branches from `/data/aios-gitops/workspace` into the Gitea bare repository after each committed write.
## Included
- `active-runtime/app/aios_chatgpt_actions_api.py` now syncs non-main branch commits to the online Git bare repository.
- `POST /api/tag` direct owner flow syncs tags when executed.
- `config/codex/gitops-online.json` records `online_git_sync=true`.
- Live and template systemd service configuration declares `AIOS_GITOPS_BARE_REPO`.
## Policy
`main` remains protected. API/Codex writes still commit to patch/request branches and owner/admin approval remains required for V-level production actions.
@@ -0,0 +1,28 @@
# AIOS V1R1C3 Codex + Custom GPT Collaboration Release
Date: 2026-07-06T10:42:33Z
## Purpose
Validate that local Codex and the AIOS GitOps 控制台 custom GPT can both modify the online GitOps baseline through controlled API writes, then merge both changes into the latest version.
## Custom GPT
https://chatgpt.com/g/g-6a4b7c82dff08191bc980798edea1bac-aios-gitops-kong-zhi-tai
## Merged Inputs
| Actor | Account | Branch | Commit | Online Sync |
|---|---|---|---|---|
| local_codex | codex-gitops-bot | `patch/collab-local-codex-20260706T104130Z` | `a62d4ce` | synced |
| custom_gpt | chatgpt-gitops-bot | `patch/collab-custom-gpt-20260706T104130Z` | `c393931` | synced |
## Result
Both branches were merged into `main`, baseline metadata was refreshed to `V1R1C3`, and the release tag `V1R1C3` was published to the online Git repository.
## Guardrails
- GPT and Codex writes entered Git as patch branch commits.
- Protected `main` was updated only by the release merge step.
- Bot tokens and custom LLM API keys remain outside Git.

Some files were not shown because too many files have changed in this diff Show More