Compare commits

...
Author SHA1 Message Date
aios f3f7db29b1 chore: preserve pending wechat backend workspace change before agent policy patch 2026-07-07 08:03:29 +08:00
aios 09c33eb448 docs(architecture): add remote Codex multi-agent orchestration design 2026-07-07 02:38:03 +08:00
aios f7abb8066b ops(codex): issue V1R1 WeChat backend fix directive 2026-07-07 02:33:50 +08:00
aios efc9ffcdc6 fix(notify): route worker delivery through pluggable backend 2026-07-07 02:31:47 +08:00
aios 05b0d7fb80 feat(notify): add pluggable WeChat send backend 2026-07-07 02:30:00 +08:00
aios 13c2d24e76 fix(notify): make OpenClaw channel name runtime-configurable 2026-07-07 02:20:13 +08:00
aios fbb17597d1 fix(notify): support runtime OpenClaw gateway auth options 2026-07-07 02:13:19 +08:00
aios 7f8261a4b6 fix(notify): use unique index for idempotency migration 2026-07-07 02:02:16 +08:00
aios 839e567060 fix(notify): make message_outbox schema migration backward-compatible 2026-07-07 01:58:22 +08:00
aios db8d9c694d feat(notify): add OpenClaw-backed message_outbox worker 2026-07-07 01:48:39 +08:00
aios 7fc2da56d2 feat(notify): add WebOps message outbox schema helper 2026-07-07 01:42:48 +08:00
aios ae30d716f9 feat(notify): add message_outbox enqueue helper 2026-07-07 01:31:55 +08:00
aios d96086b345 feat(notify): add message_outbox queue schema 2026-07-07 01:24:29 +08:00
aios 6c7928c096 feat(v1r1): document existing wechat runtime integration 2026-07-07 01:20:46 +08:00
aios 5585856cd3 feat(v1r1): add endpoint discovery for personal wechat runtime 2026-07-07 01:17:47 +08:00
aios 9ffcf93fcd feat(deploy): install v1r1 wechat worker service 2026-07-07 01:17:43 +08:00
aios 94db629904 deploy(v1r1): add personal wechat outbound worker service 2026-07-07 01:17:36 +08:00
aios 7f9d664d6b feat(v1r1): add personal wechat outbound worker 2026-07-07 01:14:58 +08:00
aios 90e05be0a9 feat(execution): add V1R1 execution API runtime contract 2026-07-07 01:09:39 +08:00
aios 70fbed1050 fix(deploy): create aios runtime user before starting daemon 2026-07-07 00:36:12 +08:00
aios ab268789ee approval(owner): preapprove V1R1C5 main entry runbook 2026-07-07 00:33:12 +08:00
aios 35e4a5350f feat(governance): add self-governing devops policy 2026-07-07 00:27:28 +08:00
aios dc5dae2a51 deploy(consensus): add systemd unit for consensus controller 2026-07-07 00:25:51 +08:00
aios 73c5b2330e feat(consensus): add multi-agent consensus controller runtime 2026-07-07 00:25:48 +08:00
aios 8aa436043d feat(consensus): add multi-agent production consensus policy 2026-07-07 00:25:42 +08:00
aios 3d4e630582 ops(openclaw): issue autonomous GitOps deployment directive 2026-07-07 00:24:12 +08:00
aios 3b609a8e57 feat(ingress): add event ingestion server 2026-07-07 00:21:40 +08:00
aios 0a4e2b741d deploy: add one-click installation script 2026-07-07 00:21:35 +08:00
aios e68313f95e deploy(systemd): add autonomous daemon service 2026-07-07 00:21:31 +08:00
aios e62ff487ab deploy(systemd): add autonomous daemon env template 2026-07-07 00:21:27 +08:00
aios 680a2f4f2b fix(ci): correct exit status handling in merge gate 2026-07-07 00:14:26 +08:00
aios 8d6d92bcec feat(version): add V1R1C5 baseline proposal 2026-07-07 00:14:19 +08:00
aios 3a5fc56c9d feat(daemon): add autonomous event bus and notification daemon 2026-07-07 00:11:20 +08:00
aios 6327664d22 feat(codex): add low-impact self-optimizing orchestrator config 2026-07-07 00:09:20 +08:00
aios 228d8fedf2 feat(codex): add bounded autonomous production policy 2026-07-07 00:03:33 +08:00
aios f94af50497 feat(skill): add auto-loaded gitops validation skill 2026-07-07 00:02:22 +08:00
aios 2a81d8ced4 ci(codex): add autonomous merge gate executor 2026-07-07 00:02:18 +08:00
aios 759e343075 feat(codex): add autonomous merge gate policy 2026-07-07 00:02:14 +08:00
aios bef2de2c66 feat(ci): add self-healing gitops configuration 2026-07-07 00:01:00 +08:00
aios e8bd867c33 ci(codex): add validation runner script 2026-07-07 00:00:56 +08:00
aios 1953cacebc test(codex): add platform UI acceptance test cases 2026-07-07 00:00:49 +08:00
aios 43be07af73 docs(codex): add GitOps platform UI validation playbook 2026-07-06 23:58:42 +08:00
aios bdea3ef5ba feat(portal): implement platform control plane runtime 2026-07-06 23:51:40 +08:00
aios 76848939e9 feat(portal): add enterprise GitOps console layout 2026-07-06 23:49:10 +08:00
aios f2a08293dd chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:46:50 +08:00
aios 866da79a81 feat(portal): add enterprise GitOps API contract 2026-07-06 23:46:41 +08:00
aios 8a810c2ae8 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:44:51 +08:00
aios c475a8238c feat(portal): add enterprise GitOps UI policy 2026-07-06 23:44:44 +08:00
aios 24c55564a0 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:43:24 +08:00
aios 55a4b32aa6 feat(portal): expand GitOps dashboard UI screens 2026-07-06 23:43:17 +08:00
aios 42c4d11457 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/manifest.json 2026-07-06 23:33:20 +08:00
aios 7be2107ad0 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/app.js 2026-07-06 23:33:18 +08:00
aios 4587c9c46d chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/styles.css 2026-07-06 23:33:15 +08:00
aios aebff86864 chore(chatgpt): update overlays/user-control-center/portal/gitops-dashboard/index.html 2026-07-06 23:33:12 +08:00
aios 76d2453ef3 Create branch for complete GitOps web UI implementation 2026-07-06 23:33:08 +08:00
40 changed files with 4477 additions and 0 deletions
@@ -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
}
}
+59
View File
@@ -0,0 +1,59 @@
{
"name": "aios-autonomous-merge-policy",
"version": "1.0.0",
"mode": "gated-autonomous",
"description": "Defines when Codex CI may mark a patch branch as auto-merge eligible and when main merge must remain blocked.",
"branch_scope": {
"allowed_source_patterns": [
"patch/*"
],
"protected_targets": [
"main"
],
"direct_main_push": false
},
"merge_gate": {
"required_status": [
"validation_passed",
"acceptance_required_passed",
"secret_scan_passed",
"policy_scan_passed",
"indirect_regression_passed",
"gray_deployment_passed"
],
"required_artifacts": [
"reports/codex/validation-report.json",
"reports/codex/merge-candidate.json",
"versions/proposals/V1R1C5-gitops-platform-ui.proposed.json"
],
"required_approval": {
"owner_or_admin": true,
"audit_record_required": true,
"approval_request_type": "release",
"approval_target": "V1R1C5"
}
},
"autonomous_behavior": {
"auto_validate": true,
"auto_self_heal": true,
"auto_create_merge_candidate": true,
"auto_merge_to_main": "only_after_policy_and_owner_approval",
"auto_tag_after_merge": false,
"max_self_heal_iterations": 5
},
"hard_blocks": [
"secret_leak_detected",
"direct_main_write_detected",
"destructive_operation_detected",
"baseline_active_version_changed_before_merge",
"auth_or_gitea_security_config_modified",
"required_test_failed",
"unapproved_high_risk_change"
],
"post_merge": {
"proposed_version": "V1R1C5",
"rollback_target": "V1R1C4",
"tag_creation": "request_only",
"baseline_activation": "owner_approved_action_only"
}
}
@@ -0,0 +1,80 @@
{
"name": "aios-bounded-autonomous-production-policy",
"version": "1.0.0",
"mode": "bounded-lights-out-production",
"description": "Allows unattended CI/self-healing/canary within pre-approved policy boundaries while preserving production safety gates.",
"non_negotiable_controls": {
"direct_main_push": false,
"secrets_in_git": false,
"destructive_operations": "forbidden",
"rollback_execution": "request_or_preapproved_runbook_only",
"tag_creation": "request_or_preapproved_runbook_only",
"baseline_activation": "preapproved_runbook_only",
"force_push": false,
"delete_tags_or_history": false
},
"autonomous_scope": {
"allowed_without_human_intervention": [
"static_validation",
"acceptance_validation",
"secret_scan",
"policy_scan",
"self_healing_patch_on_same_branch",
"merge_candidate_generation",
"gray_deployment_validation",
"failure_report_generation",
"rollback_request_generation",
"baseline_proposal_generation"
],
"requires_preapproved_runbook": [
"merge_to_main",
"create_release_tag",
"activate_new_baseline",
"production_runtime_change",
"production_rollback_execution"
],
"always_blocked": [
"force_push",
"purge_baseline",
"delete_version_history",
"delete_audit_records",
"commit_real_secret",
"disable_auth_or_gitea_protection"
]
},
"preapproval_contract": {
"path_pattern": "approvals/preapproved/*.json",
"required_fields": [
"approval_id",
"approved_by_role",
"target_branch",
"target_version",
"expires_at",
"allowed_actions",
"risk_acceptance",
"rollback_target"
],
"required_values": {
"approved_by_role": ["owner", "admin"],
"target_version": "V1R1C5",
"rollback_target": "V1R1C4"
}
},
"promotion_pipeline": [
"validate",
"self_heal_until_pass",
"generate_merge_candidate",
"gray_deploy_validate",
"check_preapproval",
"merge_or_request_approval",
"propose_or_activate_baseline",
"monitor",
"auto_generate_rollback_request_on_failure"
],
"slo_gates": {
"gray_deployment_required": true,
"error_budget_policy": "block_if_unknown",
"observability_required": true,
"post_merge_monitoring_window": "30m"
}
}
@@ -0,0 +1,88 @@
{
"name": "aios-multi-agent-consensus-policy",
"version": "1.0.0",
"mode": "bounded-autonomous-consensus",
"description": "Production-safe consensus policy for OpenClaw, Codex and ChatGPT agents supervising AIOS GitOps changes.",
"agents": {
"openclaw": {
"role": "supervisor",
"weight": 2,
"required_for_high_risk": true,
"responsibility": [
"execution supervision",
"policy compliance",
"failure containment",
"owner-facing status certification"
]
},
"codex": {
"role": "validator_and_repairer",
"weight": 2,
"required_for_high_risk": true,
"responsibility": [
"static validation",
"acceptance validation",
"self-healing patch loop",
"merge candidate generation"
]
},
"chatgpt": {
"role": "operator_and_summary_agent",
"weight": 1,
"required_for_high_risk": false,
"responsibility": [
"change explanation",
"owner notification summary",
"repair proposal generation",
"runbook authoring"
]
}
},
"decision_rules": {
"low_risk": {
"minimum_score": 2,
"required_agents": ["codex"],
"allowed_outcome": "auto_merge_candidate"
},
"medium_risk": {
"minimum_score": 3,
"required_agents": ["codex", "openclaw"],
"allowed_outcome": "auto_merge_candidate_after_gray"
},
"high_risk": {
"minimum_score": 5,
"required_agents": ["codex", "openclaw"],
"allowed_outcome": "production_request_or_preapproved_execution"
}
},
"hard_blocks": [
"credential_material_detected",
"direct_main_push_detected",
"destructive_history_operation",
"auth_protection_disabled",
"gitea_protection_disabled",
"baseline_activation_without_approval",
"owner_notification_failed_for_main_change",
"required_acceptance_test_failed"
],
"required_artifacts": {
"validation": "reports/codex/validation-report.json",
"openclaw": "reports/openclaw/final-supervision-report.json",
"consensus": "reports/consensus/decision.json",
"merge_candidate": "reports/codex/merge-candidate.json",
"baseline_proposal": "versions/proposals/V1R1C5-gitops-platform-ui.proposed.json"
},
"production_safety": {
"direct_main_push": false,
"tag_creation": "request_or_preapproved_only",
"baseline_activation": "request_or_preapproved_only",
"rollback_execution": "request_or_preapproved_only",
"owner_notification": "mandatory_for_main_version_change",
"web_ui_audit_record": "mandatory"
},
"outputs": {
"approved": "merge_candidate_ready",
"rejected": "failure_report_required",
"blocked": "owner_review_required"
}
}
@@ -0,0 +1,99 @@
{
"name": "aios-self-governing-devops-policy",
"version": "1.0.0",
"mode": "bounded-self-governing",
"description": "Self-governing DevOps policy for AIOS GitOps with dynamic agent weights, drift detection, guarded rollback requests, policy runtime and skill evolution.",
"non_negotiable_controls": {
"direct_main_push": false,
"all_writes_require_git_commit": true,
"real_credentials_in_git": false,
"disable_auth_protection": false,
"delete_audit_or_version_history": false,
"production_execution_without_policy": false
},
"governance_loop": [
"observe_events",
"evaluate_policy",
"score_agents",
"detect_consensus_drift",
"render_decision_graph",
"run_validation_or_repair",
"generate_candidate_or_request",
"notify_owner",
"persist_audit",
"learn_from_outcome",
"propose_skill_update"
],
"agent_learning": {
"enabled": true,
"weight_bounds": {
"openclaw": [1, 4],
"codex": [1, 4],
"chatgpt": [1, 3]
},
"initial_weights": {
"openclaw": 2,
"codex": 2,
"chatgpt": 1
},
"signals": {
"validation_accuracy": 0.35,
"repair_success": 0.25,
"false_positive_rate": -0.15,
"owner_override": -0.2,
"post_merge_stability": 0.25
},
"update_policy": "bounded_exponential_smoothing",
"write_target": "runtime/governance/agent-weights.json"
},
"drift_detection": {
"enabled": true,
"thresholds": {
"consensus_score_drop": 2,
"agent_disagreement_ratio": 0.5,
"repeated_failure_count": 3,
"owner_override_count": 2
},
"on_drift": "block_production_and_request_owner_review"
},
"policy_runtime": {
"required_checks": [
"no_direct_main_write",
"no_real_credentials",
"no_destructive_history_change",
"acceptance_tests_pass",
"owner_notice_for_main_change",
"rollback_target_defined",
"baseline_proposal_exists"
],
"decision_outputs": [
"allow_candidate",
"conditional_candidate",
"request_owner_review",
"block"
]
},
"guarded_rollback": {
"mode": "request_first",
"automatic_action": "create_rollback_request_and_notify_owner",
"execution": "requires_preapproved_runbook_or_owner_action",
"default_target": "V1R1C4"
},
"skill_evolution": {
"enabled": true,
"mode": "propose_only",
"sources": [
"validation_failures",
"owner_feedback",
"post_merge_incidents",
"repeated_manual_repairs"
],
"output_dir": "skills/proposals",
"activation": "merge_after_acceptance"
},
"owner_control": {
"channels": ["web_ui", "wechat_or_wecom"],
"actions": ["audit", "approve", "reject", "request_change", "request_rollback"],
"main_version_change_notice": "mandatory"
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"name": "self-healing-gitops",
"version": "1.0.0",
"mode": "codex-driven-ci",
"description": "Defines how Codex + CI loop detects failures, generates repair plan, and re-runs validation.",
"workflow": {
"trigger": ["push", "pull_request"],
"validation_script": "scripts/validate-gitops-platform-ui.sh",
"failure_handling": {
"create_failure_report": true,
"report_path": "reports/codex/failure.json",
"notify": "codex-gpt",
"auto_repair": false,
"require_human_or_gpt_review": true
},
"success_handling": {
"allow_merge_candidate": true,
"propose_baseline_update": "V1R1C5"
}
},
"self_healing_loop": {
"steps": [
"run_validation",
"collect_failures",
"send_to_gpt",
"apply_patch_to_branch",
"rerun_validation",
"repeat_until_pass"
],
"max_iterations": 5,
"stop_conditions": [
"PASS",
"critical_security_failure",
"manual_override"
]
},
"constraints": {
"no_main_write": true,
"no_secret_exposure": true,
"approval_required_for_release": true
}
}
@@ -0,0 +1,135 @@
{
"name": "aios-self-optimizing-orchestrator",
"version": "1.0.0",
"mode": "hybrid-event-plus-reconciliation",
"description": "Low-impact orchestrator for ChatGPT/Codex self-optimization, version-change notification, audit, and owner action routing.",
"design_principles": {
"execution_layer_impact": "minimal",
"writes": "git_patch_only",
"direct_main_push": false,
"secrets_in_git": false,
"production_high_risk_actions": "approval_or_preapproved_runbook_only",
"user_impact": "prefer_async_sidecar_and_read_only_observation"
},
"triggers": {
"primary": {
"type": "event_driven",
"sources": [
"git_push",
"tag_created",
"baseline_changed",
"patch_created",
"validation_failed",
"validation_passed",
"merge_candidate_created",
"runtime_health_degraded"
],
"target": "orchestrator_queue"
},
"fallback": {
"type": "scheduled_reconciliation",
"intervals": {
"codex_branch_scan": "*/15 * * * *",
"chatgpt_patch_scan": "*/15 * * * *",
"main_version_scan": "*/5 * * * *",
"notification_retry_scan": "*/10 * * * *",
"daily_owner_digest": "0 9 * * *"
},
"purpose": "Catch missed events, reconcile web UI records, and retry failed notifications."
}
},
"agents": {
"codex": {
"responsibility": [
"code validation",
"static/security/policy tests",
"self-healing patch loop",
"merge candidate report"
],
"trigger": "event_and_schedule",
"skill": "skills/gitops-platform-validation/SKILL.md"
},
"chatgpt": {
"responsibility": [
"operator summary",
"failure triage",
"GitOps patch authoring",
"owner-facing change explanation"
],
"trigger": "event_and_schedule",
"skill": "skills/gitops-owner-notification/SKILL.md"
}
},
"change_summary_required_fields": [
"event_id",
"branch",
"from_rev",
"to_rev",
"from_version",
"to_version",
"elapsed_since_previous_main_change",
"changed_features",
"code_lines_added",
"code_lines_deleted",
"config_lines_added",
"config_lines_deleted",
"test_status",
"risk",
"approval_status",
"rollback_target",
"web_ui_audit_url"
],
"owner_channels": {
"wechat": {
"enabled": true,
"provider": "wechat_or_wecom",
"webhook_secret_ref": "secret_ref://aios/notifications/owner-wechat-webhook",
"signing_secret_ref": "secret_ref://aios/notifications/owner-action-signing-key",
"message_types": [
"main_version_changed",
"merge_candidate_ready",
"validation_failed",
"rollback_request_created",
"approval_required",
"daily_digest"
],
"actions": [
"view_audit",
"approve_preapproved_runbook",
"request_rollback",
"request_change",
"reject_release"
]
},
"web_ui": {
"enabled": true,
"audit_route": "/portal/me/#gitops-audit",
"owner_action_route": "/portal/me/#gitops-owner-actions",
"record_store": "runtime/gitops/events/*.json"
}
},
"version_change_policy": {
"main_change_notification": "mandatory",
"notify_owner_on_every_main_version_change": true,
"include_diff_stats": true,
"include_elapsed_time_since_previous_main_change": true,
"include_function_summary": true,
"write_web_ui_record": true,
"retry_until_delivered_or_acknowledged": true
},
"owner_action_policy": {
"wechat_actions": "signed_short_link_or_command_token",
"web_actions": "login_required_portal_action",
"all_actions_audited": true,
"high_risk_actions": "request_only_unless_preapproved_runbook",
"rollback": "create_request_by_default",
"change_request": "create_patch_or_plan_by_default"
},
"minimal_execution_layer_changes": [
"Add sidecar orchestrator queue consumer.",
"Add event writer for GitOps version changes.",
"Add notification sender using secret_ref only.",
"Add Web UI audit/action records reading event files.",
"Keep existing /api/base /api/ver /api/diff /api/patch /api/model /api/run stable."
]
}
@@ -0,0 +1,51 @@
{
"name": "v1r1-execution-api-runtime",
"version": "1.0.0",
"mode": "api_runtime_not_filesystem",
"baseline": "V1R1C4",
"target_proposal": "V1R1C5",
"description": "Execution API runtime contract for AIOS Xiamayiti self-evolution loop. All production execution requests must go through the V1R1 runtime API rather than direct filesystem or protected credential access.",
"security": {
"direct_runtime_secret_access": false,
"direct_filesystem_production_write": false,
"all_execution_audited": true,
"owner_control_required_for_high_risk": true,
"direct_main_push": false
},
"api": {
"submit_execution_request": "POST /api/v1r1/execution/request",
"get_execution_status": "GET /api/v1r1/execution/status/{request_id}",
"append_audit": "POST /api/v1r1/audit/append",
"send_owner_notification": "POST /api/v1r1/notification/owner",
"submit_owner_action": "POST /api/v1r1/owner/action"
},
"allowed_request_types": [
"audit_record",
"owner_notification",
"evolution_finding",
"evolution_plan",
"validation_run",
"merge_candidate",
"release_request",
"rollback_request",
"baseline_activation_request"
],
"blocked_request_types": [
"write_protected_credential",
"force_push",
"delete_history",
"delete_audit",
"disable_auth",
"disable_gitea_protection",
"direct_main_push"
],
"execution_states": [
"received",
"policy_checked",
"accepted",
"queued",
"executed_as_request",
"blocked",
"failed"
]
}
@@ -0,0 +1,41 @@
{
"name": "v1r1-existing-wechat-runtime-integration",
"version": "1.0.0",
"baseline": "V1R1C4",
"description": "Integrates AIOS self-evolution notifications with the already deployed Xiamayiti/V1R1 personal WeChat runtime instead of creating a duplicate gateway.",
"detected_runtime_processes": [
{
"process_hint": "/data/agent/services/ucc-wechat-qr-adapter/adapter.py",
"role": "user-control-center personal WeChat QR adapter",
"integration_priority": 1
},
{
"process_hint": "scripts/wechat_notify_worker.py",
"role": "existing personal WeChat notification worker",
"integration_priority": 2
}
],
"preferred_integration": {
"mode": "existing_worker_queue_bridge",
"runtime_env": "AIOS_V1R1_WECHAT_BRIDGE_OUTBOX",
"behavior": "Write normalized owner notification JSON into the existing V1R1 WeChat worker queue directory. The existing V1R1 worker is responsible for actual personal WeChat delivery."
},
"fallback_integration": {
"mode": "http_endpoint",
"runtime_env": "AIOS_V1R1_WECHAT_OUTBOUND_URL",
"behavior": "POST normalized owner notification JSON to a runtime-provided V1R1 notification API endpoint."
},
"rules": {
"do_not_duplicate_wechat_gateway": true,
"do_not_store_credentials_in_git": true,
"do_not_access_runtime_secrets_directly": true,
"web_ui_and_wechat_share_event_id": true,
"delivery_status_must_be_audited": true
},
"operator_discovery_commands": [
"ps aux | grep -E 'ucc-wechat|wechat_notify'",
"pwdx <pid>",
"tr '\\0' '\\n' < /proc/<pid>/environ",
"grep -R \"queue\|outbox\|notify\|send\|wechat\" -n /data/agent /data/aios-gitops/workspace/scripts 2>/dev/null | head -100"
]
}
+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
);
+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"
@@ -0,0 +1,16 @@
# AIOS Autonomous GitOps Daemon runtime environment template
# Copy to /etc/aios/aios-autonomous-daemon.env during deployment.
# This template stores only paths and secret_ref identifiers. Runtime values are injected by the host secret manager.
AIOS_REPO_ROOT=/data/aios-gitops/workspace
AIOS_EVENT_DIR=/data/aios-gitops/runtime/events
AIOS_AUDIT_DIR=/data/aios-gitops/runtime/audit
AIOS_REQUEST_DIR=/data/aios-gitops/runtime/requests
AIOS_REPORT_DIR=/data/aios-gitops/runtime/reports/codex
AIOS_PREAPPROVAL_DIR=/data/aios-gitops/runtime/approvals/preapproved
AIOS_DAEMON_INTERVAL=30
AIOS_INGRESS_BIND=127.0.0.1
AIOS_INGRESS_PORT=8787
AIOS_OWNER_CHANNEL_REF=secret_ref://aios/notifications/owner-wechat-endpoint
AIOS_OWNER_ACTION_SIGN_REF=secret_ref://aios/notifications/owner-action-signing
AIOS_INGRESS_SIGN_REF=secret_ref://aios/notifications/inbound-event-signing
@@ -0,0 +1,21 @@
[Unit]
Description=AIOS Autonomous GitOps Daemon
After=network.target
[Service]
Type=simple
User=aios
WorkingDirectory=/data/aios-gitops/workspace
EnvironmentFile=/etc/aios/aios-autonomous-daemon.env
ExecStart=/usr/bin/python3 services/aios-autonomous-daemon/daemon.py run
Restart=always
RestartSec=5
# Hardening (minimal impact)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,18 @@
[Unit]
Description=AIOS Multi-Agent Consensus Controller
After=network.target
[Service]
Type=oneshot
User=aios
WorkingDirectory=/data/aios-gitops/workspace
ExecStart=/usr/bin/python3 services/aios-consensus-controller/controller.py
# Hardening (non-invasive)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,22 @@
[Unit]
Description=AIOS V1R1 Personal WeChat Outbound Worker
After=network.target aios-autonomous-daemon.service
Wants=aios-autonomous-daemon.service
[Service]
Type=simple
User=aios
WorkingDirectory=/data/aios-gitops/workspace
EnvironmentFile=/etc/aios/aios-autonomous-daemon.env
ExecStart=/usr/bin/python3 services/aios-v1r1-wechat-outbound-worker/worker.py
Restart=always
RestartSec=5
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,210 @@
# AIOS 远程 Codex 多 Agent 统筹架构
## 1. 核心定位
当前 AIOS / 虾马一体系统的自我进化,不再假设服务器上存在 Codex CLI。
真实运行模型是:
- Windows 11 Codex App:总控 / 统筹 / 拆分任务 / 监督多个子 agent;
- 服务器 OpenClaw:生产运行侧消息、微信、执行与通道能力;
- 服务器 Hermes Agent:运行观测、事件、消息/用户交互、反馈归集;
- 服务器 AIOS Autonomous Daemon:GitOps event loop、validation、audit、request;
- GitOps 仓库:唯一可审计状态同步面;
- WebOps / Portal:owner 审计、审批、回滚、变更入口。
## 2. 架构图
```text
Windows 11 Codex App
├─ Planner Agent
├─ Code Patch Agent
├─ Validation Agent
├─ WeChat/UCC Integration Agent
├─ WebOps UI Agent
└─ Supervisor Agent
↓ git / ssh / reports
GitOps Repository: patch/web-ui-complete-gitops-dashboard-20260706
↓ pull / event
AIOS Server Runtime
├─ aios-autonomous-daemon
├─ OpenClaw Agent / Gateway
├─ Hermes Agent
├─ UCC WeChat Adapter
├─ WebOps / message_outbox
└─ audit / reports / requests
↓
Owner WebOps + 个人微信
```
## 3. 分工
### Windows Codex App
Windows Codex 是统筹器,不是服务器守护进程。
职责:
1. 读取 GitOps 指令与当前 branch;
2. 启动多个子 agent 并行处理;
3. 生成代码/配置/文档 patch;
4. push 到同一 GitOps patch 分支;
5. 读取服务器回传的验证结果;
6. 对失败项继续修复;
7. 输出 merge candidate / blocked reason。
### OpenClaw Agent
职责:
1. 维护微信通道、message send、用户通道状态;
2. 提供个人微信实际执行能力;
3. 接收 message_outbox worker 或 UCC Adapter 的调用;
4. 输出 channel status / delivery result;
5. 不直接接收业务模块调用。
### Hermes Agent
职责:
1. 汇总系统运行事件;
2. 处理用户/消息交互流;
3. 将微信或 WebOps 反馈转为系统事件;
4. 协助 self-evolution loop 判断用户体验和业务状态;
5. 输出观测报告给 GitOps / Codex。
### AIOS Autonomous Daemon
职责:
1. 处理 runtime events;
2. 触发 validation / merge gate;
3. 写 audit / request / report;
4. 不直接改 runtime secrets;
5. 不直接调用微信底层适配器。
## 4. 并行子 Agent 模型
Windows Codex 可启动以下子 agent:
| 子 Agent | 任务 |
|---|---|
| planner | 读取 directive,拆分任务,确认风险 |
| server-inspector | 解析服务器命令输出,定位 runtime 状态 |
| code-patcher | 修改 GitOps 仓库代码 |
| test-author | 补验收用例和验证脚本 |
| wechat-integrator | 处理 message_outbox / UCC / OpenClaw 适配 |
| webops-ui | 补 WebOps 页面和 API contract |
| supervisor | 汇总 PASS/FAIL/BLOCKED |
## 5. 自动运行机制
自动运行不代表 Codex 在服务器常驻,而是通过以下 loop 运行:
```text
server event / owner request
↓
GitOps directive / report
↓
Windows Codex pulls branch
↓
Codex child agents patch code/config
↓
git push
↓
server git pull + run validation
↓
reports written
↓
Codex reads reports
↓
repeat until PASS/BLOCKED
```
## 6. 通信通道
### Git
主要状态同步面:
- directives;
- patches;
- code/config;
- docs;
- acceptance tests;
- version proposals。
### SSH / Operator Logs
服务器运行结果通过 SSH 或人工粘贴日志回传给 Codex/GPT。
### Runtime Reports
服务器写入:
```text
reports/codex/*.json
reports/openclaw/*.json
reports/hermes/*.json
reports/consensus/*.json
runtime/gitops/audit/*.json
runtime/gitops/requests/*.json
```
## 7. 生产安全边界
仍然禁止:
- 直接 main push;
- force push;
- 写 runtime secrets;
- 删除 audit / version history;
- 业务模块直接调用 OpenClaw;
- 业务模块直接调用 UCC WeChat adapter;
- 绕过 message_outbox;
- 伪造 sent 状态。
## 8. 微信链路标准
生产标准链路:
```text
业务/系统事件
→ POST /api/v1/wechat/outbox 或写 message_outbox
→ wechat_notify_worker
→ wechat_send_backend
→ ucc-adapter / openclaw-channel
→ 个人微信
→ sent/failed/retry/audit
```
当前优先后端:
```text
ucc-adapter
```
OpenClaw channel 仅作为可发现的可选后端,不得硬编码 `openclaw-weixin`。
## 9. 自我进化目标
自我进化不是“代码自己改代码”,而是虾马一体系统整体优化:
1. 业务流程优化;
2. 运行健康优化;
3. 模型路由优化;
4. 通知和审批体验优化;
5. WebOps 可观测性优化;
6. 必要时生成代码/配置 patch。
## 10. 进入自动运行的判据
满足以下条件即认为进入自动运行:
1. `aios-autonomous-daemon` running;
2. OpenClaw/Hermes 进程 running;
3. message_outbox 可写;
4. Windows Codex 监控目标 branch;
5. Codex 可读取 directive 并 push patch;
6. 服务器可运行验收命令并写 reports;
7. owner 可在 WebOps 或微信收到关键进展。
@@ -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。
@@ -0,0 +1,351 @@
# Codex 指令:修复 V1R1 微信主动发送后端
Directive ID: `CODEX-FIX-V1R1-WECHAT-BACKEND-20260707`
Branch: `patch/web-ui-complete-gitops-dashboard-20260706`
Base: `main`
Current baseline: `V1R1C4`
## 1. 当前阻塞
当前微信主动发送链路已改为:
```text
message_outbox
→ wechat_notify_worker.py
→ wechat_send_backend.py
→ openclaw-channel 或 ucc-adapter
```
实际验证结果:
```json
{
"backend": "unavailable",
"reason": "wechat outbound backend unavailable: openclaw channel not registered; ucc adapter send endpoint not detected"
}
```
已确认:
1. OpenClaw Gateway 运行中,但 `openclaw channels list --json` 只有 `{ "chat": {} }`,没有 wechat/weixin channel。
2. 当前生产微信能力优先出口应是 `ucc-wechat-qr-adapter`,不是 OpenClaw channel。
3. 当前系统存在进程:`/data/agent/services/ucc-wechat-qr-adapter/adapter.py`。
4. 当前服务配置:`/etc/aios/ucc-wechat-qr-adapter.env`。
5. 当前监听端口:`AIOS_WECHAT_QR_ADAPTER_PORT=18800`。
6. `wechat_send_backend.py` 没有探测到 UCC adapter 的可用发送接口。
## 2. 修复目标
Codex 必须补齐 V1R1 微信主动发送后端,使 `message_outbox → worker → ucc-adapter → 个人微信` 成为主路径。
不得让业务模块直接调用:
- `openclaw message send`;
- OpenClaw Gateway `127.0.0.1:18789`;
- 微信适配器底层进程;
- 本地 secrets 文件。
业务/外部系统只允许写入:
```text
message_outbox
```
或调用未来统一 API:
```text
POST /api/v1/wechat/outbox
```
## 3. Codex 必须先执行的检查
只执行命令,不输出 token。
### 3.1 检查 adapter 路由和函数
```bash
sudo grep -RniE '@.*route|@app\.(get|post)|FastAPI|Flask|send|message|notify|outbound|wechat|target' \
/data/agent/services/ucc-wechat-qr-adapter/adapter.py \
| sed -E 's/(token|password|secret|key|auth)([=: ][^ ]+)/\1=***MASKED***/Ig' \
| head -n 200
```
### 3.2 检查 adapter 健康接口
```bash
for p in \
/health \
/status \
/api/health \
/api/internal/health \
/api/internal/wechat/status \
/api/internal/wechat/send
do
echo "=== $p ==="
curl -sS "http://127.0.0.1:18800$p" || true
echo
done
```
### 3.3 如需 token,安全读取但不得打印
```bash
ADAPTER_TOKEN="$(sudo awk -F= '/^AIOS_WECHAT_QR_ADAPTER_TOKEN=/{print $2}' /etc/aios/ucc-wechat-qr-adapter.env)"
for p in \
/health \
/status \
/api/health \
/api/internal/health \
/api/internal/wechat/status
do
echo "=== $p ==="
curl -sS \
-H "Authorization: Bearer ${ADAPTER_TOKEN}" \
-H "X-AIOS-Token: ${ADAPTER_TOKEN}" \
"http://127.0.0.1:18800$p" || true
echo
done
```
## 4. 必须实现
### 4.1 如果 adapter 已有发送接口
若 `adapter.py` 已经存在发送接口,Codex 应:
1. 确认真实路径、请求格式、认证头;
2. 修改 `scripts/wechat_send_backend.py` 的 `ucc-adapter` backend 调用该真实接口;
3. 保证 token 仅从运行时环境或 env 文件读取,不进入 Git;
4. 更新验收文档。
### 4.2 如果 adapter 没有发送接口
Codex 应在 `/data/agent/services/ucc-wechat-qr-adapter/adapter.py` 对应源码路径中补内部接口,或在 GitOps 仓库新增 overlay/patch 指令,目标接口为:
```http
POST /api/internal/wechat/send
```
请求体:
```json
{
"target": "...",
"content": "...",
"account_id": "...",
"message_id": "...",
"metadata": {}
}
```
认证:
- 使用 `/etc/aios/ucc-wechat-qr-adapter.env` 中的 `AIOS_WECHAT_QR_ADAPTER_TOKEN`;
- 支持 `Authorization: Bearer <token>`;
- 支持 `X-AIOS-Token: <token>`;
- 不允许打印 token。
返回:
成功:
```json
{
"ok": true,
"provider_message_id": "...",
"status": "sent"
}
```
如果底层暂时没有真实发送函数,必须返回:
```json
{
"ok": false,
"code": "adapter_send_not_supported"
}
```
不得假装 `sent`。
## 5. 修改范围
允许修改或新增:
- `scripts/wechat_send_backend.py`
- `scripts/wechat_notify_worker.py`
- `scripts/enqueue_wechat_notification.py`
- `scripts/message_outbox_schema.py`
- `docs/ops/*wechat*`
- `tests/acceptance/*wechat*`
- UCC adapter 的 overlay/patch 文件
- systemd/env template 中与 worker 配置相关的字段
禁止修改:
- runtime secrets 明文;
- 个人微信原始 token;
- Gitea 安全配置;
- Casdoor/auth 配置;
- baseline active version;
- 删除 audit/version/history 的逻辑。
## 6. 必须新增或更新验收用例
### TC-WX-BE-01
OpenClaw channel list 只有 `{ "chat": {} }` 时,不得调用 `openclaw-weixin`。
预期:
```text
wechat_send_backend 选择 ucc-adapter 或明确 unavailable
last_error 不包含 Unknown channel: openclaw-weixin
```
### TC-WX-BE-02
UCC adapter 存活时自动选择 `ucc-adapter`。
预期:
```text
python3 scripts/wechat_send_backend.py
→ backend=ucc-adapter
```
### TC-WX-BE-03
outbox 测试消息可进入 worker。
预期:
```text
message_outbox.status 从 queued 变为 dispatching,再变为 sent 或 failed
```
### TC-WX-BE-04
如果 adapter 支持真实发送,owner 微信收到测试消息。
预期:
```text
message_outbox.status=sent
audit_logs.action=wechat_outbound_sent
```
### TC-WX-BE-05
如果 adapter 不支持发送,消息不得丢失。
预期:
```text
message_outbox.status=queued 或 failed
last_error 包含 adapter_send_not_supported
audit_logs.action=wechat_outbound_failed
```
### TC-WX-SEC-01
日志不得泄露:
- token;
- password;
- secret;
- cookie;
- 个人微信原始 ID 给普通用户;
- route proof;
- runtime secrets 路径。
## 7. 最小验收命令
### 7.1 检查 backend
```bash
cd /data/aios-gitops/workspace
python3 scripts/wechat_send_backend.py
```
### 7.2 入队测试
```bash
python3 scripts/enqueue_wechat_notification.py \
--recipient owner \
--recipient-role owner \
--message-type system_report \
--priority high \
--title "AIOS 测试通知" \
--content "这是通过 message_outbox → ucc-adapter 发送的主动微信通知。"
```
### 7.3 执行 worker
```bash
python3 scripts/wechat_notify_worker.py --once
```
### 7.4 查询状态
```bash
python3 - <<'PY'
import sqlite3
con=sqlite3.connect('/data/aios-gitops/workspace/data/webops.sqlite3')
con.row_factory=sqlite3.Row
print('message_outbox')
for r in con.execute('select id,status,retry_count,last_error,sent_at from message_outbox order by created_at desc limit 5'):
print(dict(r))
print('audit_logs')
for r in con.execute('select action,target,detail,created_at from audit_logs order by created_at desc limit 5'):
print(dict(r))
PY
```
## 8. 输出要求
Codex 完成后必须输出:
```text
Status: PASS / FAIL / BLOCKED
Branch: patch/web-ui-complete-gitops-dashboard-20260706
Files Changed: ...
Backend Selected: openclaw-channel / ucc-adapter / unavailable
Adapter Send Endpoint: detected / added / unsupported
Message Outbox Status: sent / queued / failed
Audit Status: written / missing
Owner WeChat Received: yes / no / not testable
Blocked Reason: ...
Next Action: ...
```
## 9. 当前期望结果
最优结果:
```text
backend=ucc-adapter
message_outbox.status=sent
owner 微信收到 AIOS 测试通知
```
可接受结果:
```text
backend=ucc-adapter
message_outbox.status=failed
last_error=adapter_send_not_supported
```
不可接受结果:
```text
继续报 Unknown channel: openclaw-weixin
假装 sent
日志打印 token
业务模块直接调用 OpenClaw
```
@@ -0,0 +1,154 @@
# OpenClaw 指令:AIOS Autonomous GitOps 静默优化与部署监督
Directive ID: `OPENCLAW-AIOS-AUTONOMOUS-GITOPS-DEPLOY-20260707`
Branch: `patch/web-ui-complete-gitops-dashboard-20260706`
Base: `main`
Current baseline: `V1R1C4`
Proposed baseline after approved merge: `V1R1C5`
## 1. 执行目的
OpenClaw 负责接管本分支的执行与监督,目标是在不影响现有执行层、不破坏用户使用体验的前提下,完成 AIOS Autonomous GitOps Runtime 的验证、静默优化、部署准备、Owner 通报与合并候选推进。
本次改动的目标不是直接绕过 main 保护,而是让系统形成:
- 自动 CI 验证;
- Codex 自愈修复;
- event bus + daemon 自动执行;
- 微信/网页 Owner 审计与操作通道;
- V1R1C5 基线提案;
- 失败回流自定义 GPT 修复;
- 通过后生成 merge candidate。
## 2. OpenClaw 角色
OpenClaw 是本任务的执行监督代理,职责如下:
1. 读取并监督当前分支与 main 的差异。
2. 按验收测试执行静态验证、安全验证、策略验证、灰度验证。
3. 发现失败后,生成结构化 failure report。
4. 将失败项交给自定义 GPT 继续修复同一 patch 分支。
5. 修复后重复验证,直到 PASS 或触发 hard block。
6. 生成 merge candidate,但不得直接绕过 main 保护。
7. 监督 Owner 通知记录:每个 main 版本变更必须有摘要通报与 Web UI 记录。
## 3. 禁止事项
OpenClaw 不得执行以下操作:
- 不得直接 force push;
- 不得删除版本历史、tag、audit 记录;
- 不得写入真实凭据;
- 不得关闭 auth、Gitea 登录保护或 owner sync;
- 不得在未通过验收前合并 main;
- 不得在未有审批或预授权 runbook 前执行生产 tag、baseline activation 或 rollback execution。
## 4. 必须执行的命令
```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
bash scripts/autonomous-merge-gate.sh patch/web-ui-complete-gitops-dashboard-20260706 main
```
若部署验证需要启动本地 runtime:
```bash
bash deploy/install/install.sh
python3 services/aios-autonomous-daemon/daemon.py emit --type patch_created --branch patch/web-ui-complete-gitops-dashboard-20260706 --base main
python3 services/aios-autonomous-daemon/daemon.py run --once
```
## 5. 必须验证的直接修改
OpenClaw 必须检查以下路径:
- `overlays/user-control-center/portal/gitops-dashboard/`
- `services/aios-autonomous-daemon/`
- `services/aios-event-ingress/`
- `deploy/systemd/`
- `deploy/install/`
- `config/codex/`
- `scripts/`
- `tests/acceptance/`
- `versions/proposals/V1R1C5-gitops-platform-ui.proposed.json`
- `skills/`
- `docs/codex/`
## 6. 必须验证的间接影响
OpenClaw 必须确认以下内容未被破坏:
- `/api/base`、`/api/ver`、`/api/diff`、`/api/patch`、`/api/model`、`/api/run`;
- `/base`、`/ver`、`/diff`、`/patch`、`/model`、`/run` 登录保护;
- Portal 登录入口 `/portal/me/#gitops`;
- Gitea require sign-in view;
- owner password sync;
- active baseline 仍为 `V1R1C4`,未提前激活 `V1R1C5`。
## 7. Owner 通报要求
每个 main 版本变更、merge candidate、validation failure、rollback request、approval required 事件都必须形成:
1. Owner 消息摘要;
2. Web UI 审计记录;
3. 可追踪 event_id;
4. 变更功能摘要;
5. 代码量与配置量统计;
6. 与上一次主线变化的间隔;
7. 测试状态;
8. rollback target。
## 8. OpenClaw 进度监督格式
OpenClaw 每次监督输出必须包含:
```text
Status: PASS / FAIL / BLOCKED / RUNNING
Branch: patch/web-ui-complete-gitops-dashboard-20260706
Base: main
Current Rev: <rev>
Validation: <summary>
Gray Test: <summary>
Owner Notice: <summary>
Merge Candidate: yes/no
Blocked Reason: <reason or none>
Next Action: <action>
```
## 9. 失败回流规则
若失败,OpenClaw 必须写入 failure report:
- 路径:`reports/openclaw/failure-<event_id>.json`
- 内容:失败测试 ID、文件路径、失败原因、建议修复、是否阻断合并。
随后将问题交给自定义 GPT 修改当前 patch 分支,修复后重新执行完整验收。
## 10. 合并前门禁
只有全部满足以下条件,OpenClaw 才能标记为 `merge_candidate_ready`:
- required acceptance tests 全部 PASS;
- 无凭据泄露;
- 无 direct main write;
- 无高风险未审批动作;
- 灰度验证 PASS;
- Owner 通报与 Web UI 记录正常;
- `V1R1C5` baseline proposal 存在且未提前激活。
## 11. 最终输出
OpenClaw 最终应生成:
- `reports/openclaw/final-supervision-report.json`
- `reports/codex/merge-candidate.json`
- Owner 摘要事件记录;
- Web UI audit record;
- 若失败则生成 failure report 并阻止合并。
@@ -0,0 +1,462 @@
/* AIOS GitOps Platform Control Plane
* Browser-only runtime with API-first behavior and safe local fallback.
* No credentials or secret values are stored here. Model registration accepts secret_ref only.
*/
const Platform = (() => {
const state = {
tenant: 'default',
role: 'viewer',
identity: null,
baseline: null,
versions: null,
runtime: null,
patches: null,
models: null,
approvals: [],
audit: [],
policy: null,
eventSource: null
};
const ENDPOINTS = {
identity: 'identity',
policy: 'platform/policy',
audit: 'audit',
approvals: 'approval',
approvalRequest: 'approval-request',
approvalDecision: 'approval-decision',
branch: 'branch',
plan: 'patch/plan',
base: 'base',
versions: 'ver',
diff: 'diff',
patch: 'patch',
model: 'model',
priority: 'model-priority',
runtime: 'run',
rollback: 'rollback-request',
tag: 'tag-request',
events: 'events/gitops'
};
const DEFAULT_POLICY = {
roles: {
viewer: ['read:*', 'audit:read'],
operator: ['read:*', 'audit:read', 'branch:create', 'plan:create', 'approval:create'],
admin: ['read:*', 'audit:read', 'branch:create', 'plan:create', 'approval:create', 'model:write', 'priority:write', 'release:request', 'rollback:request'],
owner: ['read:*', 'audit:*', 'branch:*', 'plan:*', 'approval:*', 'model:*', 'priority:*', 'release:*', 'rollback:*', 'runtime:*']
},
highRiskActions: ['release:execute', 'rollback:execute', 'runtime:write', 'tag:create', 'main:write'],
destructiveActions: ['delete:*', 'force_push', 'purge:*', 'clear:*'],
requiredApprovals: { low: 1, medium: 1, high: 2 },
tenantIsolation: true,
secretHygiene: {
allowSecretRefOnly: true,
forbiddenFields: ['api_key', 'token', 'password', 'secret', 'private_key', 'bearer']
}
};
function el(id) { return document.getElementById(id); }
function qs(sel) { return document.querySelector(sel); }
function qsa(sel) { return Array.from(document.querySelectorAll(sel)); }
function now() { return new Date().toISOString(); }
function safeStringify(value) {
return JSON.stringify(redact(value), null, 2);
}
function redact(value) {
if (Array.isArray(value)) return value.map(redact);
if (value && typeof value === 'object') {
const out = {};
for (const [key, val] of Object.entries(value)) {
const k = key.toLowerCase();
if (['api_key', 'apikey', 'token', 'password', 'secret', 'private_key', 'authorization', 'bearer'].some(x => k.includes(x))) {
out[key] = typeof val === 'string' && val.startsWith('secret_ref://') ? val : '[REDACTED]';
} else out[key] = redact(val);
}
return out;
}
return value;
}
function put(id, value) {
const target = el(id);
if (target) target.textContent = typeof value === 'string' ? value : safeStringify(value);
}
async function api(path, method = 'GET', body = undefined, options = {}) {
const url = `/api/${path}`;
const headers = { 'Content-Type': 'application/json', 'X-AIOS-Tenant': state.tenant };
const res = await fetch(url, {
method,
headers,
credentials: 'same-origin',
body: body === undefined ? undefined : JSON.stringify(body)
});
let data = null;
try { data = await res.json(); } catch (_) { data = { status: res.status, text: await res.text() }; }
if (!res.ok && !options.allowError) {
const err = new Error(`API ${method} ${url} failed with ${res.status}`);
err.response = data;
throw err;
}
return data;
}
function has(capability) {
const policy = state.policy || DEFAULT_POLICY;
const caps = policy.roles[state.role] || [];
return caps.includes('*') || caps.includes(capability) || caps.some(c => c.endsWith(':*') && capability.startsWith(c.slice(0, -1))) || caps.includes('read:*') && capability.startsWith('read:');
}
function guardAction(capability, risk = 'low') {
const policy = state.policy || DEFAULT_POLICY;
if (policy.destructiveActions.some(pattern => matchCapability(pattern, capability))) return { allowed: false, reason: 'destructive_action_blocked' };
if (policy.highRiskActions.includes(capability)) return { allowed: false, reason: 'high_risk_requires_approval' };
if (!has(capability)) return { allowed: false, reason: 'insufficient_role' };
if (risk === 'high' && !['admin', 'owner'].includes(state.role)) return { allowed: false, reason: 'high_risk_admin_required' };
return { allowed: true };
}
function matchCapability(pattern, capability) {
if (pattern === capability) return true;
if (pattern.endsWith('*')) return capability.startsWith(pattern.slice(0, -1));
return false;
}
function audit(action, target, status, detail = {}) {
const record = redact({
ts: now(), tenant: state.tenant, role: state.role, action, target, status, detail
});
state.audit.unshift(record);
state.audit = state.audit.slice(0, 500);
localStorage.setItem('aios.gitops.audit', JSON.stringify(state.audit));
put('audit-output', record);
renderAudit();
postAudit(record);
return record;
}
async function postAudit(record) {
try { await api(ENDPOINTS.audit, 'POST', record, { allowError: true }); } catch (_) { /* local fallback */ }
}
function loadLocal() {
try { state.audit = JSON.parse(localStorage.getItem('aios.gitops.audit') || '[]'); } catch (_) { state.audit = []; }
try { state.approvals = JSON.parse(localStorage.getItem('aios.gitops.approvals') || '[]'); } catch (_) { state.approvals = []; }
}
function saveApprovals() {
localStorage.setItem('aios.gitops.approvals', JSON.stringify(state.approvals));
}
async function bootstrap() {
loadLocal();
wireTabs();
wireForms();
await loadIdentity();
await loadPolicy();
applyRbac();
await refreshAll();
await refreshApprovals();
await refreshAudit();
connectEvents();
}
async function loadIdentity() {
try {
const identity = await api(ENDPOINTS.identity, 'GET', undefined, { allowError: true });
state.identity = identity && identity.user ? identity : { user: 'unknown', role: identity.role || 'viewer', tenant: identity.tenant || 'default' };
state.role = state.identity.role || 'viewer';
state.tenant = state.identity.tenant || 'default';
} catch (_) {
state.identity = { user: 'local-preview', role: 'admin', tenant: 'default', source: 'local_fallback' };
state.role = 'admin';
state.tenant = 'default';
}
put('identity-output', state.identity);
const roleChip = el('role-chip');
if (roleChip) roleChip.textContent = `role: ${state.role}`;
}
async function loadPolicy() {
try {
const policy = await api(ENDPOINTS.policy, 'GET', undefined, { allowError: true });
state.policy = policy && policy.roles ? policy : DEFAULT_POLICY;
} catch (_) { state.policy = DEFAULT_POLICY; }
put('rbac-output', state.policy);
renderPermissionMatrix();
}
async function refreshAll() {
const tasks = [
['baseline', ENDPOINTS.base],
['versions', ENDPOINTS.versions],
['runtime', ENDPOINTS.runtime],
['patches', ENDPOINTS.patch],
['models', ENDPOINTS.model]
];
for (const [key, endpoint] of tasks) {
try { state[key] = await api(endpoint, 'GET', undefined, { allowError: true }); }
catch (e) { state[key] = { error: e.message, response: e.response }; }
}
put('baseline-output', state.baseline);
put('versions-output', state.versions);
put('runtime-output', state.runtime);
put('patch-summary-output', state.patches);
put('model-output', state.models);
updateMetrics();
renderModelGraph();
audit('dashboard.refresh', 'all', 'ok');
}
function updateMetrics() {
const runtimeStatus = state.runtime && state.runtime.status || 'unknown';
const version = state.baseline && (state.baseline.active_version || state.baseline.version) || '-';
const branches = state.patches && Array.isArray(state.patches.branches) ? state.patches.branches.filter(b => b.startsWith('patch/')).length : 0;
const pending = state.approvals.filter(a => a.status === 'pending').length;
setText('metric-runtime', runtimeStatus);
setText('metric-version', version);
setText('metric-branches', String(branches));
setText('metric-approvals', String(pending));
setText('runtime-chip', `runtime: ${runtimeStatus}`);
}
function setText(id, text) { const node = el(id); if (node) node.textContent = text; }
function applyRbac() {
qsa('.require-operator').forEach(btn => btn.disabled = !has('approval:create') && !has('branch:create'));
qsa('.require-admin').forEach(btn => btn.disabled = !['admin', 'owner'].includes(state.role));
qsa('.guarded').forEach(btn => btn.disabled = state.role !== 'owner');
renderPermissionMatrix();
}
function renderPermissionMatrix() {
const policy = state.policy || DEFAULT_POLICY;
const caps = ['read:*', 'branch:create', 'plan:create', 'approval:create', 'model:write', 'priority:write', 'release:request', 'rollback:request', 'runtime:write'];
const rows = Object.keys(policy.roles).map(role => {
const cells = caps.map(cap => `<td>${roleHas(role, cap, policy) ? '✅' : '—'}</td>`).join('');
return `<tr><th>${escapeHtml(role)}</th>${cells}</tr>`;
}).join('');
const html = `<table><thead><tr><th>Role</th>${caps.map(c => `<th>${escapeHtml(c)}</th>`).join('')}</tr></thead><tbody>${rows}</tbody></table>`;
const target = el('permission-matrix'); if (target) target.innerHTML = html;
}
function roleHas(role, cap, policy) {
const caps = policy.roles[role] || [];
return caps.includes('*') || caps.includes(cap) || caps.some(c => c.endsWith(':*') && cap.startsWith(c.slice(0, -1))) || (caps.includes('read:*') && cap.startsWith('read:'));
}
function renderModelGraph() {
const target = el('model-priority-graph');
if (!target) return;
const base = state.models && Array.isArray(state.models.base) ? state.models.base : [];
const custom = state.models && Array.isArray(state.models.custom) ? state.models.custom : [];
const priority = state.models && Array.isArray(state.models.priority) ? state.models.priority : [];
const models = [...base, ...custom];
if (!models.length) { target.innerHTML = '<p class="empty">无模型数据</p>'; return; }
const byId = new Map(priority.map(p => [p.model_id || p.id, p]));
target.innerHTML = models.map((m, idx) => {
const p = byId.get(m.id) || { priority: 100 - idx, enabled: true, fallback_policy: 'next_enabled' };
return `<div class="node"><strong>${escapeHtml(m.display_name || m.model || m.id)}</strong><span>${escapeHtml(m.id || '')}</span><em>priority ${escapeHtml(String(p.priority))} · ${p.enabled === false ? 'disabled' : 'enabled'} · ${escapeHtml(p.fallback_policy || 'next_enabled')}</em></div>`;
}).join('<div class="edge">↓ fallback</div>');
}
async function refreshApprovals() {
try {
const remote = await api(ENDPOINTS.approvals, 'GET', undefined, { allowError: true });
if (remote && Array.isArray(remote.items)) state.approvals = remote.items;
else if (Array.isArray(remote)) state.approvals = remote;
} catch (_) { /* local approvals already loaded */ }
renderApprovals();
updateMetrics();
}
function renderApprovals() {
const target = el('approval-board');
if (!target) return;
if (!state.approvals.length) { target.innerHTML = '<p class="empty">暂无审批请求</p>'; return; }
target.innerHTML = state.approvals.map(item => {
const canDecide = has('approval:*') || state.role === 'owner';
return `<article class="approval ${escapeHtml(item.risk || 'medium')}">
<header><strong>${escapeHtml(item.type)}</strong><span>${escapeHtml(item.status)}</span></header>
<p>${escapeHtml(item.target)}</p><small>${escapeHtml(item.reason || '')}</small>
<footer>${canDecide && item.status === 'pending' ? `<button data-approve="${escapeHtml(item.id)}">Approve</button><button data-reject="${escapeHtml(item.id)}">Reject</button>` : ''}</footer>
</article>`;
}).join('');
qsa('[data-approve]').forEach(btn => btn.onclick = () => decideApproval(btn.dataset.approve, 'approved'));
qsa('[data-reject]').forEach(btn => btn.onclick = () => decideApproval(btn.dataset.reject, 'rejected'));
}
async function createApproval(payload) {
const guard = guardAction('approval:create', payload.risk || 'medium');
if (!guard.allowed) return { status: 'blocked', ...guard };
const item = {
id: `apr-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
tenant: state.tenant,
status: 'pending',
created_at: now(),
created_by: state.identity && state.identity.user || 'unknown',
...payload
};
try {
const remote = await api(ENDPOINTS.approvalRequest, 'POST', item, { allowError: true });
if (remote && (remote.id || remote.status)) {
audit('approval.create', item.target, 'ok', remote);
await refreshApprovals();
return remote;
}
} catch (_) { /* local fallback */ }
state.approvals.unshift(item); saveApprovals(); renderApprovals(); updateMetrics();
audit('approval.create.local', item.target, 'ok', item);
return item;
}
async function decideApproval(id, status) {
const guard = guardAction('approval:decide', 'high');
if (!guard.allowed) { audit('approval.decide', id, 'blocked', guard); return; }
try {
const remote = await api(ENDPOINTS.approvalDecision, 'POST', { id, status }, { allowError: true });
audit('approval.decide', id, 'ok', remote);
} catch (_) {
const item = state.approvals.find(a => a.id === id);
if (item) { item.status = status; item.decided_at = now(); item.decided_by = state.identity && state.identity.user || 'unknown'; }
saveApprovals(); audit('approval.decide.local', id, 'ok', { status });
}
renderApprovals(); updateMetrics();
}
async function refreshAudit() {
try {
const remote = await api(ENDPOINTS.audit, 'GET', undefined, { allowError: true });
if (remote && Array.isArray(remote.items)) state.audit = remote.items.concat(state.audit).slice(0, 500);
} catch (_) { /* local fallback */ }
renderAudit();
}
function renderAudit() {
const target = el('audit-table'); if (!target) return;
const filter = (el('audit-filter') && el('audit-filter').value || '').toLowerCase();
const rows = state.audit.filter(r => !filter || JSON.stringify(r).toLowerCase().includes(filter)).slice(0, 100).map(r =>
`<tr><td>${escapeHtml(r.ts)}</td><td>${escapeHtml(r.tenant || '')}</td><td>${escapeHtml(r.role || '')}</td><td>${escapeHtml(r.action)}</td><td>${escapeHtml(r.target)}</td><td>${escapeHtml(r.status)}</td></tr>`
).join('');
target.innerHTML = `<table><thead><tr><th>Time</th><th>Tenant</th><th>Role</th><th>Action</th><th>Target</th><th>Status</th></tr></thead><tbody>${rows || '<tr><td colspan="6">无审计记录</td></tr>'}</tbody></table>`;
}
function parseDiffTree(diffData) {
const stdout = diffData && diffData.diff && diffData.diff.stdout || diffData && diffData.stdout || '';
const files = [];
let current = null;
stdout.split('\n').forEach(line => {
if (line.startsWith('diff --git ')) {
const parts = line.split(' ');
current = { file: (parts[3] || '').replace(/^b\//, ''), additions: 0, deletions: 0, chunks: 0 };
files.push(current);
} else if (current) {
if (line.startsWith('@@')) current.chunks++;
else if (line.startsWith('+') && !line.startsWith('+++')) current.additions++;
else if (line.startsWith('-') && !line.startsWith('---')) current.deletions++;
}
});
return files;
}
function renderDiffTree(diffData) {
const target = el('diff-tree'); if (!target) return;
const files = parseDiffTree(diffData);
if (!files.length) { target.innerHTML = '<p class="empty">未解析到文件级 diff,可能是接口返回过大或为空。</p>'; return; }
target.innerHTML = files.map(f => `<div class="diff-file"><strong>${escapeHtml(f.file)}</strong><span>+${f.additions} / -${f.deletions} / ${f.chunks} chunks</span></div>`).join('');
}
function connectEvents() {
if (!window.EventSource) return;
try {
state.eventSource = new EventSource(`/api/${ENDPOINTS.events}?tenant=${encodeURIComponent(state.tenant)}`);
state.eventSource.onmessage = (event) => {
const payload = JSON.parse(event.data);
audit('event.gitops', payload.type || 'event', 'ok', payload);
if (['approval', 'patch', 'runtime', 'model'].includes(payload.type)) refreshAll();
};
state.eventSource.onerror = () => { if (state.eventSource) state.eventSource.close(); };
} catch (_) { /* server-sent events optional */ }
}
function wireTabs() {
qsa('.tabs button').forEach(button => {
button.onclick = () => {
qsa('.tabs button').forEach(b => b.classList.remove('active'));
qsa('.tab').forEach(tab => tab.classList.remove('active'));
button.classList.add('active');
const target = el(`tab-${button.dataset.tab}`);
if (target) target.classList.add('active');
};
});
}
function wireForms() {
const refreshBtn = el('refresh-all'); if (refreshBtn) refreshBtn.onclick = refreshAll;
const refreshModel = el('refresh-model'); if (refreshModel) refreshModel.onclick = refreshAll;
const refreshApprovalsBtn = el('refresh-approvals'); if (refreshApprovalsBtn) refreshApprovalsBtn.onclick = refreshApprovals;
const clearApprovals = el('clear-local-approvals'); if (clearApprovals) clearApprovals.onclick = () => { state.approvals = []; saveApprovals(); renderApprovals(); audit('approval.local.clear', 'localStorage', 'ok'); };
const refreshAuditBtn = el('refresh-audit'); if (refreshAuditBtn) refreshAuditBtn.onclick = refreshAudit;
const auditFilter = el('audit-filter'); if (auditFilter) auditFilter.oninput = renderAudit;
const exportAudit = el('export-audit'); if (exportAudit) exportAudit.onclick = () => downloadJson('aios-gitops-audit.json', state.audit);
bindForm('approval-form', async f => createApproval({ type: f.get('type'), target: f.get('target'), risk: f.get('risk'), reason: f.get('reason') }), 'approval-output');
bindForm('diff-form', async f => {
const data = await api(`${ENDPOINTS.diff}?from=${encodeURIComponent(f.get('from'))}&to=${encodeURIComponent(f.get('to'))}`, 'GET', undefined, { allowError: true });
renderDiffTree(data); return data;
}, 'diff-output');
bindForm('branch-form', async f => checkedApi('branch:create', ENDPOINTS.branch, 'POST', { branch: f.get('branch'), base: f.get('base'), message: f.get('message') }), 'branch-output');
bindForm('plan-form', async f => checkedApi('plan:create', ENDPOINTS.plan, 'POST', { branch: f.get('branch'), text: f.get('text') }), 'plan-output');
bindForm('model-form', async f => {
const payload = { provider_name: f.get('provider_name'), base_url: f.get('base_url'), model_name: f.get('model_name'), display_name: f.get('display_name'), api_key_secret_ref: f.get('api_key_secret_ref') };
if (!String(payload.api_key_secret_ref || '').startsWith('secret_ref://')) return { status: 'blocked', reason: 'api_key_secret_ref_required' };
return checkedApi('model:write', ENDPOINTS.model, 'POST', payload, 'high');
}, 'model-register-output');
bindForm('priority-form', async f => checkedApi('priority:write', ENDPOINTS.priority, 'PUT', { model_id: f.get('model_id'), priority: Number(f.get('priority')), enabled: f.get('enabled') === 'true', fallback_policy: f.get('fallback_policy') }, 'medium'), 'priority-output');
bindForm('rollback-form', async f => checkedApi('rollback:request', ENDPOINTS.rollback, 'POST', { target_version: f.get('target_version'), reason: f.get('reason'), branch: f.get('branch') }, 'high'), 'rollback-output');
bindForm('tag-form', async f => checkedApi('release:request', ENDPOINTS.tag, 'POST', { version: f.get('version'), message: f.get('message') }, 'high'), 'tag-output');
}
function bindForm(id, handler, outputId) {
const form = el(id); if (!form) return;
form.onsubmit = async (e) => {
e.preventDefault();
const f = new FormData(form);
try {
const result = await handler(f);
put(outputId, result);
audit(`${id}.submit`, outputId, result && result.status === 'blocked' ? 'blocked' : 'ok', result);
} catch (err) {
const detail = { error: err.message, response: err.response };
put(outputId, detail);
audit(`${id}.submit`, outputId, 'failed', detail);
}
};
}
async function checkedApi(capability, endpoint, method, payload, risk = 'low') {
const guard = guardAction(capability, risk);
if (!guard.allowed) return { status: 'blocked', capability, ...guard };
return await api(endpoint, method, payload, { allowError: true });
}
function downloadJson(filename, value) {
const blob = new Blob([safeStringify(value)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
function escapeHtml(value) {
return String(value == null ? '' : value).replace(/[&<>'"]/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\'': '&#39;', '"': '&quot;' }[ch]));
}
return { bootstrap, refreshAll, state };
})();
window.addEventListener('DOMContentLoaded', Platform.bootstrap);
@@ -0,0 +1,56 @@
{
"name": "aios-gitops-enterprise-api-contract",
"version": "1.0.0",
"scope": "portal-gitops-dashboard",
"principles": {
"all_writes_require_git_commit": true,
"direct_main_push_from_ui": false,
"secrets_in_git": false,
"destructive_operations": "forbidden_or_request_only",
"server_side_authz_is_authoritative": true
},
"read_endpoints": {
"baseline": "GET /api/base",
"versions": "GET /api/ver",
"runtime": "GET /api/run",
"patches": "GET /api/patch",
"model_registry": "GET /api/model",
"diff_raw": "GET /api/diff?from={from}&to={to}",
"diff_tree": "GET /api/diff/tree?from={from}&to={to}",
"approval_queue": "GET /api/approval/queue",
"release_pipeline": "GET /api/release/pipeline",
"audit_log": "GET /api/audit/log?limit={limit}",
"me": "GET /api/me"
},
"write_endpoints": {
"create_branch": "POST /api/branch",
"natural_language_plan": "POST /api/patch/plan",
"register_model": "POST /api/model",
"set_model_priority": "PUT /api/model-priority",
"rollback_request": "POST /api/rollback-request",
"tag_request": "POST /api/tag-request",
"approval_decision": "POST /api/approval/decision",
"release_promotion_request": "POST /api/release/promote-request",
"audit_append": "POST /api/audit/log"
},
"approval_decision_payload": {
"request_id": "string",
"decision": "approve | reject",
"reason": "string",
"branch": "patch/approval-..."
},
"release_promotion_payload": {
"from_environment": "dev | staging",
"to_environment": "staging | prod",
"version": "VxRxCx",
"reason": "string",
"branch": "patch/release-promote-..."
},
"response_contract": {
"write_success_any_of": ["branch", "commit", "rev", "sync_status"],
"auth_failure": {
"401": "Bearer token not injected",
"403": "Bearer token injected but scope insufficient"
}
}
}
@@ -0,0 +1,79 @@
{
"name": "aios-gitops-enterprise-ui-policy",
"version": "1.0.0",
"scope": "portal-gitops-dashboard",
"client_side_notice": "Client-side RBAC is advisory only. Server-side Actions/API authorization remains authoritative.",
"roles": {
"viewer": {
"label": "Viewer",
"permissions": [
"read:baseline",
"read:versions",
"read:diff",
"read:patch",
"read:model",
"read:runtime",
"read:audit"
]
},
"developer": {
"label": "Developer",
"inherits": ["viewer"],
"permissions": [
"write:branch",
"write:plan"
]
},
"admin": {
"label": "Admin",
"inherits": ["developer"],
"permissions": [
"write:model",
"write:model_priority",
"request:rollback",
"request:tag"
]
},
"owner": {
"label": "Owner",
"inherits": ["admin"],
"permissions": [
"approve:rollback",
"approve:tag",
"approve:priority",
"approve:release"
]
}
},
"risk_policy": {
"direct_main_write": "disabled_in_ui",
"api_keys_in_git": "forbidden",
"rollback": "request_only",
"tag_create": "request_only_by_default",
"runtime_change": "request_only",
"baseline_high_risk_change": "request_only",
"bulk_delete": "forbidden"
},
"secret_redaction": {
"enabled": true,
"patterns": [
"api_key",
"apikey",
"authorization",
"bearer",
"token",
"password",
"secret",
"private_key",
"credential"
],
"replacement": "[REDACTED]"
},
"approval_queues": [
"rollback",
"tag",
"model_priority",
"release",
"runtime_change"
]
}
@@ -0,0 +1,224 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>AIOS Enterprise GitOps Control Plane</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="shell">
<header class="topbar">
<div>
<p class="eyebrow">cnnetmkt.top / AIOS Enterprise</p>
<h1>GitOps Enterprise Control Plane</h1>
<p class="subtitle">审批流、RBAC、Release Pipeline、Diff Tree、模型优先级、审计查询</p>
</div>
<div class="statusbar">
<span id="role-chip" class="chip">role: loading</span>
<span id="runtime-chip" class="chip muted">runtime: loading</span>
<button id="refresh-all" class="primary">刷新全部</button>
</div>
</header>
<nav class="tabs" aria-label="Enterprise GitOps modules">
<button data-tab="overview" class="active">总览</button>
<button data-tab="rbac">RBAC</button>
<button data-tab="approval">审批流</button>
<button data-tab="diff">Diff Tree</button>
<button data-tab="patch">Patch / Branch</button>
<button data-tab="model">模型控制</button>
<button data-tab="release">Release Pipeline</button>
<button data-tab="audit">审计中心</button>
</nav>
<main>
<section id="tab-overview" class="tab active">
<div class="grid four">
<article class="metric"><span>Runtime</span><strong id="metric-runtime">-</strong></article>
<article class="metric"><span>Active Version</span><strong id="metric-version">-</strong></article>
<article class="metric"><span>Patch Branches</span><strong id="metric-branches">-</strong></article>
<article class="metric"><span>Pending Approvals</span><strong id="metric-approvals">-</strong></article>
</div>
<div class="grid two">
<article class="card"><h2>Baseline</h2><pre id="baseline-output">loading...</pre></article>
<article class="card"><h2>Versions</h2><pre id="versions-output">loading...</pre></article>
<article class="card"><h2>Runtime</h2><pre id="runtime-output">loading...</pre></article>
<article class="card"><h2>Patch Summary</h2><pre id="patch-summary-output">loading...</pre></article>
</div>
</section>
<section id="tab-rbac" class="tab grid two">
<article class="card">
<h2>当前用户权限</h2>
<p class="hint">按钮级权限由 role + capability 共同控制;viewer 只能读。</p>
<pre id="identity-output">loading...</pre>
</article>
<article class="card">
<h2>RBAC 策略</h2>
<pre id="rbac-output">loading...</pre>
</article>
<article class="card span-two">
<h2>权限矩阵</h2>
<div id="permission-matrix" class="table-wrap">loading...</div>
</article>
</section>
<section id="tab-approval" class="tab">
<div class="grid two">
<article class="card">
<h2>Approval Queue</h2>
<p class="hint">高风险操作进入审批队列;UI 不直接执行 destructive / production 变更。</p>
<div class="toolbar">
<button id="refresh-approvals" class="secondary">刷新审批队列</button>
<button id="clear-local-approvals" class="secondary guarded">清理本地缓存</button>
</div>
<div id="approval-board" class="approval-board">loading...</div>
</article>
<article class="card">
<h2>创建审批请求</h2>
<form id="approval-form" class="stack">
<label>类型
<select name="type">
<option value="merge">merge</option>
<option value="release">release</option>
<option value="rollback">rollback</option>
<option value="model_priority">model_priority</option>
<option value="runtime_change">runtime_change</option>
</select>
</label>
<label>目标 <input name="target" placeholder="branch / version / model id" required /></label>
<label>风险
<select name="risk"><option>low</option><option selected>medium</option><option>high</option></select>
</label>
<label>说明 <textarea name="reason" rows="5" required></textarea></label>
<button class="primary require-operator" type="submit">提交审批请求</button>
</form>
<pre id="approval-output">等待操作...</pre>
</article>
</div>
</section>
<section id="tab-diff" class="tab">
<article class="card">
<h2>File-level Diff Tree</h2>
<p class="hint">支持 from/to 参数;会自动按文件聚合 diff,便于 review。</p>
<form id="diff-form" class="form-row wrap">
<label>From <input name="from" value="HEAD~1" /></label>
<label>To <input name="to" value="HEAD" /></label>
<button class="primary" type="submit">读取 Diff</button>
</form>
<div id="diff-tree" class="diff-tree">等待查询...</div>
<pre id="diff-output">等待查询...</pre>
</article>
</section>
<section id="tab-patch" class="tab grid two">
<article class="card">
<h2>创建 Patch Branch</h2>
<p class="hint">默认从 main 创建非生产 patch 分支;main 写入仅通过明确授权的 GitOps Action。</p>
<form id="branch-form" class="stack">
<label>Branch <input name="branch" placeholder="patch/my-change" required /></label>
<label>Base <input name="base" value="main" /></label>
<label>Message <input name="message" value="Create GitOps patch branch" /></label>
<button class="primary require-operator" type="submit">创建 Branch</button>
</form>
<pre id="branch-output">等待操作...</pre>
</article>
<article class="card">
<h2>自然语言 GitOps Plan</h2>
<p class="hint">仅生成可审计 plan;高风险操作必须审批。</p>
<form id="plan-form" class="stack">
<label>目标 Branch <input name="branch" value="patch/web-ui-complete-gitops-dashboard-20260706" /></label>
<label>Plan Text <textarea name="text" rows="5" placeholder="描述要执行的 GitOps 变更" required></textarea></label>
<button class="primary require-operator" type="submit">生成 Plan</button>
</form>
<pre id="plan-output">等待操作...</pre>
</article>
</section>
<section id="tab-model" class="tab">
<div class="grid two">
<article class="card">
<h2>模型注册表</h2>
<button id="refresh-model" class="secondary">刷新模型</button>
<pre id="model-output">loading...</pre>
</article>
<article class="card">
<h2>模型优先级图</h2>
<div id="model-priority-graph" class="graph">loading...</div>
</article>
<article class="card">
<h2>注册自定义模型</h2>
<p class="hint">仅允许 secret_ref;禁止真实 API key 明文。</p>
<form id="model-form" class="stack">
<label>Provider <input name="provider_name" placeholder="openai / one-api / compatible" required /></label>
<label>Base URL <input name="base_url" placeholder="https://example.com/v1" required /></label>
<label>Model Name <input name="model_name" placeholder="model-id" required /></label>
<label>Display Name <input name="display_name" placeholder="Human readable name" /></label>
<label>API Key Secret Ref <input name="api_key_secret_ref" placeholder="secret_ref://..." required /></label>
<button class="primary require-admin" type="submit">注册模型</button>
</form>
<pre id="model-register-output">等待操作...</pre>
</article>
<article class="card">
<h2>模型优先级 / Fallback</h2>
<form id="priority-form" class="stack">
<label>Model ID <input name="model_id" placeholder="base.gpt-5.5" required /></label>
<label>Priority <input name="priority" type="number" value="100" required /></label>
<label>Enabled <select name="enabled"><option value="true">true</option><option value="false">false</option></select></label>
<label>Fallback Policy <input name="fallback_policy" value="next_enabled" /></label>
<button class="primary require-admin" type="submit">保存优先级</button>
</form>
<pre id="priority-output">等待操作...</pre>
</article>
</div>
</section>
<section id="tab-release" class="tab">
<div class="pipeline" id="release-pipeline">
<div class="stage">Patch</div><div class="arrow">→</div><div class="stage">Review</div><div class="arrow">→</div><div class="stage">Approval</div><div class="arrow">→</div><div class="stage guarded-stage">Tag Request</div><div class="arrow">→</div><div class="stage guarded-stage">Deploy</div>
</div>
<div class="grid two">
<article class="card danger-zone">
<h2>Rollback Request</h2>
<p class="hint">只创建审批请求;不直接执行回滚。</p>
<form id="rollback-form" class="stack">
<label>Target Version <input name="target_version" placeholder="V1R1C3" required /></label>
<label>Reason <textarea name="reason" rows="4" required></textarea></label>
<label>Request Branch <input name="branch" value="patch/rollback-request-web-ui" /></label>
<button class="danger require-admin" type="submit">创建 Rollback 请求</button>
</form>
<pre id="rollback-output">等待操作...</pre>
</article>
<article class="card danger-zone">
<h2>Tag / Version Request</h2>
<p class="hint">默认创建 owner/admin approval request;UI 禁用直接执行。</p>
<form id="tag-form" class="stack">
<label>Version <input name="version" placeholder="V1R1C5" required /></label>
<label>Message <input name="message" placeholder="Release request" /></label>
<label class="disabled"><input name="execute" type="checkbox" disabled /> 直接执行(禁用)</label>
<button class="danger require-admin" type="submit">创建 Tag 请求</button>
</form>
<pre id="tag-output">等待操作...</pre>
</article>
</div>
</section>
<section id="tab-audit" class="tab">
<article class="card">
<h2>审计中心</h2>
<div class="toolbar">
<input id="audit-filter" placeholder="过滤 action / target / status" />
<button id="export-audit" class="secondary">导出 JSON</button>
<button id="refresh-audit" class="secondary">刷新</button>
</div>
<div id="audit-table" class="table-wrap">loading...</div>
<pre id="audit-output">暂无操作。</pre>
</article>
</section>
</main>
</div>
<script src="./app.js"></script>
</body>
</html>
@@ -0,0 +1,22 @@
{
"name": "gitops-dashboard",
"version": "1.0.0",
"mount": "/portal/me/#gitops-dashboard",
"description": "Complete GitOps Web UI module",
"features": [
"baseline_view",
"version_list",
"diff_view",
"patch_view",
"model_registry",
"runtime_status"
],
"api": {
"base": "/api/base",
"ver": "/api/ver",
"diff": "/api/diff",
"patch": "/api/patch",
"model": "/api/model",
"runtime": "/api/run"
}
}
@@ -0,0 +1,6 @@
body{font-family:Arial,Helvetica,sans-serif;background:#0b0f14;color:#e6edf3;margin:0}
header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#111826;border-bottom:1px solid #222}
nav button{margin-right:8px;padding:8px 10px;background:#1f2937;color:#e6edf3;border:1px solid #2b3645;cursor:pointer}
nav button:hover{background:#2b3645}
main{padding:16px}
pre{background:#0f172a;padding:12px;border:1px solid #1f2937;overflow:auto;min-height:400px}
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/web-ui-complete-gitops-dashboard-20260706",
"base": "main",
"created_by": "gitops-write-bot",
"created_at": "2026-07-06T15:33:08Z",
"direct_main_push": false,
"owner_approval_required": true
}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -e
BRANCH=$1
BASE=${2:-main}
if [ -z "$BRANCH" ]; then
echo "Usage: autonomous-merge-gate.sh <branch> <base>"
exit 1
fi
echo "[Autonomous Gate] Evaluating: $BRANCH vs $BASE"
REPORT_DIR="reports/codex"
mkdir -p "$REPORT_DIR"
# Run validation and capture exit code correctly
bash scripts/validate-gitops-platform-ui.sh "$BRANCH" "$BASE"
VALIDATION_RC=$?
if [ $VALIDATION_RC -ne 0 ]; then
echo "FAIL: validation failed"
exit $VALIDATION_RC
fi
# Secret safety re-check
if grep -R "BEGIN PRIVATE KEY" -n overlays/ >/dev/null 2>&1; then
echo "BLOCKED: secret leak"
exit 2
fi
# Create merge candidate artifact
cat > "$REPORT_DIR/merge-candidate.json" <<EOF
{
"branch": "$BRANCH",
"base": "$BASE",
"status": "merge_candidate",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
echo "PASS: merge candidate ready"
exit 0
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Enqueue a WeChat notification into WebOps message_outbox.
Business modules should call this helper or the equivalent WebOps API instead of
calling OpenClaw / WeChat adapters directly.
"""
from __future__ import annotations
import argparse
import json
import os
import sqlite3
import time
from pathlib import Path
ROOT = Path(os.environ.get("AIOS_WEBOPS_ROOT", "/data/aios-gitops/workspace"))
DB = Path(os.environ.get("AIOS_WEBOPS_DB", str(ROOT / "data" / "webops.sqlite3")))
MIGRATION = ROOT / "db" / "migrations" / "20260707_message_outbox.sql"
def ensure_schema(con: sqlite3.Connection) -> None:
if MIGRATION.exists():
con.executescript(MIGRATION.read_text(encoding="utf-8"))
else:
con.executescript(
"""
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
);
"""
)
def main() -> int:
parser = argparse.ArgumentParser(description="Enqueue a WeChat notification")
parser.add_argument("--recipient", "--recipient-user-id", dest="recipient_user_id", required=True)
parser.add_argument("--recipient-role", default="owner")
parser.add_argument("--message-type", default="alert")
parser.add_argument("--priority", default="normal", choices=["critical", "high", "normal", "low"])
parser.add_argument("--title", default="AIOS 通知")
parser.add_argument("--content", required=True)
parser.add_argument("--payload", default="{}")
parser.add_argument("--id")
args = parser.parse_args()
DB.parent.mkdir(parents=True, exist_ok=True)
msg_id = args.id or f"msg_{int(time.time() * 1000)}"
with sqlite3.connect(DB) as con:
ensure_schema(con)
con.execute(
"""
INSERT INTO message_outbox (
id, channel, recipient_user_id, recipient_role, message_type,
priority, title, content, payload, status, created_at
) VALUES (?, 'wechat', ?, ?, ?, ?, ?, ?, ?, 'queued', datetime('now'))
""",
(
msg_id,
args.recipient_user_id,
args.recipient_role,
args.message_type,
args.priority,
args.title,
args.content,
args.payload,
),
)
con.commit()
print(json.dumps({"status": "queued", "id": msg_id, "db": str(DB)}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Ensure WebOps message outbox schema.
This schema is the only supported active outbound notification entrypoint for
business modules and external systems. Delivery is performed by the worker.
The migration is backward-compatible with already-created SQLite tables:
columns are added before indexes that reference them, and UNIQUE constraints are
created as indexes because SQLite cannot add a UNIQUE column via ALTER TABLE.
"""
from __future__ import annotations
import os
import sqlite3
from pathlib import Path
ROOT = Path(os.environ.get("AIOS_WEBOPS_ROOT", "/data/aios-gitops/workspace"))
DB = Path(os.environ.get("AIOS_WEBOPS_DB", str(ROOT / "data" / "webops.sqlite3")))
BASE_TABLES_SQL = """
CREATE TABLE IF NOT EXISTS message_outbox (
id TEXT PRIMARY KEY,
idempotency_key TEXT,
channel TEXT NOT NULL,
recipient_user_id TEXT,
recipient_role TEXT,
message_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT,
content TEXT NOT NULL,
source_system TEXT,
source_event_id TEXT,
require_approval INTEGER NOT NULL DEFAULT 0,
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,
cancelled_at TEXT
);
CREATE TABLE IF NOT EXISTS channel_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
user_role TEXT,
channel_type TEXT NOT NULL,
external_chat_id TEXT NOT NULL,
notification_prefs TEXT,
status TEXT NOT NULL DEFAULT 'active',
updated_at TEXT NOT NULL
);
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
);
"""
INDEX_SQL = """
CREATE UNIQUE INDEX IF NOT EXISTS idx_message_outbox_idempotency
ON message_outbox(idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_message_outbox_dispatch
ON message_outbox(channel, status, priority, created_at);
CREATE INDEX IF NOT EXISTS idx_channel_bindings_lookup
ON channel_bindings(user_id, channel_type, status, updated_at);
CREATE INDEX IF NOT EXISTS idx_channel_bindings_role_lookup
ON channel_bindings(user_role, channel_type, status, updated_at);
"""
COLUMNS = {
"message_outbox": {
"idempotency_key": "TEXT",
"recipient_user_id": "TEXT",
"recipient_role": "TEXT",
"source_system": "TEXT",
"source_event_id": "TEXT",
"require_approval": "INTEGER NOT NULL DEFAULT 0",
"cancelled_at": "TEXT",
},
"channel_bindings": {
"user_role": "TEXT",
"notification_prefs": "TEXT",
},
}
def existing_columns(con: sqlite3.Connection, table: str) -> set[str]:
return {row[1] for row in con.execute(f"PRAGMA table_info({table})")}
def ensure_schema(db_path: Path = DB) -> None:
db_path.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(db_path) as con:
con.executescript(BASE_TABLES_SQL)
for table, cols in COLUMNS.items():
existing = existing_columns(con, table)
for name, ddl in cols.items():
if name not in existing:
con.execute(f"ALTER TABLE {table} ADD COLUMN {name} {ddl}")
con.executescript(INDEX_SQL)
con.commit()
if __name__ == "__main__":
ensure_schema()
print(f"schema_ok db={DB}")
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -e
BRANCH=$1
BASE=${2:-main}
if [ -z "$BRANCH" ]; then
echo "Usage: validate-gitops-platform-ui.sh <branch> <base>"
exit 1
fi
echo "[Codex] Validating branch: $BRANCH against $BASE"
echo "[1/5] Git diff check"
git diff --stat $BASE...$BRANCH
echo "[2/5] Acceptance test load"
if [ ! -f tests/acceptance/gitops-platform-ui.acceptance.json ]; then
echo "FAIL: acceptance file missing"
exit 2
fi
echo "[3/5] Secret scan"
if grep -R "BEGIN PRIVATE KEY" -n overlays/ >/dev/null 2>&1; then
echo "FAIL: private key detected"
exit 3
fi
if grep -R "Bearer " -n overlays/ >/dev/null 2>&1; then
echo "WARN: bearer token pattern found"
fi
echo "[4/5] UI structural validation"
FILES=(
overlays/user-control-center/portal/gitops-dashboard/index.html
overlays/user-control-center/portal/gitops-dashboard/app.js
)
for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "FAIL: missing $f"
exit 4
fi
done
echo "[5/5] Policy validation"
grep -R "direct_main_push" -n overlays/user-control-center/portal/gitops-dashboard/ || true
echo "PASS: Codex validation completed"
exit 0
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Production WeChat notify worker for AIOS / Xiamayiti.
Flow:
message_outbox -> channel_bindings -> wechat_send_backend -> sent/failed/retry/audit
Business modules must not call OpenClaw or UCC Adapter directly. They only
enqueue messages. The pluggable backend chooses OpenClaw channel or UCC Adapter
at runtime.
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import sqlite3
import time
from pathlib import Path
from typing import Any, Dict
ROOT = Path(os.environ.get("AIOS_WEBOPS_ROOT", "/data/aios-gitops/workspace"))
DB = Path(os.environ.get("AIOS_WEBOPS_DB", str(ROOT / "data" / "webops.sqlite3")))
DEFAULT_LIMIT = int(os.environ.get("AIOS_WECHAT_WORKER_LIMIT", "20"))
PRIORITY_SQL = """
CASE priority
WHEN 'critical' THEN 1
WHEN 'high' THEN 2
WHEN 'normal' THEN 3
ELSE 4
END
"""
def load_module(path: Path, name: str):
spec = importlib.util.spec_from_file_location(name, path)
if not spec or not spec.loader:
raise RuntimeError(f"cannot load module: {path}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # type: ignore[attr-defined]
return mod
def ensure_schema(con: sqlite3.Connection) -> None:
schema_path = ROOT / "scripts" / "message_outbox_schema.py"
if schema_path.exists():
mod = load_module(schema_path, "message_outbox_schema")
mod.ensure_schema(DB)
return
con.executescript(
"""
CREATE TABLE IF NOT EXISTS message_outbox (
id TEXT PRIMARY KEY,
idempotency_key TEXT,
channel TEXT NOT NULL,
recipient_user_id TEXT,
recipient_role TEXT,
message_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
title TEXT,
content TEXT NOT NULL,
source_system TEXT,
source_event_id TEXT,
require_approval INTEGER NOT NULL DEFAULT 0,
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,
cancelled_at TEXT
);
CREATE TABLE IF NOT EXISTS channel_bindings (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
user_role TEXT,
channel_type TEXT NOT NULL,
external_chat_id TEXT NOT NULL,
notification_prefs TEXT,
status TEXT NOT NULL DEFAULT 'active',
updated_at TEXT NOT NULL
);
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
);
"""
)
def audit(con: sqlite3.Connection, action: str, target: str, detail: Dict[str, Any]) -> None:
con.execute(
"""
INSERT INTO audit_logs(id, actor, action, target, detail, created_at)
VALUES(?, 'system', ?, ?, ?, datetime('now'))
""",
(f"audit_{int(time.time() * 1000)}", action, target, json.dumps(detail, ensure_ascii=False)),
)
def load_wechat_target(con: sqlite3.Connection, row: sqlite3.Row) -> str:
recipient_user_id = row["recipient_user_id"]
recipient_role = row["recipient_role"]
if recipient_user_id:
found = con.execute(
"""
SELECT external_chat_id
FROM channel_bindings
WHERE user_id = ?
AND channel_type = 'wechat'
AND status = 'active'
ORDER BY updated_at DESC
LIMIT 1
""",
(recipient_user_id,),
).fetchone()
if found:
return found["external_chat_id"]
if recipient_role:
found = con.execute(
"""
SELECT external_chat_id
FROM channel_bindings
WHERE (user_role = ? OR user_id = ?)
AND channel_type = 'wechat'
AND status = 'active'
ORDER BY updated_at DESC
LIMIT 1
""",
(recipient_role, recipient_role),
).fetchone()
if found:
return found["external_chat_id"]
raise RuntimeError(f"wechat binding not found for recipient_user_id={recipient_user_id} role={recipient_role}")
def sanitize_content(row: sqlite3.Row) -> str:
content = row["content"] or ""
blocked_markers = ["BEGIN PRIVATE KEY", "Bearer ", "PASSWORD=", "TOKEN=", "/data/aios-gitops/runtime/secrets"]
for marker in blocked_markers:
if marker in content:
content = content.replace(marker, "[REDACTED]")
title = row["title"] or "AIOS 通知"
return f"{title}\n\n{content}"
def send_with_backend(target: str, content: str, row: sqlite3.Row, dry_run: bool = False) -> Dict[str, Any]:
backend_path = ROOT / "scripts" / "wechat_send_backend.py"
if not backend_path.exists():
raise RuntimeError("wechat_send_backend.py not found")
backend = load_module(backend_path, "wechat_send_backend")
metadata = {}
if row["payload"]:
try:
metadata = json.loads(row["payload"])
except Exception:
metadata = {"raw_payload": row["payload"][:1000]}
return backend.send_message(
target=target,
content=content,
account_id=os.environ.get("AIOS_WECHAT_ACCOUNT_ID", ""),
message_id=row["id"],
metadata=metadata,
dry_run=dry_run,
)
def mark_dispatching(con: sqlite3.Connection, msg_id: str) -> bool:
cur = con.execute("UPDATE message_outbox SET status='dispatching' WHERE id=? AND status='queued'", (msg_id,))
return cur.rowcount == 1
def process_row(con: sqlite3.Connection, row: sqlite3.Row, dry_run: bool = False) -> None:
msg_id = row["id"]
if int(row["require_approval"] or 0) == 1 and row["message_type"] == "approval_request":
con.execute("UPDATE message_outbox SET status='approval_pending' WHERE id=?", (msg_id,))
audit(con, "wechat_outbound_approval_pending", row["recipient_user_id"] or row["recipient_role"] or "unknown", {"message_id": msg_id})
return
if not mark_dispatching(con, msg_id):
return
try:
target = load_wechat_target(con, row)
content = sanitize_content(row)
result = send_with_backend(target, content, row, dry_run=dry_run)
ok = bool(result.get("ok"))
if not ok:
code = result.get("code") or result.get("status") or "send_failed"
raise RuntimeError(f"wechat send failed via {result.get('backend')}: {code}; {result.get('error') or result}")
status = "sent" if not dry_run else "queued"
con.execute(
"""
UPDATE message_outbox
SET status=?,
sent_at=CASE WHEN ?='sent' THEN datetime('now') ELSE sent_at END,
payload=json_set(COALESCE(payload, '{}'), '$.send_result', ?),
last_error=NULL
WHERE id=?
""",
(status, status, json.dumps(result, ensure_ascii=False), msg_id),
)
audit(con, "wechat_outbound_sent" if not dry_run else "wechat_outbound_dry_run", row["recipient_user_id"] or row["recipient_role"] or "unknown", {"message_id": msg_id, "backend": result.get("backend"), "result": result})
except Exception as exc:
retry_count = int(row["retry_count"] or 0) + 1
status = "failed" if retry_count >= 5 else "queued"
con.execute(
"UPDATE message_outbox SET retry_count=?, status=?, last_error=? WHERE id=?",
(retry_count, status, str(exc)[:1000], msg_id),
)
audit(con, "wechat_outbound_failed", row["recipient_user_id"] or row["recipient_role"] or "unknown", {"message_id": msg_id, "retry_count": retry_count, "error": str(exc)[:1000]})
def run_once(limit: int, dry_run: bool = False) -> Dict[str, Any]:
DB.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(DB) as con:
con.row_factory = sqlite3.Row
ensure_schema(con)
rows = con.execute(
f"SELECT * FROM message_outbox WHERE channel='wechat' AND status='queued' ORDER BY {PRIORITY_SQL}, created_at LIMIT ?",
(limit,),
).fetchall()
for row in rows:
process_row(con, row, dry_run=dry_run)
con.commit()
return {"status": "ok", "processed": len(rows), "db": str(DB), "dry_run": dry_run}
def main() -> int:
parser = argparse.ArgumentParser(description="Dispatch message_outbox WeChat notifications through the selected backend")
parser.add_argument("--once", action="store_true", help="Run one dispatch pass and exit")
parser.add_argument("--dry-run", action="store_true", help="Run backend dry-run if supported and keep messages queued")
parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
parser.add_argument("--interval", type=int, default=int(os.environ.get("AIOS_WECHAT_WORKER_INTERVAL", "10")))
args = parser.parse_args()
if args.once:
print(json.dumps(run_once(args.limit, dry_run=args.dry_run), ensure_ascii=False))
return 0
while True:
print(json.dumps(run_once(args.limit, dry_run=args.dry_run), ensure_ascii=False), flush=True)
time.sleep(args.interval)
if __name__ == "__main__":
raise SystemExit(main())
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""Pluggable WeChat send backend for AIOS / Xiamayiti.
Selection order:
1. AIOS_WECHAT_SEND_BACKEND, if explicitly set;
2. UCC adapter backend, when ucc-wechat-qr-adapter is alive;
3. OpenClaw channel backend, only when channel discovery finds wechat/weixin;
4. unavailable with explicit diagnostics.
This module must not print runtime secrets and must not hard-code account,
target, adapter credentials, or channel names.
"""
from __future__ import annotations
import json
import os
import subprocess
import urllib.error
import urllib.request
from typing import Any, Dict, List, Optional
OPENCLAW_BIN = os.environ.get("AIOS_OPENCLAW_BIN", "openclaw")
OPENCLAW_GATEWAY = os.environ.get("AIOS_OPENCLAW_GATEWAY", "http://127.0.0.1:18789")
OPENCLAW_AUTH_MODE = os.environ.get("AIOS_OPENCLAW_AUTH_MODE", "")
OPENCLAW_AUTH_VALUE = os.environ.get("AIOS_OPENCLAW_AUTH_VALUE", "")
WECHAT_ACCOUNT_ID = os.environ.get("AIOS_WECHAT_ACCOUNT_ID", "")
WECHAT_CHANNEL = os.environ.get("AIOS_WECHAT_CHANNEL", "")
SEND_BACKEND = os.environ.get("AIOS_WECHAT_SEND_BACKEND", "")
UCC_ADAPTER_URL = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_URL", "http://127.0.0.1:18800")
UCC_ADAPTER_TOKEN = os.environ.get("AIOS_UCC_WECHAT_ADAPTER_TOKEN") or os.environ.get("AIOS_WECHAT_QR_ADAPTER_TOKEN", "")
UCC_SEND_PATH = os.environ.get("AIOS_UCC_WECHAT_SEND_PATH", "/api/internal/wechat/send")
class BackendResult:
def __init__(self, ok: bool, backend: str, status: str, data: Dict[str, Any]) -> None:
self.ok = ok
self.backend = backend
self.status = status
self.data = data
def to_dict(self) -> Dict[str, Any]:
return {"ok": self.ok, "backend": self.backend, "status": self.status, **self.data}
def _openclaw_base_args() -> List[str]:
args = [OPENCLAW_BIN]
if OPENCLAW_GATEWAY:
args.extend(["--gateway", OPENCLAW_GATEWAY])
if OPENCLAW_AUTH_MODE and OPENCLAW_AUTH_VALUE:
if OPENCLAW_AUTH_MODE == "token":
args.extend(["--token", OPENCLAW_AUTH_VALUE])
elif OPENCLAW_AUTH_MODE == "password":
args.extend(["--password", OPENCLAW_AUTH_VALUE])
return args
def _run_openclaw(args: List[str], timeout: int = 30) -> subprocess.CompletedProcess[str]:
return subprocess.run(args, text=True, capture_output=True, timeout=timeout, check=False)
def _json_or_raw(text: str) -> Dict[str, Any]:
try:
return json.loads(text or "{}")
except Exception:
return {"raw": (text or "")[:1000]}
def discover_openclaw_wechat_channel() -> Optional[str]:
if WECHAT_CHANNEL:
return WECHAT_CHANNEL
cmd = _openclaw_base_args() + ["channels", "list", "--json"]
proc = _run_openclaw(cmd, timeout=15)
if proc.returncode != 0:
return None
data = _json_or_raw(proc.stdout)
candidates: List[str] = []
def walk(value: Any) -> None:
if isinstance(value, dict):
name = str(value.get("name") or value.get("channel") or value.get("id") or "")
typ = str(value.get("type") or value.get("kind") or "")
joined = f"{name} {typ}".lower()
if ("wechat" in joined or "weixin" in joined) and name:
candidates.append(name)
for child in value.values():
walk(child)
elif isinstance(value, list):
for child in value:
walk(child)
walk(data)
return candidates[0] if candidates else None
def _auth_headers() -> Dict[str, str]:
headers = {"Content-Type": "application/json"}
if UCC_ADAPTER_TOKEN:
headers["Authorization"] = "Bearer " + UCC_ADAPTER_TOKEN
headers["X-AIOS-Token"] = UCC_ADAPTER_TOKEN
return headers
def _unique_paths(paths: List[str]) -> List[str]:
seen = set()
out = []
for path in paths:
if not path.startswith("/"):
path = "/" + path
if path not in seen:
seen.add(path)
out.append(path)
return out
def _ucc_send_paths() -> List[str]:
return _unique_paths([UCC_SEND_PATH, "/api/internal/wechat/send", "/internal/channels/wechat/send"])
def ucc_adapter_alive() -> bool:
for path in ("/health", "/api/internal/health", "/api/health", "/status", "/api/internal/wechat/status"):
try:
req = urllib.request.Request(UCC_ADAPTER_URL.rstrip("/") + path, headers=_auth_headers(), method="GET")
with urllib.request.urlopen(req, timeout=5) as resp:
if 200 <= resp.status < 500:
return True
except Exception:
continue
return False
def select_backend() -> Dict[str, Any]:
explicit = SEND_BACKEND.strip().lower()
if explicit:
return {"backend": explicit, "reason": "explicit"}
if ucc_adapter_alive():
return {"backend": "ucc-adapter", "reason": "ucc_adapter_alive"}
channel = discover_openclaw_wechat_channel()
if channel:
return {"backend": "openclaw-channel", "channel": channel, "reason": "openclaw_channel_discovered"}
return {
"backend": "unavailable",
"reason": "wechat outbound backend unavailable: ucc adapter not reachable; openclaw channel not registered",
}
def send_openclaw_channel(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> BackendResult:
channel = WECHAT_CHANNEL or discover_openclaw_wechat_channel()
if not channel:
return BackendResult(False, "openclaw-channel", "failed", {"code": "openclaw_wechat_channel_not_registered"})
account = account_id or WECHAT_ACCOUNT_ID
if not account:
return BackendResult(False, "openclaw-channel", "failed", {"code": "openclaw_account_not_configured"})
cmd = _openclaw_base_args() + [
"message",
"send",
"--channel",
channel,
"--account",
account,
"--target",
target,
"-m",
content,
]
if dry_run:
cmd.append("--dry-run")
cmd.append("--json")
proc = _run_openclaw(cmd)
if proc.returncode != 0:
return BackendResult(
False,
"openclaw-channel",
"failed",
{"code": "openclaw_send_failed", "error": (proc.stderr or proc.stdout)[:1000], "channel": channel},
)
return BackendResult(
True,
"openclaw-channel",
"sent" if not dry_run else "dry_run",
{"channel": channel, "result": _json_or_raw(proc.stdout)},
)
def _post_ucc(path: str, payload: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
url = UCC_ADAPTER_URL.rstrip("/") + path
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=_auth_headers(), method="POST")
with urllib.request.urlopen(req, timeout=20) as resp:
return resp.status, _json_or_raw(resp.read().decode("utf-8", errors="replace"))
def _ucc_result_is_ok(data: Dict[str, Any], dry_run: bool) -> bool:
status = str(data.get("status") or data.get("delivery_status") or "").lower()
if dry_run:
return bool(data.get("ok") or data.get("accepted")) and status in {"dry_run", "sent", "accepted", "ok"}
return bool(data.get("ok")) and status == "sent"
def send_ucc_adapter(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> BackendResult:
payload = {
"target": target,
"content": content,
"account_id": account_id or WECHAT_ACCOUNT_ID,
"message_id": message_id,
"metadata": metadata or {},
"dry_run": dry_run,
}
last_error: Dict[str, Any] = {}
for path in _ucc_send_paths():
try:
http_status, data = _post_ucc(path, payload)
ok = _ucc_result_is_ok(data, dry_run)
status = str(data.get("status") or ("sent" if ok and not dry_run else "dry_run" if ok else "failed"))
code = str(data.get("code") or status)
return BackendResult(
ok,
"ucc-adapter",
status if ok else "failed",
{"code": code, "http_status": http_status, "endpoint": path, "result": data},
)
except urllib.error.HTTPError as exc:
data = _json_or_raw(exc.read().decode("utf-8", errors="replace"))
last_error = {"code": data.get("code") or "ucc_adapter_http_error", "http_status": exc.code, "endpoint": path, "error": data}
if exc.code in {404, 405}:
continue
return BackendResult(False, "ucc-adapter", "failed", last_error)
except Exception as exc:
last_error = {"code": "ucc_adapter_unavailable", "endpoint": path, "error": str(exc)[:1000]}
continue
return BackendResult(False, "ucc-adapter", "failed", last_error or {"code": "ucc_adapter_unavailable"})
def send_message(
target: str,
content: str,
account_id: str = "",
message_id: str = "",
metadata: Optional[Dict[str, Any]] = None,
dry_run: bool = False,
) -> Dict[str, Any]:
selected = select_backend()
backend = selected.get("backend")
if backend == "ucc-adapter":
return send_ucc_adapter(target, content, account_id, message_id, metadata, dry_run).to_dict()
if backend == "openclaw-channel":
return send_openclaw_channel(target, content, account_id, message_id, metadata, dry_run).to_dict()
if backend == "unavailable":
return BackendResult(False, "unavailable", "failed", {"code": "wechat_outbound_backend_unavailable", "error": selected.get("reason")}).to_dict()
return BackendResult(False, backend or "unknown", "failed", {"code": "unsupported_backend", "backend": backend}).to_dict()
if __name__ == "__main__":
print(json.dumps(select_backend(), ensure_ascii=False, indent=2))
+444
View File
@@ -0,0 +1,444 @@
#!/usr/bin/env python3
"""AIOS Autonomous GitOps Daemon.
Low-impact sidecar daemon for:
- reading GitOps events from a file-backed event bus;
- running safe validation/self-healing commands;
- sending owner notifications through a runtime-injected WeChat/WeCom webhook;
- creating audit records and request artifacts instead of bypassing production gates.
No credentials are stored in this file. Runtime secrets must be injected by the host
through environment variables or secret-mounted files.
"""
from __future__ import annotations
import argparse
import datetime as _dt
import hashlib
import hmac
import json
import os
import pathlib
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.request
from typing import Any, Dict, Iterable, List, Optional, Tuple
ROOT = pathlib.Path(os.environ.get("AIOS_REPO_ROOT", ".")).resolve()
CONFIG_PATH = ROOT / "config" / "codex" / "self-optimizing-orchestrator.json"
PROD_POLICY_PATH = ROOT / "config" / "codex" / "autonomous-production-policy.json"
MERGE_POLICY_PATH = ROOT / "config" / "codex" / "autonomous-merge-policy.json"
EVENT_DIR = pathlib.Path(os.environ.get("AIOS_EVENT_DIR", str(ROOT / "runtime" / "gitops" / "events")))
AUDIT_DIR = pathlib.Path(os.environ.get("AIOS_AUDIT_DIR", str(ROOT / "runtime" / "gitops" / "audit")))
REQUEST_DIR = pathlib.Path(os.environ.get("AIOS_REQUEST_DIR", str(ROOT / "runtime" / "gitops" / "requests")))
REPORT_DIR = pathlib.Path(os.environ.get("AIOS_REPORT_DIR", str(ROOT / "reports" / "codex")))
PREAPPROVAL_DIR = pathlib.Path(os.environ.get("AIOS_PREAPPROVAL_DIR", str(ROOT / "approvals" / "preapproved")))
SAFE_AUTONOMOUS_EVENTS = {
"git_push",
"patch_created",
"validation_failed",
"validation_passed",
"merge_candidate_created",
"runtime_health_degraded",
}
OWNER_NOTIFY_EVENTS = {
"main_version_changed",
"baseline_changed",
"merge_candidate_created",
"validation_failed",
"rollback_request_created",
"approval_required",
"daily_digest",
}
PRODUCTION_ACTIONS = {
"merge_to_main",
"create_release_tag",
"activate_new_baseline",
"production_runtime_change",
"production_rollback_execution",
}
ALWAYS_BLOCKED = {
"force_push",
"purge_baseline",
"delete_version_history",
"delete_audit_records",
"commit_real_secret",
"disable_auth_or_gitea_protection",
}
def utc_now() -> str:
return _dt.datetime.now(_dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def load_json(path: pathlib.Path, default: Any) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return default
except json.JSONDecodeError as exc:
raise RuntimeError(f"Invalid JSON: {path}: {exc}") from exc
def write_json(path: pathlib.Path, data: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
tmp.replace(path)
def event_id(event: Dict[str, Any]) -> str:
raw = json.dumps(event, ensure_ascii=False, sort_keys=True).encode("utf-8")
return hashlib.sha256(raw).hexdigest()[:16]
def audit(action: str, status: str, detail: Dict[str, Any]) -> Dict[str, Any]:
record = {
"ts": utc_now(),
"action": action,
"status": status,
"detail": redact(detail),
}
write_json(AUDIT_DIR / f"{record['ts'].replace(':', '-')}-{action}.json", record)
print(json.dumps(record, ensure_ascii=False), flush=True)
return record
def redact(value: Any) -> Any:
if isinstance(value, list):
return [redact(v) for v in value]
if isinstance(value, dict):
out: Dict[str, Any] = {}
for key, val in value.items():
lowered = key.lower()
sensitive = any(word in lowered for word in ["api_key", "apikey", "password", "secret", "private_key", "credential", "webhook"])
if sensitive and not (isinstance(val, str) and val.startswith("secret_ref://")):
out[key] = "[REDACTED]"
else:
out[key] = redact(val)
return out
return value
def run_cmd(cmd: List[str], timeout: int = 300) -> Tuple[int, str, str]:
result = subprocess.run(
cmd,
cwd=str(ROOT),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
)
return result.returncode, result.stdout, result.stderr
def read_event_files(limit: int = 100) -> List[pathlib.Path]:
EVENT_DIR.mkdir(parents=True, exist_ok=True)
return sorted(EVENT_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime)[:limit]
def archive_event(path: pathlib.Path, status: str) -> None:
target = EVENT_DIR / status / path.name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(target))
def get_webhook_url() -> Optional[str]:
# Runtime-only injection. Do not commit real values.
return os.environ.get("AIOS_OWNER_WECHAT_WEBHOOK_URL") or os.environ.get("AIOS_OWNER_WECOM_WEBHOOK_URL")
def sign_payload(payload: Dict[str, Any]) -> Optional[str]:
secret = os.environ.get("AIOS_OWNER_ACTION_SIGNING_SECRET")
if not secret:
return None
raw = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
return hmac.new(secret.encode("utf-8"), raw, hashlib.sha256).hexdigest()
def send_owner_wechat(event: Dict[str, Any], summary: Dict[str, Any]) -> Dict[str, Any]:
url = get_webhook_url()
if not url:
return {"status": "skipped", "reason": "wechat_webhook_not_configured"}
content = format_owner_message(event, summary)
body = {
"msgtype": "markdown",
"markdown": {"content": content},
}
signature = sign_payload(body)
if signature:
body["aios_signature"] = signature
req = urllib.request.Request(
url,
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = resp.read().decode("utf-8", errors="replace")
return {"status": "sent", "http_status": resp.status, "response": data[:500]}
except urllib.error.URLError as exc:
return {"status": "failed", "error": str(exc)}
def format_owner_message(event: Dict[str, Any], summary: Dict[str, Any]) -> str:
title = event.get("type", "gitops_event")
branch = summary.get("branch") or event.get("branch") or "-"
from_version = summary.get("from_version") or event.get("from_version") or "-"
to_version = summary.get("to_version") or event.get("to_version") or "-"
changed = summary.get("changed_features") or event.get("changed_features") or []
if isinstance(changed, list):
changed_text = ", ".join(str(x) for x in changed[:8]) or "-"
else:
changed_text = str(changed)
return "\n".join([
f"### AIOS GitOps 通报:{title}",
f"- Branch: `{branch}`",
f"- Version: `{from_version}` → `{to_version}`",
f"- Code Δ: +{summary.get('code_lines_added', 0)} / -{summary.get('code_lines_deleted', 0)}",
f"- Config Δ: +{summary.get('config_lines_added', 0)} / -{summary.get('config_lines_deleted', 0)}",
f"- Elapsed: {summary.get('elapsed_since_previous_main_change', '-')}",
f"- Risk: {summary.get('risk', event.get('risk', 'unknown'))}",
f"- Test: {summary.get('test_status', '-')}",
f"- Rollback: `{summary.get('rollback_target', 'V1R1C4')}`",
f"- Changed: {changed_text}",
f"- Web UI: {summary.get('web_ui_audit_url', '/portal/me/#gitops-audit')}",
])
def diff_stats(base: str, branch: str) -> Dict[str, int]:
code_add = code_del = config_add = config_del = 0
rc, stdout, _ = run_cmd(["git", "diff", "--numstat", f"{base}...{branch}"], timeout=60)
if rc != 0:
return {"code_lines_added": 0, "code_lines_deleted": 0, "config_lines_added": 0, "config_lines_deleted": 0}
config_ext = {".json", ".yaml", ".yml", ".toml", ".ini", ".conf", ".service"}
for line in stdout.splitlines():
parts = line.split("\t")
if len(parts) < 3 or parts[0] == "-" or parts[1] == "-":
continue
add, delete, path = int(parts[0]), int(parts[1]), parts[2]
if pathlib.Path(path).suffix in config_ext or path.startswith("config/"):
config_add += add
config_del += delete
else:
code_add += add
code_del += delete
return {
"code_lines_added": code_add,
"code_lines_deleted": code_del,
"config_lines_added": config_add,
"config_lines_deleted": config_del,
}
def build_summary(event: Dict[str, Any]) -> Dict[str, Any]:
branch = event.get("branch") or "patch/web-ui-complete-gitops-dashboard-20260706"
base = event.get("base") or "main"
stats = diff_stats(base, branch)
return {
"event_id": event.get("id") or event_id(event),
"branch": branch,
"from_rev": event.get("from_rev", base),
"to_rev": event.get("to_rev", branch),
"from_version": event.get("from_version", "V1R1C4"),
"to_version": event.get("to_version", "V1R1C5"),
"elapsed_since_previous_main_change": event.get("elapsed_since_previous_main_change", "unknown"),
"changed_features": event.get("changed_features", ["GitOps autonomous orchestration", "Owner notifications", "event bus", "safe daemon"]),
"test_status": event.get("test_status", "pending"),
"risk": event.get("risk", "high"),
"approval_status": event.get("approval_status", "required"),
"rollback_target": event.get("rollback_target", "V1R1C4"),
"web_ui_audit_url": event.get("web_ui_audit_url", "/portal/me/#gitops-audit"),
**stats,
}
def has_preapproval(action: str, event: Dict[str, Any]) -> bool:
target_version = event.get("to_version") or "V1R1C5"
rollback_target = event.get("rollback_target") or "V1R1C4"
now_dt = _dt.datetime.now(_dt.timezone.utc)
for path in PREAPPROVAL_DIR.glob("*.json"):
try:
doc = json.loads(path.read_text(encoding="utf-8"))
except Exception:
continue
allowed = doc.get("allowed_actions", [])
if action not in allowed:
continue
if doc.get("target_version") != target_version:
continue
if doc.get("rollback_target") != rollback_target:
continue
if doc.get("approved_by_role") not in ["owner", "admin"]:
continue
expires_at = doc.get("expires_at")
if not expires_at:
continue
try:
exp = _dt.datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
except ValueError:
continue
if exp > now_dt:
return True
return False
def create_request(event: Dict[str, Any], reason: str) -> Dict[str, Any]:
req = {
"id": f"req-{utc_now().replace(':', '-')}-{event_id(event)}",
"created_at": utc_now(),
"event": redact(event),
"reason": reason,
"status": "pending_owner_review",
"web_ui": "/portal/me/#gitops-owner-actions",
}
write_json(REQUEST_DIR / f"{req['id']}.json", req)
return req
def run_validation(event: Dict[str, Any]) -> Dict[str, Any]:
branch = event.get("branch") or "patch/web-ui-complete-gitops-dashboard-20260706"
base = event.get("base") or "main"
script = ROOT / "scripts" / "validate-gitops-platform-ui.sh"
if not script.exists():
return {"status": "blocked", "reason": "validation_script_missing"}
rc, out, err = run_cmd(["bash", str(script), branch, base], timeout=600)
REPORT_DIR.mkdir(parents=True, exist_ok=True)
(REPORT_DIR / "validation-daemon.log").write_text(out + "\n" + err, encoding="utf-8")
return {"status": "passed" if rc == 0 else "failed", "exit_code": rc, "log": "reports/codex/validation-daemon.log"}
def run_merge_gate(event: Dict[str, Any]) -> Dict[str, Any]:
branch = event.get("branch") or "patch/web-ui-complete-gitops-dashboard-20260706"
base = event.get("base") or "main"
script = ROOT / "scripts" / "autonomous-merge-gate.sh"
if not script.exists():
return {"status": "blocked", "reason": "merge_gate_script_missing"}
rc, out, err = run_cmd(["bash", str(script), branch, base], timeout=600)
(REPORT_DIR / "merge-gate-daemon.log").write_text(out + "\n" + err, encoding="utf-8")
return {"status": "passed" if rc == 0 else "failed", "exit_code": rc, "log": "reports/codex/merge-gate-daemon.log"}
def handle_event(event: Dict[str, Any]) -> Dict[str, Any]:
event_type = event.get("type", "unknown")
action = event.get("action", event_type)
if action in ALWAYS_BLOCKED:
req = create_request(event, f"Blocked permanently by autonomous production policy: {action}")
audit("event.blocked", "blocked", {"event": event, "request": req})
return {"status": "blocked", "request": req}
summary = build_summary(event)
if event_type in OWNER_NOTIFY_EVENTS:
notify_result = send_owner_wechat(event, summary)
audit("owner.notify", notify_result.get("status", "unknown"), {"event": event, "summary": summary, "result": notify_result})
if event_type in {"git_push", "patch_created", "validation_failed"}:
validation = run_validation(event)
audit("codex.validation", validation["status"], {"event": event, "validation": validation})
if validation["status"] != "passed":
req = create_request(event, "Validation failed; GPT repair loop required.")
return {"status": "validation_failed", "request": req, "validation": validation}
gate = run_merge_gate(event)
audit("codex.merge_gate", gate["status"], {"event": event, "gate": gate})
return {"status": "merge_candidate" if gate["status"] == "passed" else "gate_failed", "validation": validation, "gate": gate}
if action in PRODUCTION_ACTIONS:
if has_preapproval(action, event):
# This daemon deliberately records intent. The actual protected write must be
# performed by configured GitOps Actions with server-side authorization.
req = create_request(event, f"Preapproval found for {action}; execute through protected GitOps Action.")
audit("production.preapproved", "request_created", {"event": event, "request": req})
return {"status": "preapproved_request_created", "request": req}
req = create_request(event, f"Production action requires preapproved runbook: {action}")
audit("production.approval_required", "pending", {"event": event, "request": req})
return {"status": "approval_required", "request": req}
audit("event.record", "ok", {"event": event, "summary": summary})
return {"status": "recorded", "summary": summary}
def process_once() -> int:
count = 0
for path in read_event_files():
try:
event = json.loads(path.read_text(encoding="utf-8"))
result = handle_event(event)
archive_event(path, "done")
count += 1
audit("event.processed", "ok", {"path": str(path), "result": result})
except Exception as exc: # noqa: BLE001 - daemon must not crash on single event
archive_event(path, "failed")
audit("event.processed", "failed", {"path": str(path), "error": str(exc)})
return count
def emit_event(args: argparse.Namespace) -> None:
event = {
"id": args.id or f"evt-{utc_now().replace(':', '-')}",
"type": args.type,
"action": args.action or args.type,
"branch": args.branch,
"base": args.base,
"from_version": args.from_version,
"to_version": args.to_version,
"risk": args.risk,
"created_at": utc_now(),
}
write_json(EVENT_DIR / f"{event['id']}.json", event)
print(json.dumps(redact(event), ensure_ascii=False, indent=2))
def main(argv: Optional[Iterable[str]] = None) -> int:
parser = argparse.ArgumentParser(description="AIOS Autonomous GitOps Daemon")
sub = parser.add_subparsers(dest="cmd", required=True)
run = sub.add_parser("run")
run.add_argument("--interval", type=int, default=30)
run.add_argument("--once", action="store_true")
emit = sub.add_parser("emit")
emit.add_argument("--id")
emit.add_argument("--type", required=True)
emit.add_argument("--action")
emit.add_argument("--branch", default="patch/web-ui-complete-gitops-dashboard-20260706")
emit.add_argument("--base", default="main")
emit.add_argument("--from-version", default="V1R1C4")
emit.add_argument("--to-version", default="V1R1C5")
emit.add_argument("--risk", default="high")
args = parser.parse_args(list(argv) if argv is not None else None)
if args.cmd == "emit":
emit_event(args)
return 0
if args.cmd == "run":
EVENT_DIR.mkdir(parents=True, exist_ok=True)
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
REQUEST_DIR.mkdir(parents=True, exist_ok=True)
if args.once:
return 0 if process_once() >= 0 else 1
audit("daemon.start", "ok", {"event_dir": str(EVENT_DIR)})
while True:
process_once()
time.sleep(args.interval)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""AIOS Multi-Agent Consensus Controller
This service aggregates decisions from OpenClaw, Codex, and ChatGPT agents
and computes a bounded consensus decision for GitOps operations.
It does NOT directly modify production state. It only produces decision artifacts.
"""
import json
import os
import pathlib
import time
from typing import Dict, Any
ROOT = pathlib.Path(os.environ.get("AIOS_REPO_ROOT", ".")).resolve()
CONSENSUS_DIR = ROOT / "reports" / "consensus"
AGENT_DIRS = {
"openclaw": ROOT / "reports" / "openclaw",
"codex": ROOT / "reports" / "codex",
"chatgpt": ROOT / "reports" / "chatgpt",
}
CONSENSUS_DIR.mkdir(parents=True, exist_ok=True)
def load_latest(path: pathlib.Path) -> Dict[str, Any]:
if not path.exists():
return {}
files = sorted(path.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
if not files:
return {}
return json.loads(files[0].read_text(encoding="utf-8"))
def compute_consensus(reports: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
weights = {"openclaw": 2, "codex": 2, "chatgpt": 1}
score = 0
for agent, report in reports.items():
status = report.get("status", "unknown")
if status in ("PASS", "approved", "merge_candidate"):
score += weights.get(agent, 1)
elif status in ("FAIL", "blocked"):
score -= weights.get(agent, 1)
if score >= 4:
decision = "approved"
elif score >= 2:
decision = "conditional"
else:
decision = "rejected"
return {
"decision": decision,
"score": score,
"timestamp": time.time(),
}
def main():
reports = {
"openclaw": load_latest(AGENT_DIRS["openclaw"]),
"codex": load_latest(AGENT_DIRS["codex"]),
"chatgpt": load_latest(AGENT_DIRS["chatgpt"]),
}
result = compute_consensus(reports)
out_path = CONSENSUS_DIR / "decision.json"
out_path.write_text(json.dumps(result, indent=2), encoding="utf-8")
print("CONSENSUS:", result)
if __name__ == "__main__":
main()
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""AIOS Event Ingress Server
Receives GitOps events via HTTP and writes them into the local event bus directory.
No authentication secrets are embedded; optional signature validation can be enabled via env.
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import os
import pathlib
import time
EVENT_DIR = pathlib.Path(os.environ.get("AIOS_EVENT_DIR", "runtime/gitops/events"))
EVENT_DIR.mkdir(parents=True, exist_ok=True)
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode('utf-8')
try:
event = json.loads(body)
except Exception:
self.send_response(400)
self.end_headers()
return
event_id = event.get("id", f"evt-{int(time.time())}")
path = EVENT_DIR / f"{event_id}.json"
path.write_text(json.dumps(event, indent=2), encoding='utf-8')
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
def log_message(self, format, *args):
return
if __name__ == "__main__":
port = int(os.environ.get("AIOS_INGRESS_PORT", "8787"))
server = HTTPServer(("0.0.0.0", port), Handler)
print(f"[AIOS] Event ingress running on {port}")
server.serve_forever()
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""V1R1 Personal WeChat Endpoint Discovery
Helps locate active V1R1 personal WeChat runtime endpoints on the host.
"""
import subprocess
import re
CANDIDATES = [8788, 8789, 8790, 8088, 8000]
print("[AIOS] Scanning for V1R1 WeChat endpoints...")
for port in CANDIDATES:
try:
result = subprocess.run(
["bash", "-lc", f"ss -ltnp | grep {port}"],
capture_output=True,
text=True
)
if result.stdout:
print(f"[FOUND] Port {port}: {result.stdout.strip()}")
else:
print(f"[MISS] Port {port}")
except Exception as e:
print(f"[ERROR] Port {port}: {e}")
print("[AIOS] Discovery complete. Please map discovered endpoint to AIOS_V1R1_WECHAT_OUTBOUND_URL")
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""AIOS V1R1 Personal WeChat Outbound Worker.
This worker is the bridge between the Xiamayiti self-evolution loop and the
already-deployed V1R1 personal WeChat capability.
Design rules:
- Do not embed credentials.
- Do not access runtime/secrets directly.
- Read owner-notification outbox JSON files.
- Deliver to a runtime-configured V1R1 notification endpoint, or try local V1R1 candidates.
- Write delivery status for audit/Web UI.
"""
from __future__ import annotations
import datetime as dt
import json
import os
import pathlib
import shutil
import time
import urllib.error
import urllib.request
from typing import Any, Dict, Iterable, List, Optional
ROOT = pathlib.Path(os.environ.get("AIOS_REPO_ROOT", "/data/aios-gitops/workspace"))
BASE = pathlib.Path(os.environ.get("AIOS_RUNTIME_ROOT", "/data/aios-gitops/runtime"))
OUTBOX = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_OUTBOX", str(BASE / "integrations" / "v1r1-personal-wechat" / "outbox")))
SENT = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_SENT", str(BASE / "integrations" / "v1r1-personal-wechat" / "sent")))
FAILED = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_FAILED", str(BASE / "integrations" / "v1r1-personal-wechat" / "failed")))
STATUS = pathlib.Path(os.environ.get("AIOS_V1R1_WECHAT_STATUS", str(BASE / "integrations" / "v1r1-personal-wechat" / "status")))
AUDIT = pathlib.Path(os.environ.get("AIOS_AUDIT_DIR", str(BASE / "audit")))
INTERVAL = int(os.environ.get("AIOS_V1R1_WECHAT_INTERVAL", "10"))
# Runtime-configured endpoint. This can point to the existing V1R1 personal-WeChat module.
PRIMARY_ENDPOINT = os.environ.get("AIOS_V1R1_WECHAT_OUTBOUND_URL", "")
LOCAL_CANDIDATES = [
"http://127.0.0.1:8788/api/v1r1/personal-wechat/send",
"http://127.0.0.1:8788/api/v1r1/notification/owner",
"http://127.0.0.1:8789/api/owner/wechat/send",
"http://127.0.0.1:8790/owner/notify",
]
def now() -> str:
return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def write_json(path: pathlib.Path, value: Dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
tmp.replace(path)
def audit(action: str, status: str, detail: Dict[str, Any]) -> None:
record = {"ts": now(), "action": action, "status": status, "detail": detail}
safe_name = record["ts"].replace(":", "-") + f"-{action}.json"
write_json(AUDIT / safe_name, record)
print(json.dumps(record, ensure_ascii=False), flush=True)
def normalize_message(item: Dict[str, Any]) -> Dict[str, Any]:
title = item.get("title") or "AIOS GitOps 通报"
content = item.get("content") or item.get("message") or json.dumps(item, ensure_ascii=False)
priority = item.get("priority") or "normal"
return {
"event_id": item.get("event_id") or item.get("id") or f"evt-{int(time.time())}",
"channel": "personal_wechat",
"priority": priority,
"title": title,
"content": content,
"source": item.get("source") or "aios-self-evolution",
"branch": item.get("branch"),
"from_version": item.get("from_version"),
"to_version": item.get("to_version"),
"rollback_target": item.get("rollback_target") or "V1R1C4",
"web_ui_audit_url": item.get("web_ui_audit_url") or "/portal/me/#gitops-audit",
"created_at": item.get("created_at") or now(),
}
def endpoints() -> List[str]:
values: List[str] = []
if PRIMARY_ENDPOINT:
values.append(PRIMARY_ENDPOINT)
values.extend(LOCAL_CANDIDATES)
# Preserve order but remove duplicates.
result: List[str] = []
seen = set()
for value in values:
if value and value not in seen:
result.append(value)
seen.add(value)
return result
def post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=8) as resp:
text = resp.read().decode("utf-8", errors="replace")[:500]
return {"ok": 200 <= resp.status < 300, "status": resp.status, "response": text, "url": url}
def deliver(message: Dict[str, Any]) -> Dict[str, Any]:
attempts = []
for url in endpoints():
try:
result = post_json(url, message)
attempts.append(result)
if result.get("ok"):
return {"status": "sent", "endpoint": url, "attempts": attempts}
except urllib.error.URLError as exc:
attempts.append({"ok": False, "url": url, "error": str(exc)})
except Exception as exc: # noqa: BLE001
attempts.append({"ok": False, "url": url, "error": str(exc)})
return {"status": "failed", "attempts": attempts, "reason": "no_endpoint_accepted_message"}
def process_file(path: pathlib.Path) -> None:
try:
item = json.loads(path.read_text(encoding="utf-8"))
except Exception as exc: # noqa: BLE001
failed_target = FAILED / path.name
FAILED.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(failed_target))
audit("v1r1.wechat.parse", "failed", {"path": str(path), "error": str(exc)})
return
message = normalize_message(item)
result = deliver(message)
status_record = {"ts": now(), "file": path.name, "message": message, "delivery": result}
write_json(STATUS / (path.stem + ".status.json"), status_record)
if result.get("status") == "sent":
SENT.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(SENT / path.name))
audit("v1r1.wechat.deliver", "sent", status_record)
else:
FAILED.mkdir(parents=True, exist_ok=True)
shutil.move(str(path), str(FAILED / path.name))
audit("v1r1.wechat.deliver", "failed", status_record)
def process_once() -> int:
OUTBOX.mkdir(parents=True, exist_ok=True)
count = 0
for path in sorted(OUTBOX.glob("*.json"), key=lambda p: p.stat().st_mtime):
process_file(path)
count += 1
return count
def emit_startup_notice() -> pathlib.Path:
OUTBOX.mkdir(parents=True, exist_ok=True)
path = OUTBOX / f"startup-{int(time.time())}.json"
write_json(path, {
"event_id": f"startup-{int(time.time())}",
"type": "owner_notification",
"channel": "personal_wechat",
"priority": "high",
"title": "AIOS 自我进化系统已启动",
"content": "AIOS 虾马一体 self-evolution loop 已进入运行态。daemon、event bus、GitOps 写链路已恢复;后续将通过 V1R1 个人微信模块与 Web UI 审计同步进展。",
"source": "aios-v1r1-wechat-outbound-worker",
"branch": "patch/web-ui-complete-gitops-dashboard-20260706",
"from_version": "V1R1C4",
"to_version": "V1R1C5",
"rollback_target": "V1R1C4",
"web_ui_audit_url": "/portal/me/#gitops-audit",
"created_at": now()
})
return path
def main() -> int:
import argparse
parser = argparse.ArgumentParser(description="AIOS V1R1 personal WeChat outbound worker")
parser.add_argument("--once", action="store_true")
parser.add_argument("--emit-startup-notice", action="store_true")
args = parser.parse_args()
OUTBOX.mkdir(parents=True, exist_ok=True)
SENT.mkdir(parents=True, exist_ok=True)
FAILED.mkdir(parents=True, exist_ok=True)
STATUS.mkdir(parents=True, exist_ok=True)
AUDIT.mkdir(parents=True, exist_ok=True)
if args.emit_startup_notice:
path = emit_startup_notice()
audit("v1r1.wechat.outbox", "created", {"path": str(path)})
if args.once:
process_once()
return 0
audit("v1r1.wechat.worker.start", "ok", {"outbox": str(OUTBOX), "has_primary_endpoint": bool(PRIMARY_ENDPOINT)})
while True:
process_once()
time.sleep(INTERVAL)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,52 @@
# Skill: GitOps Platform Validation
## Name
gitops-platform-validation
## Version
1.0.0
## Purpose
Provide reusable Codex execution behavior for validating GitOps platform UI branches and enabling self-healing CI loops.
## Trigger
- branch matches: patch/*
- contains: gitops-dashboard
## Actions
1. Run validation script
```bash
bash scripts/validate-gitops-platform-ui.sh <branch> main
```
2. If FAIL:
- generate failure report
- invoke GPT repair loop
- re-run validation
3. If PASS:
- run autonomous merge gate
```bash
bash scripts/autonomous-merge-gate.sh <branch> main
```
4. If merge candidate created:
- propose V1R1C5 baseline update
## Constraints
- no secrets in logs
- no direct main write
- destructive operations forbidden
- all merges must pass policy gate
## Self-Healing Behavior
- max iterations: 5
- stop on:
- PASS
- security failure
- manual override
## Output
- validation report
- merge candidate artifact
- optional baseline proposal
@@ -0,0 +1,239 @@
{
"suite": "gitops-platform-ui",
"version": "1.0.0",
"branch_under_test": "patch/web-ui-complete-gitops-dashboard-20260706",
"base_branch": "main",
"purpose": "Validate AIOS GitOps Platform Control Plane UI before merge to main and before V1R1C5 baseline proposal.",
"required_result": "PASS",
"tests": [
{
"id": "STATIC-001",
"group": "static_files",
"required": true,
"description": "Platform UI files exist.",
"assert": {
"files_exist": [
"overlays/user-control-center/portal/gitops-dashboard/index.html",
"overlays/user-control-center/portal/gitops-dashboard/app.js",
"overlays/user-control-center/portal/gitops-dashboard/styles.css",
"overlays/user-control-center/portal/gitops-dashboard/manifest.json",
"overlays/user-control-center/portal/gitops-dashboard/gitops-ui-policy.json",
"overlays/user-control-center/portal/gitops-dashboard/gitops-enterprise-api.contract.json"
]
}
},
{
"id": "STATIC-002",
"group": "static_files",
"required": true,
"description": "Enterprise tabs exist in HTML.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/index.html": [
"tab-overview",
"tab-rbac",
"tab-approval",
"tab-diff",
"tab-patch",
"tab-model",
"tab-release",
"tab-audit"
]
}
}
},
{
"id": "RUNTIME-001",
"group": "frontend_runtime",
"required": true,
"description": "Platform runtime bootstraps on DOMContentLoaded.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/app.js": [
"Platform.bootstrap",
"DOMContentLoaded",
"guardAction",
"createApproval",
"renderDiffTree",
"audit(" ,
"redact("
]
}
}
},
{
"id": "SEC-001",
"group": "security",
"required": true,
"description": "No real credentials are committed.",
"assert": {
"secret_scan": {
"paths": [
"overlays/user-control-center/portal/gitops-dashboard",
"docs/codex",
"tests/acceptance",
"scripts",
"skills"
],
"allow_patterns": [
"secret_ref://",
"[REDACTED]",
"api_key_secret_ref",
"forbiddenFields",
"secret_redaction",
"No credentials"
],
"deny_patterns": [
"Bearer [A-Za-z0-9._-]{20,}",
"sk-[A-Za-z0-9]{20,}",
"ghp_[A-Za-z0-9]{20,}",
"AKIA[0-9A-Z]{16}",
"BEGIN PRIVATE KEY",
"BEGIN RSA PRIVATE KEY"
]
}
}
},
{
"id": "SEC-002",
"group": "security",
"required": true,
"description": "Model registration requires secret_ref and never raw api_key.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/app.js": [
"api_key_secret_ref",
"startsWith('secret_ref://')",
"api_key_secret_ref_required"
]
},
"file_not_contains": {
"overlays/user-control-center/portal/gitops-dashboard/index.html": [
"name=\"api_key\"",
"name=\"token\"",
"name=\"password\""
]
}
}
},
{
"id": "POLICY-001",
"group": "gitops_policy",
"required": true,
"description": "Direct main writes and destructive actions are disabled or blocked.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/app.js": [
"main:write",
"destructive_action_blocked",
"high_risk_requires_approval"
],
"overlays/user-control-center/portal/gitops-dashboard/gitops-ui-policy.json": [
"direct_main_write",
"disabled_in_ui",
"bulk_delete",
"forbidden"
]
}
}
},
{
"id": "API-001",
"group": "api_contract",
"required": true,
"description": "Read/write endpoint contract remains compatible with existing GitOps APIs.",
"assert": {
"file_contains": {
"overlays/user-control-center/portal/gitops-dashboard/gitops-enterprise-api.contract.json": [
"GET /api/base",
"GET /api/ver",
"GET /api/run",
"GET /api/patch",
"GET /api/model",
"POST /api/branch",
"POST /api/patch/plan",
"POST /api/model",
"PUT /api/model-priority",
"POST /api/rollback-request",
"POST /api/tag-request"
]
}
}
},
{
"id": "REGRESSION-001",
"group": "indirect_regression",
"required": true,
"description": "This branch must not alter auth, gitea owner sync, baseline version, or destructive history files.",
"assert": {
"diff_not_touch_paths": [
"config/auth/",
"config/casdoor/",
"config/gitea/",
"versions/V1R1C4.json",
"runtime/gitops/current.json"
],
"diff_not_contain": [
"active_version\": \"V1R1C5",
"force push",
"delete tag",
"purge baseline",
"clear audit"
]
}
},
{
"id": "BASELINE-001",
"group": "baseline_proposal",
"required": true,
"description": "A V1R1C5 proposal exists but is not activated before merge.",
"assert": {
"files_exist": [
"versions/proposals/V1R1C5-gitops-platform-ui.proposed.json"
],
"file_contains": {
"versions/proposals/V1R1C5-gitops-platform-ui.proposed.json": [
"V1R1C5",
"V1R1C4",
"approval_required"
]
}
}
},
{
"id": "SKILL-001",
"group": "auto_loaded_skill",
"required": true,
"description": "Codex/GPT reusable skill exists and references the validation workflow.",
"assert": {
"files_exist": [
"skills/gitops-platform-validation/SKILL.md"
],
"file_contains": {
"skills/gitops-platform-validation/SKILL.md": [
"validate-gitops-platform-ui.sh",
"gitops-platform-ui.acceptance.json",
"self-healing"
]
}
}
}
],
"merge_gate": {
"allow_merge_to_main_when": [
"all required tests PASS",
"gray deployment PASS",
"no secret leak",
"no direct main write",
"V1R1C5 proposal reviewed",
"owner/admin approval recorded"
],
"block_merge_when": [
"any required test FAIL",
"any required test BLOCKED without explicit owner override",
"secret leak detected",
"destructive operation detected",
"baseline active version changed before merge"
]
}
}
@@ -0,0 +1,51 @@
{
"version": "V1R1C5",
"status": "proposed",
"activation": "approval_required",
"base_version": "V1R1C4",
"rollback_target": "V1R1C4",
"purpose": "Promote AIOS GitOps Platform Control Plane with autonomous CI, self-healing validation, bounded production policy, event bus, daemon, owner notification, Web UI audit and Codex validation skills.",
"risk": "high",
"approval": {
"required": true,
"roles": ["owner", "admin"],
"reason": "Introduces autonomous orchestration and production-adjacent daemon. Main merge, tag creation and baseline activation remain gated."
},
"scope": {
"direct_changes": [
"overlays/user-control-center/portal/gitops-dashboard",
"services/aios-autonomous-daemon",
"config/codex",
"scripts",
"tests/acceptance",
"skills",
"docs/codex",
"docs/ops",
"deploy/systemd"
],
"indirect_impact": [
"Portal GitOps UI visibility",
"Owner notification workflow",
"Codex validation workflow",
"Self-healing patch loop",
"Web UI audit trail"
]
},
"non_goals": [
"No direct main push",
"No committed secrets",
"No destructive history deletion",
"No production rollback execution without approval or preapproved runbook"
],
"validation": {
"acceptance_suite": "tests/acceptance/gitops-platform-ui.acceptance.json",
"script": "scripts/validate-gitops-platform-ui.sh",
"merge_gate": "scripts/autonomous-merge-gate.sh",
"gray_validation": "scripts/gray-validate-gitops-platform-ui.sh"
},
"owner_notification": {
"required_on_main_version_change": true,
"channels": ["wechat_or_wecom", "web_ui_audit"],
"webhook_secret_ref": "secret_ref://aios/notifications/owner-wechat-webhook"
}
}