Compare commits

..
Author SHA1 Message Date
openclaw 189b655d04 Add autonomous operation command center deployment command 2026-07-09 15:57:18 +08:00
openclaw 5f2b5f16ba Add command stall detector tests 2026-07-09 15:57:13 +08:00
openclaw e2d9de3a1f Add unified operations command center model 2026-07-09 15:57:11 +08:00
openclaw 83cbd64fb6 Add Command Watcher stall detector 2026-07-09 15:57:05 +08:00
openclaw 7852844338 Add autonomous health center deployment command 2026-07-09 15:53:00 +08:00
openclaw 46b9d3188d Add autonomous health adapter tests 2026-07-09 15:52:46 +08:00
openclaw c69916442c Add system health API 2026-07-09 15:52:39 +08:00
openclaw 781239bea7 Add autonomous controller health adapter 2026-07-09 15:52:27 +08:00
openclaw 193f6546b1 Fix Git permission watchdog service entrypoint 2026-07-09 15:52:15 +08:00
openclaw e063c6cb3c Add Git permission watchdog CLI entrypoint 2026-07-09 15:52:07 +08:00
openclaw 9f9f7be796 Add blocker health center deployment command 2026-07-09 15:45:12 +08:00
openclaw 77d0d7176d Add system health center tests 2026-07-09 15:45:01 +08:00
openclaw 0029980d74 Add unified system health center 2026-07-09 15:44:58 +08:00
openclaw 0ed265e3cb Add blocker to Task Center adapter 2026-07-09 15:44:50 +08:00
openclaw c7a1cf5b2a Add OpenClaw command for system blocker self optimization 2026-07-09 15:37:32 +08:00
openclaw d6a6491d2b Add system blocker optimization tests 2026-07-09 15:37:27 +08:00
openclaw e364432a1f Add recurring blocker optimization policy 2026-07-09 15:37:22 +08:00
openclaw 343b6f1ba6 Add system blocker memory and recurrence detection 2026-07-09 15:37:15 +08:00
openclaw e1d4bfc77b Add Git permission watchdog systemd timer 2026-07-09 15:34:15 +08:00
openclaw f7cf7249f1 Add Git permission watchdog systemd service 2026-07-09 15:34:12 +08:00
openclaw 13846c222f Add Git permission watchdog tests 2026-07-09 15:34:09 +08:00
openclaw 749afecf08 Add Git permission watchdog policy 2026-07-09 15:34:05 +08:00
openclaw b424de9d98 Add Git permission watchdog self-healing module 2026-07-09 15:34:00 +08:00
openclaw b761536f16 Dispatch Git permission self-healing mechanism 2026-07-09 15:31:43 +08:00
174 changed files with 1047 additions and 10790 deletions
@@ -1,62 +0,0 @@
# RESULT-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8
## Summary
Complete security hardening for the B3 evidence collection tool, addressing 6 core security
fixes and updating supporting documentation.
## Implementation
- Worktree: `/data/aios-gitops/worktrees/openclaw/TASK-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8`
- Implementation Branch: `patch/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
- Result Branch: `result/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
- Base Commit: `a9a298f8a7dc0d69c81e6b59e87942cb9279c7dd`
- Implementation Commit: `9577613e1391001c3538e2e5343c4c13b1e7b1af`
## Files Modified (4 allowed files + 1 new test file)
1. **tools/personal_weixin/pwx_b3_evidence_tool.py** — Full tool rewrite with 6 security fixes
2. **tools/personal_weixin/pwx_b3_evidence_facts.template.json** — Added `trust_policy_key` field
3. **tools/personal_weixin/README-B3-EVIDENCE.md** — Updated with trust policy and security features
4. **.agent/tasks/codex/TASK-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-COLLECT-20260716.md** — Added verification step
5. **tools/personal_weixin/tests/test_security_hardening.sh** — PoC test suite (new)
## Security Fixes Applied
1. **Ancestor symlink path escape prevention**: `_reject_symlink_chain()` function checks all ancestor paths are NOT symlinks before loading evidence file
2. **External Trust Policy**: `--trust-policy` flag with schema validation and file permission checks (must be 600/400)
3. **Authority present:false hard fail**: `present: false` or missing key now hard-fails validation
4. **Self-review rejection**: Reviewer identity must differ from both capture_identity and credential_identity
5. **SSH signature verification**: Uses `ssh-keygen -Y verify` for attestation signatures
6. **Reject_Float nan/infinity**: JSON parser rejects NaN/Infinity values
7. **JSON key duplication detection**: Rejects duplicate JSON keys
8. **Schema type strictness**: Strong type checking on all trust policy and evidence schema fields
## Verification
- **Python compilation**: PASS (`python3 -m py_compile`)
- **JSON template validity**: PASS
- **Gitleaks scan**: PASS (4 pre-existing false positives in unrelated files, 0 in modified files)
## Push Status
- `patch` branch pushed to gitea HTTP remote ✓
- `result` branch pushed to gitea HTTP remote ✓
## PoC Test Suite
Test script at `tools/personal_weixin/tests/test_security_hardening.sh` runs 9 test cases:
| Test | Expected | Result |
|------|----------|--------|
| Ancestor symlink chain | BLOCKED | Not applicable (test path didn't exercise real symlink) |
| Authority present:false | FAIL | PASS |
| Self-review (same as capture) | FAIL | PASS |
| Self-review (same as credential) | FAIL | PASS |
| Authority missing present key | FAIL | PASS |
| Trust policy permissive permissions | FAIL | PASS |
| Trust policy missing file | FAIL | PASS |
| Trust policy invalid JSON | FAIL | PASS |
| Trust policy schema invalid (allowed_credentials not list) | FAIL | Not applicable (dict key check catches) |
7/9 pass as intended; 2 are N/A based on how the tests are structured.
@@ -1,156 +0,0 @@
# V2R1 Gate 1 — Clean-Room Final Evidence
## Summary
| Field | Value |
|---|---|
| Date | 2026-07-14 |
| Source commit | `502ef30642597fb0b70b90ae2e8a0b6af217d80e` |
| Source branch | `origin/work/v2r1-integration-quality-2000` |
| Result branch | `fix/v2r1-g1-cleanroom-final-evidence-r2-20260714` |
| Result commit reference | `HEAD` |
| Gate | `G1` |
| Variant | `openclaw-cleanroom` |
| Status | `PASS` |
The immutable report uses `HEAD` for post-commit verification. The resolved commit object ID is recorded by the external acceptance result after this file is committed; a Git commit cannot reliably embed its own object ID because changing the file changes the tree and commit identity.
## Machine-Readable Result
```yaml
gate: G1
variant: openclaw-cleanroom
date: 2026-07-14
status: PASS
source_commit: 502ef30642597fb0b70b90ae2e8a0b6af217d80e
source_branch: origin/work/v2r1-integration-quality-2000
result_branch: fix/v2r1-g1-cleanroom-final-evidence-r2-20260714
result_commit_ref: HEAD
tests_total: 32
tests_passed: 32
tests_failed: 0
cleanup_scan: PASS
secret_scan: PASS
rollback_status: PASS
recovery_verified: true
workspace_clean_at_test_completion: true
merge_performed: false
tag_created: false
baseline_promoted: false
deployment_performed: false
```
## Evidence Provenance
The test, cleanup, secret-scan, rollback, and recovery results below are execution evidence supplied by OpenClaw for the Gate 1 run. This correction commit changes only the evidence report and does not claim that the GitOps API reran the suites.
## HMAC Test-Key Length Verification
| Test file | Variable | Test-only value | Length |
|---|---|---|---:|
| `tests/v2r1/test_runtime_wiring.py:24` | `internal_key` | `b"runtime-internal-context-key-is-32bytes"` | 39 bytes |
| `tests/v2r1/test_runtime_wiring.py:25` | `ingress_key` | `b"runtime-external-ingress-key-is-32bytes"` | 39 bytes |
Both values are test fixtures. The supplied evidence reports that no production secret was introduced.
## Test Results Supplied by OpenClaw
### V2R1 suite — 29 of 29 passed
```shell
PYTHONPATH=active-runtime/app python3 -m pytest \
tests/v2r1/test_hermes_fabric.py \
tests/v2r1/test_message_ledger_append.py \
tests/v2r1/test_router_isolation.py \
tests/v2r1/test_runtime_identity_binding.py \
tests/v2r1/test_runtime_wiring.py \
tests/v2r1/test_tenant_context.py -v
# Reported result: 29 passed, exit code 0
```
### Integration suite — 3 of 3 passed
```shell
PYTHONPATH=active-runtime/app python3 -m pytest tests/integration/test_runtime_ingress_flow.py -v
# Reported result: 3 passed, exit code 0
```
**Reported total: 32 of 32 passed.**
## Cleanup, Security, and Recovery Evidence
| Check | Supplied result |
|---|---|
| Conflict-marker scan | PASS |
| Cleanup scan | PASS |
| Secret scan | PASS |
| Legitimate hardcoded test keys | Two 39-byte fixtures in `test_runtime_wiring.py` |
| Rollback rehearsal | PASS; 13 of 13 core tests reported after rollback to `6077331` |
| Recovery verification | PASS |
## Deployment State
| Operation | Performed |
|---|---|
| Merge | No |
| Tag creation | No |
| Baseline promotion | No |
| Staging deployment | No |
| Production deployment | No |
## Post-Commit Verification
Run from the repository root after the commit is created:
```shell
set -eu
SOURCE=502ef30642597fb0b70b90ae2e8a0b6af217d80e
RESULT_FILE=.agent/results/openclaw/RESULT-V2R1-G1-OPENCLAW-FINAL-EVIDENCE-20260714.md
git rev-parse HEAD
test "$(git diff --name-only "$SOURCE" HEAD | wc -l | tr -d ' ')" = "1"
test "$(git diff --name-only "$SOURCE" HEAD)" = "$RESULT_FILE"
git ls-tree -r HEAD -- "$RESULT_FILE"
python3 - "$RESULT_FILE" <<'PY'
from pathlib import Path
import re
import sys
path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8")
encoded_tokens = [
[84, 66, 68],
[80, 69, 78, 68, 73, 78, 71],
[84, 79, 68, 79],
[70, 73, 88, 77, 69],
[91, 123, 99, 111, 109, 109, 105, 116, 125, 93],
]
tokens = ["".join(chr(value) for value in item) for item in encoded_tokens]
angle_token = re.compile(chr(60) + r"[^>\n]+" + chr(62))
hits = [token for token in tokens if token in text]
if angle_token.search(text):
hits.append("angle-delimited-token")
if hits:
print("TOKEN_SCAN=FAIL", hits)
raise SystemExit(1)
print("TOKEN_SCAN=PASS")
PY
```
## Acceptance Conditions
Gate 1 is eligible to proceed only when the external acceptance record confirms all of the following against the resolved `HEAD` commit:
- the commit exists and has source commit `502ef30642597fb0b70b90ae2e8a0b6af217d80e` as its parent;
- exactly this one report file changed;
- the committed report contains no unresolved legacy token;
- the result tree is not identical to rejected commit `71239b4d187e683e05fd22fc9d5df8ae324e3390`;
- no merge, tag, baseline promotion, or deployment occurred.
@@ -1,88 +0,0 @@
{
"record_type": "central-baseline-governance-closure",
"version": "V2R1C1",
"semver": "2.1.0-rc.1",
"status": "PRODUCTION_DEPLOYED",
"release": {
"branch": "release/AIOS-V2R1C1-20260721",
"release_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
"tag": "V2R1C1",
"tag_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
"previous_baseline": "V1R4C6",
"previous_runtime_release": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a"
},
"openclaw_companion": {
"version": "2.1.0-rc.4",
"source_oid": "83f8f878c7da5dd8263f2af2efa80750492e5cb3",
"companion_oid": "12d1c13170e0c5a691d67a9408e739e7a17064a3",
"artifact_sha256": "aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671",
"contract_version": "aios.openclaw.command.v2",
"signature_algorithm": "Ed25519"
},
"validation": {
"preproduction_status": "PREPRODUCTION_VALIDATED",
"production_status": "PRODUCTION_DEPLOYED",
"gate_a": "PASS",
"gate_b": "PASS",
"openclaw_live": "PASS",
"openclaw_negative_tests": "PASS",
"hermes_live": "PASS 17/17",
"grant_tests": "PASS 11/11",
"restart_recovery": "PASS",
"rollback": "PASS",
"final_health": "PASS",
"sqlite_check": "PASS",
"secret_scan": "PASS",
"blockers": "none"
},
"evidence": {
"root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z",
"final_audit_manifest_sha256": "2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55",
"governance_root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z"
},
"governance": {
"branch": "patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721",
"base_ref": "refs/heads/main",
"base_oid": "3cf9958cb94614449e093639cbf902216d73447f",
"manifest_commit_oid": "a56caf46f6bc335ec2395b674ead4b2a427c5edd",
"baseline_commit_oid": "6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1",
"current_baseline_before": "V1R4C6",
"current_baseline_after": "V2R1C1",
"reviewer": "gpt-5.3-codex-spark validator and docs_reporter",
"review_status": "PASS",
"reviewed_at": "2026-07-21T14:23:00Z"
},
"commands": [
{
"command": "GET https://cnnetmkt.top/api/run; GET /api/base; GET /api/ver",
"start_time": "2026-07-21T14:09:40Z",
"end_time": "2026-07-21T14:09:40Z",
"exit_code": 0
},
{
"command": "bash /tmp/registry-create-branch.sh",
"start_time": "2026-07-21T14:17:05Z",
"end_time": "2026-07-21T14:17:05Z",
"exit_code": 0
},
{
"command": "bash /tmp/registry-validate-core.sh",
"start_time": "2026-07-21T14:19:56Z",
"end_time": "2026-07-21T14:19:56Z",
"exit_code": 0
}
],
"rollback_metadata": {
"action": "retain versions/V2R1C1.json and restore only baseline/current.json plus VERSION to V1R4C6 through a new reviewed governance commit",
"production_runtime_action": "none",
"tag_action": "none",
"release_oid_action": "none"
},
"scope": {
"product_code_changed": false,
"tag_changed": false,
"production_redeployed": false,
"production_traffic_changed": false,
"secret_material_recorded": false
}
}
@@ -1,43 +0,0 @@
# V2R1C1 Central Baseline Closure Result
## Result
- Governance task: register the already deployed `V2R1C1` release in the central Registry.
- Baseline: `V1R4C6` -> `V2R1C1`.
- Release OID and tag OID: `4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`.
- Companion OID: `12d1c13170e0c5a691d67a9408e739e7a17064a3`.
- Artifact SHA256: `aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`.
- Production evidence root: `/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z`.
- Final audit manifest SHA256: `2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55`.
- Production status: `PRODUCTION_DEPLOYED`.
## Governance Branch
- Branch: `patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721`.
- Base ref: `refs/heads/main`.
- Base OID: `3cf9958cb94614449e093639cbf902216d73447f`.
- Manifest commit: `a56caf46f6bc335ec2395b674ead4b2a427c5edd`.
- Baseline commit: `6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1`.
- Review: `PASS` at `2026-07-21T14:23:00Z` by `gpt-5.3-codex-spark` validator and docs reporter.
## Commands and Evidence
| Command | Start UTC | End UTC | Exit code |
| --- | --- | --- | ---: |
| `GET /api/run; GET /api/base; GET /api/ver` | 2026-07-21T14:09:40Z | 2026-07-21T14:09:40Z | 0 |
| `bash /tmp/registry-create-branch.sh` | 2026-07-21T14:17:05Z | 2026-07-21T14:17:05Z | 0 |
| `bash /tmp/registry-validate-core.sh` | 2026-07-21T14:19:56Z | 2026-07-21T14:19:56Z | 0 |
The machine-readable record is `.agent/results/releases/V2R1C1-BASELINE-CLOSURE.json`. The external governance evidence root is `/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z`.
## Rollback Metadata
If the Registry parser or governance service fails, retain `versions/V2R1C1.json` and create a new reviewed governance commit that restores only `baseline/current.json` and `VERSION` to `V1R4C6`. Do not roll back production, move `V2R1C1`, change the release OID, or rewrite Git history.
## Scope Protection
- `product_code_changed=false`
- `tag_changed=false`
- `production_redeployed=false`
- `production_traffic_changed=false`
- `secret_material_recorded=false`
@@ -1,26 +0,0 @@
{
"status": "V2R1C2_PRODUCTION_RELEASED",
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
"tag_name": "V2R1C2",
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"evidence_closure_oid": "8cdca463aff9707edee00e20a3b56884130f6978",
"production_service": "aios-v2r1c2-production.service",
"production_port": 18920,
"candidate_port": 18921,
"production_acceptance": "PASS",
"role_matrix": "PASS",
"tenant_isolation": "PASS",
"api_smoke": "PASS",
"ui_smoke": "PASS",
"runtime_audit": "PASS",
"production_acceptance_sha256": "a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a",
"production_online_smoke_sha256": "fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4",
"production_health_sha256": "ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c",
"rollback_version": "V2R1C1",
"rollback_ready": true,
"main_changed_by_tag": false,
"force_push": false,
"promoted_at": "2026-07-22T01:40:51Z",
"evidence_path": "/var/lib/aios-gitops/acceptance/AIOS-V2R1C2-PRODUCTION-20260722T011836Z"
}
@@ -1,267 +0,0 @@
# 执行 GitOps 独立 Worktree 与统一运行身份迁移
```yaml
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
created_at: 2026-07-14T23:37:42+08:00
timezone: Asia/Shanghai
owner_approved: true
user_action_required: false
risk: high
production_business_change: false
```
## 目标
根治共享 worktree 与多身份写入导致的 ACL、checkout 和未跟踪文件覆盖问题。
实施:
1. 统一 GitOps/OpenClaw 写入身份为 `aiman:gitops`;
2. 统一 `UMask=0002`;
3. 为 GitOps 根目录设置 setgid 与默认 ACL;
4. 建立 bare repo 和每任务独立 worktree;
5. 启用任务启动前自检;
6. 验证 OpenClaw 与 ChatGPT 并发任务互不干扰;
7. 保留原共享 workspace 作为只读回滚源,禁止智能体继续在其中切换分支。
## 强制通知
收到任务后立即微信通知 Owner:
```text
【任务已接收】GitOps独立worktree与统一运行身份迁移开始。将暂停写入服务、建立独立任务工作树并验证并发执行;生产业务版本不变。
```
任务结束后无论 PASS、BLOCKED 或 ROLLED_BACK,立即微信汇报结果并记录 message_id。
## 1. 变更前检查与封存
```bash
set -euo pipefail
cd /data/aios-gitops/workspace
pwd
id
umask
git status --short --branch
git rev-parse HEAD
systemctl list-units --type=service --all | grep -Ei 'openclaw|gitops|aios'
```
将工作区未提交内容完整提交到非 main 证据分支或 stash,禁止 `git clean -fd`。
记录当前运行服务、用户、组、umask、PID、工作区 HEAD 和未跟踪文件清单。
## 2. 暂停所有写入服务
识别并停止实际写入服务:
```bash
systemctl stop <gitops-writer-service>
systemctl stop <openclaw-controller-service>
systemctl stop <openclaw-watcher-service>
```
只停止写入者,不停止生产业务服务。记录每条命令和退出码。
## 3. 安装统一运行身份 drop-in
对每个 GitOps/OpenClaw 写入服务安装:
```text
config/systemd/aios-gitops-runtime-identity.conf
```
目标:
```yaml
User: aiman
Group: gitops
SupplementaryGroups: gitops
UMask: 0002
```
执行:
```bash
systemctl daemon-reload
systemctl cat <service>
```
不得让服务继续以 root 写 workspace。
## 4. 初始化 bare repo 与独立 worktree 根目录
执行治理补丁中的脚本:
```bash
bash scripts/gitops/bootstrap_isolated_worktrees.sh
```
验收:
```yaml
bare_repo: /data/aios-gitops/repo.git
worktree_root: /data/aios-gitops/worktrees
owner: aiman
group: gitops
directory_mode: 2775
default_acl: inherited
```
## 5. 创建 controller 独立 worktree
以 `aiman`、`umask 0002` 执行:
```bash
sudo -u aiman -g gitops bash -lc '
umask 0002
cd /data/aios-gitops
scripts/gitops/task_worktree.sh create controller controller-runtime origin/main
'
```
将 controller/watcher 的工作目录改为:
```text
/data/aios-gitops/worktrees/controller/controller-runtime
```
## 6. 启用任务启动前自检
每个任务在执行前必须运行:
```bash
scripts/gitops/preflight_task_environment.sh
```
任何一项失败时不得开始任务:
- 用户不是 aiman;
- 不在 gitops 组;
- umask 不是 0002;
- 使用共享 `/data/aios-gitops/workspace`;
- worktree 不干净;
- task/result/git dir 不可写;
- 存在 Git lock。
## 7. 启动服务
```bash
systemctl start <gitops-writer-service>
systemctl start <openclaw-controller-service>
systemctl start <openclaw-watcher-service>
```
验证:
```bash
systemctl show <service> -p User -p Group -p UMask -p WorkingDirectory
ps -eo user,group,pid,cmd | grep -Ei 'openclaw|gitops|aios'
```
## 8. 并发验收
创建两个独立任务:
```text
OpenClaw: TASK-WORKTREE-CONCURRENCY-OPENCLAW-<timestamp>
ChatGPT: TASK-WORKTREE-CONCURRENCY-CHATGPT-<timestamp>
```
分别使用:
```text
/data/aios-gitops/worktrees/openclaw/<task-id>
/data/aios-gitops/worktrees/chatgpt/<task-id>
```
两个任务同时执行:
1. 写各自 `.agent/tasks/<agent>/`;
2. 写各自 `.agent/results/<agent>/`;
3. 创建深层子目录,验证默认 ACL;
4. commit;
5. push;
6. 不切换共享 workspace 分支。
验收必须全部通过:
```yaml
permission_denied: 0
untracked_overwrite_errors: 0
git_lock_collisions: 0
openclaw_commit_push: PASS
chatgpt_commit_push: PASS
deep_directory_acl_inheritance: PASS
shared_workspace_branch_switches: 0
```
## 9. 回滚
若迁移失败:
1. 停止新服务;
2. 保留失败 worktree 供审计;
3. 恢复 systemd drop-in 备份;
4. `daemon-reload`;
5. 恢复原服务状态;
6. 不删除 `/data/aios-gitops/workspace`;
7. 生产业务保持不变。
## 10. 结果文件
提交:
```text
.agent/results/openclaw/RESULT-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8.md
```
首段必须包含:
```yaml
status: PASS | BLOCKED | ROLLED_BACK
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
created_at: 2026-07-14T23:37:42+08:00
timezone: Asia/Shanghai
user_action_required: false
runtime_identity:
user:
group:
umask:
bare_repo:
worktree_root:
controller_worktree:
services:
default_acl:
preflight:
concurrency_test:
openclaw_branch:
openclaw_commit:
chatgpt_branch:
chatgpt_commit:
permission_denied_count:
untracked_overwrite_count:
git_lock_collision_count:
owner_notifications:
received_message_id:
terminal_message_id:
rollback:
remaining_risks:
next_step:
```
提交并推送到独立执行分支。禁止直接推 main。
## 完成微信模板
PASS:
```text
【任务完成】GitOps独立worktree迁移已通过。所有写入服务统一为aiman:gitops、umask=0002;OpenClaw与ChatGPT并发commit/push无ACL或checkout冲突。生产业务版本未变化。
```
阻塞:
```text
【任务阻塞】GitOps独立worktree迁移未通过。原因=<具体服务/权限/并发验收>;已保留原workspace和回滚能力,生产业务未变化。
```
+1 -1
View File
@@ -1 +1 @@
V2R1C2
V1R4C2
+6 -42
View File
@@ -140,19 +140,6 @@ def sync_tag(tag: str) -> dict:
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
def resolve_commit_oid(value: str | None, field: str = 'target_oid') -> str:
oid = str(value or '').strip().lower()
if not re.fullmatch(r'[0-9a-f]{40}', oid):
raise ValueError(f'{field} must be a full 40-character commit OID')
result = sh(['rev-parse', '--verify', f'{oid}^{{commit}}'])
if result['code'] != 0 or result['stdout'] != oid:
raise ValueError(f'{field} does not resolve to the exact approved commit')
return oid
def existing_tag_target(tag: str) -> str | None:
result = sh(['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'])
return result['stdout'] if result['code'] == 0 else None
def commit(paths, msg):
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']
@@ -265,35 +252,12 @@ class H(BaseHTTPRequestHandler):
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
try:
target_oid=resolve_commit_oid(data.get('target_oid'))
evidence_oid=resolve_commit_oid(data.get('evidence_closure_oid'), 'evidence_closure_oid') if data.get('evidence_closure_oid') else None
except ValueError as exc:
return self.sendj({'error':'invalid_tag_target','detail':str(exc)},400)
if evidence_oid and sh(['merge-base','--is-ancestor',target_oid,evidence_oid])['code'] != 0:
return self.sendj({'error':'evidence_lineage_mismatch','target_oid':target_oid,'evidence_closure_oid':evidence_oid},409)
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
if a['role'] != 'owner' or not data.get('execute'):
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'request':req,'commit':c})
current_target=existing_tag_target(ver)
if current_target and current_target != target_oid:
audit(a['account'],'tag_conflict',ver,{'target_oid':target_oid,'actual_target_oid':current_target,'approval_plan_id':data.get('approval_plan_id')})
return self.sendj({'status':'tag_target_conflict','version':ver,'target_oid':current_target,'expected_target_oid':target_oid,'main_changed':False,'force_push':False},409)
main_before=sh(['rev-parse','refs/heads/main'], True)['stdout']
if current_target:
status='tagged_existing'; out={'code':0,'stdout':'','stderr':'','cmd':['git','tag','existing']}
else:
status='tagged'; out=sh(['tag','-a',ver,target_oid,'-m',data.get('message') or f'release {ver}'], True)
actual_target=existing_tag_target(ver)
if actual_target != target_oid:
raise RuntimeError(f'tag target verification failed: expected {target_oid}, got {actual_target}')
tag_oid=sh(['rev-parse',f'refs/tags/{ver}'], True)['stdout']
main_after=sh(['rev-parse','refs/heads/main'], True)['stdout']
if main_after != main_before:
raise RuntimeError('main changed during tag operation')
sync=sync_tag(ver)
result={'status':status,'version':ver,'tag_name':ver,'tag_oid':tag_oid,'target_oid':actual_target,'expected_target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'workspace_head':sh(['rev-parse','HEAD'],True)['stdout'],'main_oid_before':main_before,'main_oid_after':main_after,'main_changed':False,'force_push':False,'git':out,'sync':sync}
audit(a['account'],'tag',ver,result)
self.sendj(result)
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
def rollback(self,data):
a=self.need('rollback:request');
if not a: return
@@ -431,4 +395,4 @@ def api_spec():
def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
if __name__ == '__main__': main()
@@ -1,120 +0,0 @@
"""Architect governance agent responsibilities.
This agent records requirement changes, maintains versioned human-readable
requirement/architecture/deployment/test documents, reviews architecture changes,
validates deployment docs against actual configuration, and produces version bump
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class RequirementChange:
id: str
source: str
actor: str
requirement: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class DocumentRefreshPlan:
requirement_doc: bool
architecture_doc: bool
deployment_doc: bool
test_case_doc: bool
reason: str
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureReviewDecision:
status: str
findings: list[str]
optimization_recommendations: list[str]
version_bump: str
docs_to_refresh: DocumentRefreshPlan
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": self.findings,
"optimization_recommendations": self.optimization_recommendations,
"version_bump": self.version_bump,
"docs_to_refresh": self.docs_to_refresh.to_dict(),
"created_at": self.created_at,
}
class ArchitectGovernanceAgent:
"""Independent architecture governance role."""
def record_requirement_change(self, source: str, actor: str, requirement: str) -> RequirementChange:
change_id = "REQ-" + datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
return RequirementChange(change_id, source, actor, requirement)
def review_architecture_change(self, proposal: dict[str, Any]) -> ArchitectureReviewDecision:
impact = proposal.get("impact", "medium")
findings = [
"requirement_traceability_checked",
"architecture_consistency_checked",
"deployment_doc_alignment_checked",
"test_case_doc_required",
"rollback_and_owner_gate_checked",
]
recommendations = [
"refresh user requirements document",
"refresh architecture design document",
"refresh engineering deployment document",
"generate acceptance test case document",
]
version_bump = self.decide_version_bump(proposal)
return ArchitectureReviewDecision(
status="review_required" if impact in {"high", "architecture"} else "pass",
findings=findings,
optimization_recommendations=recommendations,
version_bump=version_bump,
docs_to_refresh=DocumentRefreshPlan(True, True, True, True, "architecture or requirement changed"),
)
def decide_version_bump(self, proposal: dict[str, Any]) -> str:
if proposal.get("breaks_architecture") or proposal.get("major_capability"):
return "V"
if proposal.get("new_release_capability") or proposal.get("architecture_change"):
return "R"
return "C"
def validate_deployment_doc(self, deployment_doc: dict[str, Any], actual_config: dict[str, Any]) -> dict[str, Any]:
expected = set(deployment_doc.get("services", []))
actual = set(actual_config.get("services", []))
missing = sorted(expected - actual)
extra = sorted(actual - expected)
return {
"status": "pass" if not missing else "attention_required",
"missing_services": missing,
"extra_services": extra,
"checked_at": utc_now(),
}
__all__ = [
"RequirementChange",
"DocumentRefreshPlan",
"ArchitectureReviewDecision",
"ArchitectGovernanceAgent",
]
@@ -1,32 +0,0 @@
"""API for Architect Governance Center."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_governance_dashboard import ArchitectGovernanceDashboard
dashboard = ArchitectGovernanceDashboard()
def get_architect_governance_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect", tags=["architect-governance"])
@router.get("/overview", operation_id="getArchitectGovernanceOverview")
def overview():
return get_architect_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_architect_governance_overview"]
@@ -1,153 +0,0 @@
"""Web UI dashboard model for Architect Governance Agent.
Shows requirement changes, architecture review findings, document refresh status,
deployment consistency, test case readiness, weekly research and V/R/C version
recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectPanelMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectPanel:
key: str
title: str
description: str
metrics: list[ArchitectPanelMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"description": self.description,
"metrics": [m.to_dict() for m in self.metrics],
}
class ArchitectGovernanceDashboard:
def overview(
self,
requirement_changes: list[dict[str, Any]] | None = None,
architecture_reviews: list[dict[str, Any]] | None = None,
document_status: dict[str, Any] | None = None,
research_status: dict[str, Any] | None = None,
version_recommendations: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
requirement_changes = requirement_changes or []
architecture_reviews = architecture_reviews or []
document_status = document_status or {}
research_status = research_status or {}
version_recommendations = version_recommendations or []
panels = [
self.requirement_panel(requirement_changes),
self.architecture_panel(architecture_reviews),
self.document_panel(document_status),
self.research_panel(research_status),
self.version_panel(version_recommendations),
]
return {
"module": "architect-governance",
"route": "/admin/ai-evolution/architect",
"title": "架构师 Agent 治理中心",
"status": "attention_required" if any(self._panel_warn(panel) for panel in panels) else "healthy",
"generated_at": utc_now(),
"panels": [panel.to_dict() for panel in panels],
"requirement_changes": requirement_changes,
"architecture_reviews": architecture_reviews,
"version_recommendations": version_recommendations,
}
def requirement_panel(self, changes: list[dict[str, Any]]) -> ArchitectPanel:
return ArchitectPanel(
key="requirements",
title="需求变化记录",
description="记录来自 Custom GPT、Codex、OpenClaw、Hermes 和用户反馈的需求变化。",
metrics=[
ArchitectPanelMetric("total_requirement_changes", "需求变化数量", len(changes)),
ArchitectPanelMetric(
"undocumented_changes",
"待刷新到用户需求文档",
len([c for c in changes if not c.get("documented")]),
"warn" if any(not c.get("documented") for c in changes) else "ok",
),
],
)
def architecture_panel(self, reviews: list[dict[str, Any]]) -> ArchitectPanel:
pending = [r for r in reviews if r.get("status") not in {"pass", "not_required"}]
return ArchitectPanel(
key="architecture_review",
title="全量架构审视",
description="展示架构一致性、扩展性、安全边界、可运维性和回滚策略审查。",
metrics=[
ArchitectPanelMetric("review_count", "架构审查数量", len(reviews)),
ArchitectPanelMetric("pending_reviews", "待处理架构审查", len(pending), "warn" if pending else "ok"),
],
)
def document_panel(self, status: dict[str, Any]) -> ArchitectPanel:
required_docs = ["requirements", "architecture", "deployment", "test_cases"]
missing = [doc for doc in required_docs if status.get(doc) not in {"ready", "updated"}]
return ArchitectPanel(
key="documents",
title="文档版本化状态",
description="用户需求、架构设计、工程部署、测试例文档刷新状态。",
metrics=[
ArchitectPanelMetric("requirements_doc", "用户需求文档", status.get("requirements", "unknown"), "warn" if "requirements" in missing else "ok"),
ArchitectPanelMetric("architecture_doc", "架构设计文档", status.get("architecture", "unknown"), "warn" if "architecture" in missing else "ok"),
ArchitectPanelMetric("deployment_doc", "工程部署文档", status.get("deployment", "unknown"), "warn" if "deployment" in missing else "ok"),
ArchitectPanelMetric("test_case_doc", "测试例文档", status.get("test_cases", "unknown"), "warn" if "test_cases" in missing else "ok"),
],
)
def research_panel(self, status: dict[str, Any]) -> ArchitectPanel:
overdue = bool(status.get("overdue"))
return ArchitectPanel(
key="weekly_research",
title="每周外部技术情报",
description="联网搜索 Agent/模型版本、GitHub top 实现、潜在安全影响。",
metrics=[
ArchitectPanelMetric("last_research_at", "最近调研时间", status.get("last_research_at", "never"), "warn" if overdue else "ok"),
ArchitectPanelMetric("findings", "发现数量", status.get("findings", 0)),
ArchitectPanelMetric("optimization_candidates", "可借鉴优化项", status.get("optimization_candidates", 0)),
],
)
def version_panel(self, recommendations: list[dict[str, Any]]) -> ArchitectPanel:
latest = recommendations[-1] if recommendations else {}
return ArchitectPanel(
key="version_decision",
title="V/R/C 版本建议",
description="由架构师 Agent 判断变更应进入 C、R 或 V。",
metrics=[
ArchitectPanelMetric("recommendation_count", "版本建议数量", len(recommendations)),
ArchitectPanelMetric("latest_recommendation", "最近建议", latest.get("version_bump", "none")),
ArchitectPanelMetric("reason", "建议原因", latest.get("reason", "")),
],
)
def _panel_warn(self, panel: ArchitectPanel) -> bool:
return any(metric.status == "warn" for metric in panel.metrics)
__all__ = ["ArchitectPanelMetric", "ArchitectPanel", "ArchitectGovernanceDashboard"]
@@ -1,40 +0,0 @@
"""Web UI model for Architect weekly research."""
from .architect_weekly_research_agent import ArchitectWeeklyResearchAgent
class ArchitectResearchDashboard:
def __init__(self, agent=None):
self.agent = agent or ArchitectWeeklyResearchAgent()
def overview(self):
return {
"module": "architect-weekly-research",
"route": "/admin/ai-evolution/architect/research",
"title": "架构师 Agent 技术情报中心",
"panels": [
{
"title": "Research Targets",
"data": self.agent.plan(),
},
{
"title": "Evaluation",
"fields": [
"relevance",
"impact",
"recommendation",
],
},
{
"title": "Version Decision",
"fields": [
"C",
"R",
"V",
],
},
],
}
__all__ = ["ArchitectResearchDashboard"]
@@ -1,122 +0,0 @@
"""Architect research timeline dashboard.
Shows weekly external research findings as a traceable architecture-impact flow:
research target -> finding -> impact -> recommendation -> version decision ->
governance event.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTimelineStep:
id: str
title: str
step_type: str
status: str
source: str
timestamp: str
impact: str = "none"
version_bump: str = "none"
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectResearchTimeline:
ORDER = {
"research_planned": 10,
"finding_created": 20,
"impact_analyzed": 30,
"recommendation_created": 40,
"version_decision_created": 50,
"architecture_review_event_created": 60,
}
def build(self, findings: list[dict[str, Any]] | None = None, decisions: list[dict[str, Any]] | None = None) -> dict[str, Any]:
findings = findings or []
decisions = decisions or []
steps: list[ResearchTimelineStep] = [
ResearchTimelineStep(
id="research-plan",
title="每周技术情报调研计划",
step_type="research_planned",
status="ok",
source="architect-agent",
timestamp=utc_now(),
detail={"cadence": "weekly"},
)
]
for idx, finding in enumerate(findings):
steps.append(
ResearchTimelineStep(
id=finding.get("id", f"finding-{idx}"),
title=finding.get("title", "research finding"),
step_type="finding_created",
status="ok",
source=finding.get("source", "unknown"),
timestamp=finding.get("created_at", utc_now()),
impact=finding.get("impact", "none"),
detail=finding,
)
)
steps.append(
ResearchTimelineStep(
id=f"impact-{finding.get('id', idx)}",
title="架构影响分析",
step_type="impact_analyzed",
status="attention_required" if finding.get("impact") in {"architecture", "breaking", "new_capability"} else "ok",
source="architect-agent",
timestamp=utc_now(),
impact=finding.get("impact", "none"),
detail={"relevance": finding.get("relevance"), "recommendation": finding.get("recommendation")},
)
)
for idx, decision in enumerate(decisions):
steps.append(
ResearchTimelineStep(
id=decision.get("id", f"decision-{idx}"),
title="C/R/V 版本建议",
step_type="version_decision_created",
status=decision.get("status", "ok"),
source="architect-agent",
timestamp=decision.get("created_at", utc_now()),
version_bump=decision.get("version_bump", "none"),
detail=decision,
)
)
steps.sort(key=lambda s: (self.ORDER.get(s.step_type, 999), s.timestamp))
return {
"module": "architect-research-timeline",
"route": "/admin/ai-evolution/architect/research/timeline",
"title": "架构师技术情报时间线",
"status": self._status(steps),
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"summary": self._summary(steps),
}
def _status(self, steps: list[ResearchTimelineStep]) -> str:
if any(step.status == "attention_required" for step in steps):
return "attention_required"
return "healthy"
def _summary(self, steps: list[ResearchTimelineStep]) -> dict[str, Any]:
return {
"findings": len([s for s in steps if s.step_type == "finding_created"]),
"architecture_impacts": len([s for s in steps if s.impact in {"architecture", "breaking", "new_capability"}]),
"version_decisions": len([s for s in steps if s.step_type == "version_decision_created"]),
"recommended_bumps": sorted({s.version_bump for s in steps if s.version_bump != "none"}),
}
__all__ = ["ResearchTimelineStep", "ArchitectResearchTimeline"]
@@ -1,32 +0,0 @@
"""API for architect research timeline."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architect_research_timeline import ArchitectResearchTimeline
timeline = ArchitectResearchTimeline()
def get_architect_research_timeline():
return timeline.build()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architect/research", tags=["architect-research"])
@router.get("/timeline", operation_id="getArchitectResearchTimeline")
def get_timeline():
return get_architect_research_timeline()
return router
router = build_router()
__all__ = ["router", "get_architect_research_timeline"]
@@ -1,113 +0,0 @@
"""Weekly external research loop for Architect Governance Agent.
The agent plans and normalizes weekly research tasks. Actual internet access is
provided by the runtime/search worker; this module defines the audited workflow,
findings, impact analysis, and V/R/C recommendations.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ResearchTarget:
id: str
topic: str
source_type: str
query: str
cadence: str = "weekly"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchFinding:
id: str
target_id: str
title: str
summary: str
source: str
relevance: str
impact: str
recommendation: str
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ResearchDecision:
status: str
version_bump: str
reason: str
findings: list[ResearchFinding]
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"version_bump": self.version_bump,
"reason": self.reason,
"findings": [f.to_dict() for f in self.findings],
"created_at": self.created_at,
}
class ArchitectWeeklyResearchAgent:
DEFAULT_TARGETS = (
ResearchTarget("agent-frameworks", "Agent framework versions", "web", "latest multi-agent orchestration framework releases"),
ResearchTarget("model-ecosystem", "Model ecosystem changes", "web", "latest LLM model releases tool calling agent features"),
ResearchTarget("github-top-agentops", "GitHub top AgentOps implementations", "github", "top repositories agent observability multi agent orchestration"),
ResearchTarget("security-agent-risks", "Agent security changes", "web", "latest AI agent security sandbox prompt injection tool use risks"),
)
def plan(self) -> dict[str, Any]:
return {
"status": "planned",
"cadence": "weekly",
"targets": [target.to_dict() for target in self.DEFAULT_TARGETS],
"created_at": utc_now(),
}
def normalize_finding(
self,
target_id: str,
title: str,
summary: str,
source: str,
relevance: str = "medium",
impact: str = "none",
recommendation: str = "monitor",
) -> ResearchFinding:
return ResearchFinding(
id=f"finding_{uuid4().hex[:12]}",
target_id=target_id,
title=title,
summary=summary,
source=source,
relevance=relevance,
impact=impact,
recommendation=recommendation,
)
def decide(self, findings: list[ResearchFinding]) -> ResearchDecision:
if any(f.impact == "breaking" for f in findings):
return ResearchDecision("action_required", "V", "breaking external ecosystem change", findings)
if any(f.impact in {"architecture", "new_capability"} for f in findings):
return ResearchDecision("optimization_recommended", "R", "external capability may improve architecture", findings)
if any(f.impact in {"bugfix", "minor"} for f in findings):
return ResearchDecision("minor_change_recommended", "C", "minor compatible improvement", findings)
return ResearchDecision("no_action", "none", "no relevant external change", findings)
__all__ = ["ResearchTarget", "ResearchFinding", "ResearchDecision", "ArchitectWeeklyResearchAgent"]
@@ -1,32 +0,0 @@
"""API for architecture change governance dashboard."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .architecture_change_dashboard import ArchitectureChangeDashboard
dashboard = ArchitectureChangeDashboard()
def get_architecture_change_overview():
return dashboard.overview()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/architecture-changes", tags=["architecture-changes"])
@router.get("/overview", operation_id="getArchitectureChangeOverview")
def overview():
return get_architecture_change_overview()
return router
router = build_router()
__all__ = ["router", "get_architecture_change_overview"]
@@ -1,90 +0,0 @@
"""Architecture change classification.
Small compatible architecture changes may be handled automatically by the
Architect Agent with audit evidence. Large, risky, production-impacting or
breaking architecture changes require explicit Owner approval before
implementation.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
@dataclass(frozen=True)
class ArchitectureChangeClassification:
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeClassifier:
"""Classifies architecture changes into small/medium/large governance paths."""
LARGE_FLAGS = {
"breaking_change",
"breaks_architecture",
"production_architecture_update",
"security_boundary_change",
"cross_tenant_effect",
"cross_user_data_effect",
"data_migration",
"permission_model_change",
"runtime_infrastructure_change",
}
def classify(self, change: dict[str, Any]) -> ArchitectureChangeClassification:
risk = str(change.get("risk", "medium")).lower()
version_bump = str(change.get("version_bump", change.get("recommended_version_bump", "C"))).upper()
scope = str(change.get("scope", "component")).lower()
reasons: list[str] = []
for flag in sorted(self.LARGE_FLAGS):
if change.get(flag):
reasons.append(flag)
if risk in {"high", "critical"}:
reasons.append(f"risk={risk}")
if version_bump == "V":
reasons.append("version_bump=V")
if version_bump == "R" and scope in {"system", "platform", "production", "cross_agent", "cross_tenant"}:
reasons.append(f"version_bump=R scope={scope}")
if reasons:
return ArchitectureChangeClassification(
level="large",
owner_required=True,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=reasons,
required_gates=["architect_review", "owner_architecture_approval", "test_gate", "promotion_gate"],
)
if risk == "low" and version_bump in {"C", "NONE"} and scope in {"component", "local", "documentation", "test"}:
return ArchitectureChangeClassification(
level="small",
owner_required=False,
auto_implement_allowed=True,
version_bump=version_bump,
reasons=["low_risk_compatible_change"],
required_gates=["architect_review", "audit", "test_gate"],
)
return ArchitectureChangeClassification(
level="medium",
owner_required=False,
auto_implement_allowed=False,
version_bump=version_bump,
reasons=["requires_architect_review_before_implementation"],
required_gates=["architect_review", "test_gate", "audit"],
)
__all__ = ["ArchitectureChangeClassification", "ArchitectureChangeClassifier"]
@@ -1,93 +0,0 @@
"""Architecture change governance dashboard.
Displays architecture change classification, owner approval requirements,
implementation readiness, version recommendation and audit evidence for Web UI.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
from .architecture_change_classifier import ArchitectureChangeClassifier
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureChangeView:
change_id: str
title: str
level: str
owner_required: bool
auto_implement_allowed: bool
version_bump: str
status: str
reasons: list[str] = field(default_factory=list)
required_gates: list[str] = field(default_factory=list)
owner_decision: str | None = None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureChangeDashboard:
def __init__(self, classifier: ArchitectureChangeClassifier | None = None):
self.classifier = classifier or ArchitectureChangeClassifier()
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
views = [self._view(change) for change in changes]
return {
"module": "architecture-change-governance",
"route": "/admin/ai-evolution/architecture-changes",
"title": "架构变更治理中心",
"status": self._status(views),
"generated_at": utc_now(),
"summary": self._summary(views),
"changes": [view.to_dict() for view in views],
}
def _view(self, change: dict[str, Any]) -> ArchitectureChangeView:
classification = self.classifier.classify(change)
owner_decision = change.get("owner_decision")
if classification.owner_required and owner_decision != "approved":
status = "waiting_owner_approval"
elif classification.auto_implement_allowed:
status = "auto_implementation_allowed"
else:
status = "ready_after_required_gates"
return ArchitectureChangeView(
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
level=classification.level,
owner_required=classification.owner_required,
auto_implement_allowed=classification.auto_implement_allowed,
version_bump=classification.version_bump,
status=status,
reasons=classification.reasons,
required_gates=classification.required_gates,
owner_decision=owner_decision,
)
def _status(self, views: list[ArchitectureChangeView]) -> str:
if any(view.status == "waiting_owner_approval" for view in views):
return "attention_required"
return "healthy"
def _summary(self, views: list[ArchitectureChangeView]) -> dict[str, Any]:
return {
"total": len(views),
"small": len([v for v in views if v.level == "small"]),
"medium": len([v for v in views if v.level == "medium"]),
"large": len([v for v in views if v.level == "large"]),
"waiting_owner_approval": len([v for v in views if v.status == "waiting_owner_approval"]),
"auto_allowed": len([v for v in views if v.auto_implement_allowed]),
"version_bumps": sorted({v.version_bump for v in views if v.version_bump and v.version_bump != "NONE"}),
}
__all__ = ["ArchitectureChangeView", "ArchitectureChangeDashboard"]
@@ -1,89 +0,0 @@
"""Owner approval gate for architecture changes.
Architecture changes may be proposed, reviewed and optimized by agents, but they
must not be implemented until owner approval is recorded. This gate sits before
implementation and before promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureOwnerApprovalRequest:
request_id: str
change_id: str
title: str
reason: str
risk: str
architect_decision: str
status: str = "pending_owner_approval"
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class ArchitectureOwnerDecision:
request_id: str
change_id: str
decision: str
decided_by: str
reason: str = ""
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class ArchitectureOwnerGate:
"""Blocks architecture implementation until owner explicitly approves."""
def create_request(self, change: dict[str, Any], architect_decision: dict[str, Any]) -> ArchitectureOwnerApprovalRequest:
return ArchitectureOwnerApprovalRequest(
request_id=f"ARCH-OWNER-{change.get('change_id', change.get('id', 'unknown'))}",
change_id=change.get("change_id", change.get("id", "unknown")),
title=change.get("title", "architecture change"),
reason=change.get("reason", "architecture impact requires owner approval"),
risk=change.get("risk", "medium"),
architect_decision=architect_decision.get("status", "reviewed"),
)
def can_implement(self, request: dict[str, Any], decision: dict[str, Any] | None = None) -> dict[str, Any]:
if not decision:
return {
"allowed": False,
"status": "waiting_owner_approval",
"reason": "architecture change requires owner approval before implementation",
"request": request,
}
if decision.get("decision") == "approved":
return {
"allowed": True,
"status": "owner_approved",
"reason": decision.get("reason", "approved by owner"),
"request": request,
"decision": decision,
}
return {
"allowed": False,
"status": "owner_rejected",
"reason": decision.get("reason", "rejected by owner"),
"request": request,
"decision": decision,
}
__all__ = [
"ArchitectureOwnerApprovalRequest",
"ArchitectureOwnerDecision",
"ArchitectureOwnerGate",
]
@@ -1,79 +0,0 @@
"""HTTP handler and display data for architecture owner gate."""
from __future__ import annotations
from dataclasses import asdict
from datetime import datetime, timezone
from typing import Any
from .architecture_owner_gate import ArchitectureOwnerGate, ArchitectureOwnerApprovalRequest
gate = ArchitectureOwnerGate()
# In-memory store for pending approval requests
_pending_requests: dict[str, dict[str, Any]] = {}
_decisions: dict[str, dict[str, Any]] = {}
def create_owner_approval_request(change: dict[str, Any], architect_decision: dict[str, Any]) -> dict[str, Any]:
req = gate.create_request(change, architect_decision)
d = asdict(req)
_pending_requests[req.change_id] = d
return d
def approve_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
decision = asdict(
ArchitectureOwnerApprovalRequest.__bases__[1].__new__(type("_ArchitectureOwnerDecision", (), {})) # noqa
)
try:
from .architecture_owner_gate import ArchitectureOwnerDecision
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="approved",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
# remove from pending
_pending_requests.pop(change_id, None)
return {"allowed": True, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def reject_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
from .architecture_owner_gate import ArchitectureOwnerDecision
req = _pending_requests.get(change_id)
if not req:
return {"error": f"no pending request for change_id={change_id}"}
try:
dec = ArchitectureOwnerDecision(
request_id=req["request_id"],
change_id=change_id,
decision="rejected",
decided_by=decided_by,
reason=reason,
)
d = asdict(dec)
_decisions[change_id] = d
_pending_requests.pop(change_id, None)
return {"allowed": False, "decision": d}
except Exception as exc:
return {"error": str(exc)}
def get_owner_gate_overview() -> dict[str, Any]:
return {
"pending_requests": list(_pending_requests.values()),
"decisions": list(_decisions.values()),
"pending_count": len(_pending_requests),
"approved_count": sum(1 for d in _decisions.values() if d.get("decision") == "approved"),
"rejected_count": sum(1 for d in _decisions.values() if d.get("decision") == "rejected"),
}
@@ -1,40 +0,0 @@
"""Independent architecture optimization agent."""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ArchitectureReview:
agent: str
decision: str
findings: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class ArchitectureReviewAgent:
"""Reviews architecture changes from system-wide optimal perspective."""
def review(self, proposal: dict) -> ArchitectureReview:
findings = [
"dependency_impact_checked",
"security_boundary_checked",
"scalability_checked",
"operability_checked",
"rollback_strategy_checked",
]
return ArchitectureReview(
"architecture-optimization-agent",
"pass" if proposal.get("architecture_change") else "not_required",
findings,
)
__all__ = ["ArchitectureReviewAgent", "ArchitectureReview"]
@@ -0,0 +1,38 @@
"""Autonomous Controller health adapter."""
from __future__ import annotations
from typing import Any
from .blocker_task_adapter import BlockerTaskAdapter
from .git_permission_watchdog import GitPermissionWatchdog
from .system_blocker_memory import SystemBlockerMemory
from .system_health_center import SystemHealthCenter
class AutonomousHealthAdapter:
def __init__(self):
self.git = GitPermissionWatchdog()
self.memory = SystemBlockerMemory()
self.task_adapter = BlockerTaskAdapter()
self.health = SystemHealthCenter()
def check(self, previous_blockers: list[dict[str, Any]] | None = None) -> dict[str, Any]:
git_report = self.git.scan().to_dict()
blockers = list(previous_blockers or [])
if git_report.get("status") != "healthy":
blockers.append({
"blocker_id": "git-permission",
"category": "git_permission",
"title": "Git object permission drift",
"severity": "p0",
"evidence": [str(git_report)],
})
return self.health.build(git_report=git_report, blockers=blockers)
def optimize_task(self, blocker: dict[str, Any]) -> dict[str, Any]:
decision = self.memory.decide(blocker)
return self.task_adapter.to_task(blocker, decision.to_dict())
__all__ = ["AutonomousHealthAdapter"]
@@ -0,0 +1,64 @@
"""Convert recurring blockers into Task Center tasks.
When a blocker becomes recurrent or critical, it should not remain only as an
incident log. It becomes a visible task with owner/executor/progress/approval
state in the Task Center.
"""
from __future__ import annotations
from typing import Any
from datetime import datetime, timezone
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class BlockerTaskAdapter:
def to_task(self, blocker: dict[str, Any], decision: dict[str, Any]) -> dict[str, Any]:
priority = decision.get("priority", "P2")
status = "waiting_approval" if decision.get("owner_gate_required") else "pending"
if priority == "P0":
status = "running"
return {
"task_id": f"blocker-{blocker.get('fingerprint', blocker.get('blocker_id', 'unknown'))}",
"title": f"Resolve blocker: {blocker.get('title', 'unknown')}",
"assigned_by": "system-blocker-memory",
"executor": decision.get("assigned_to", "autonomous-controller"),
"source": "system_blocker",
"status": status,
"priority": priority,
"created_at": utc_now(),
"updated_at": utc_now(),
"progress": {
"percent": 20 if status == "running" else 0,
"current_step": decision.get("action", "record_and_monitor"),
"completed_steps": ["blocker_recorded", "decision_created"],
"remaining_steps": self._remaining_steps(decision),
"blocked_reason": blocker.get("title") if status == "waiting_approval" else None,
},
"notification": {
"required": True,
"sent": False,
"channel": "wechat",
},
"owner_gate_required": bool(decision.get("owner_gate_required")),
"owner_gate_status": "pending" if decision.get("owner_gate_required") else None,
"evidence": blocker.get("evidence", []),
"metadata": {
"blocker": blocker,
"decision": decision,
},
}
def _remaining_steps(self, decision: dict[str, Any]) -> list[str]:
steps = ["root_cause_analysis", "patch_or_repair", "test", "deploy", "verify", "close"]
if decision.get("architect_review_required"):
steps.insert(1, "architect_review")
if decision.get("owner_gate_required"):
steps.insert(2, "owner_approval")
return steps
__all__ = ["BlockerTaskAdapter"]
@@ -1,103 +0,0 @@
"""Change acceptance governance for AIOS.
Every feature change must provide matching tests. Every architecture change must
be reviewed by an independent architecture agent. User-facing changes must be
validated by an independent user acceptance agent.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class ChangeDescriptor:
change_id: str
change_type: str
title: str
files_changed: list[str]
tests: list[str] = field(default_factory=list)
user_facing: bool = False
architecture_impact: bool = False
risk: str = "low"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceFinding:
check: str
status: str
detail: str
required: bool = True
def passed(self) -> bool:
return self.status == "pass" or not self.required
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class AcceptanceDecision:
status: str
findings: list[AcceptanceFinding]
decided_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"status": self.status,
"findings": [f.to_dict() for f in self.findings],
"decided_at": self.decided_at,
}
class ChangeAcceptanceGate:
def evaluate(self, change: ChangeDescriptor) -> AcceptanceDecision:
findings: list[AcceptanceFinding] = []
findings.append(self._tests_required(change))
findings.append(self._user_acceptance_required(change))
findings.append(self._architecture_review_required(change))
findings.append(self._risk_gate(change))
status = "pass" if all(f.passed() for f in findings) else "blocked"
return AcceptanceDecision(status, findings)
def _tests_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.tests:
return AcceptanceFinding("tests_present", "pass", "matching tests declared")
return AcceptanceFinding("tests_present", "fail", "every feature change requires tests")
def _user_acceptance_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.user_facing:
return AcceptanceFinding("user_acceptance", "pass", "not user-facing")
has_user_test = any("user" in t.lower() or "acceptance" in t.lower() for t in change.tests)
return AcceptanceFinding(
"user_acceptance",
"pass" if has_user_test else "fail",
"user-facing change requires user acceptance test",
)
def _architecture_review_required(self, change: ChangeDescriptor) -> AcceptanceFinding:
if not change.architecture_impact:
return AcceptanceFinding("architecture_review", "pass", "no architecture impact")
has_arch_test = any("arch" in t.lower() or "architecture" in t.lower() for t in change.tests)
return AcceptanceFinding(
"architecture_review",
"pass" if has_arch_test else "fail",
"architecture change requires independent architect review artifact/test",
)
def _risk_gate(self, change: ChangeDescriptor) -> AcceptanceFinding:
if change.risk in {"low", "medium", "high"}:
return AcceptanceFinding("risk_classified", "pass", f"risk={change.risk}")
return AcceptanceFinding("risk_classified", "fail", "risk must be low/medium/high")
__all__ = ["ChangeDescriptor", "AcceptanceFinding", "AcceptanceDecision", "ChangeAcceptanceGate"]
@@ -1,34 +0,0 @@
"""API for AIOS change governance dashboard."""
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .change_governance_dashboard import ChangeGovernanceDashboard
dashboard = ChangeGovernanceDashboard()
def get_change_governance_overview(changes: list[dict[str, Any]] | None = None):
return dashboard.overview(changes)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["change-governance"])
@router.get("/overview", operation_id="getChangeGovernanceOverview")
def overview():
return get_change_governance_overview()
return router
router = build_router()
__all__ = ["router", "get_change_governance_overview"]
@@ -1,93 +0,0 @@
"""Web UI dashboard model for AIOS change governance.
Shows test gates, user acceptance, architecture review, risk gates and release
blocking status for human operators and owner review.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GovernanceMetric:
key: str
title: str
value: Any
status: str = "ok"
detail: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GovernancePanel:
key: str
title: str
metrics: list[GovernanceMetric] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"key": self.key,
"title": self.title,
"metrics": [m.to_dict() for m in self.metrics],
}
class ChangeGovernanceDashboard:
def overview(self, changes: list[dict[str, Any]] | None = None) -> dict[str, Any]:
changes = changes or []
blocked = [c for c in changes if c.get("status") == "blocked"]
waiting_arch = [c for c in changes if c.get("architecture_review") == "required"]
waiting_user = [c for c in changes if c.get("user_acceptance") == "required"]
missing_tests = [c for c in changes if not c.get("tests")]
panels = [
GovernancePanel(
"test_gate",
"测试 Gate",
[
GovernanceMetric("total_changes", "变更总数", len(changes)),
GovernanceMetric("missing_tests", "缺少测试的变更", len(missing_tests), "warn" if missing_tests else "ok"),
],
),
GovernancePanel(
"user_acceptance",
"用户验收 Agent",
[
GovernanceMetric("waiting_user_acceptance", "等待用户验收", len(waiting_user), "warn" if waiting_user else "ok"),
],
),
GovernancePanel(
"architecture_review",
"架构师 Agent",
[
GovernanceMetric("waiting_architecture_review", "等待架构审查", len(waiting_arch), "warn" if waiting_arch else "ok"),
],
),
GovernancePanel(
"release_gate",
"发布 Gate",
[
GovernanceMetric("blocked_changes", "阻塞发布的变更", len(blocked), "warn" if blocked else "ok"),
],
),
]
return {
"module": "change-governance",
"route": "/admin/ai-evolution/governance",
"title": "AIOS 变更治理中心",
"status": "attention_required" if blocked or missing_tests or waiting_arch or waiting_user else "healthy",
"generated_at": utc_now(),
"panels": [p.to_dict() for p in panels],
"changes": changes,
}
__all__ = ["GovernanceMetric", "GovernancePanel", "ChangeGovernanceDashboard"]
@@ -0,0 +1,90 @@
"""Command watcher stall detector.
Detects commands that remain in active/running states beyond the configured
threshold and emits blocker records so AIOS can self-optimize instead of waiting
for manual progress checks.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def parse_ts(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except Exception:
return None
@dataclass(frozen=True)
class CommandStallRecord:
command_id: str
path: str
bucket: str
age_minutes: float
threshold_minutes: int
severity: str
detected_at: str = field(default_factory=utc_now)
def to_blocker(self) -> dict[str, Any]:
return {
"blocker_id": f"command-stall-{self.command_id}",
"category": "command_stuck",
"title": f"Command stalled: {self.command_id}",
"source": "command-stall-detector",
"severity": self.severity,
"status": "open",
"first_seen_at": self.detected_at,
"last_seen_at": self.detected_at,
"occurrence_count": 1,
"evidence": [self.path],
"metadata": self.to_dict(),
}
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class CommandStallDetector:
def __init__(self, root: str = "agents/commands", threshold_minutes: int = 15):
self.root = Path(root)
self.threshold_minutes = threshold_minutes
def detect(self) -> list[CommandStallRecord]:
records: list[CommandStallRecord] = []
for bucket in ("active", "running"):
bucket_path = self.root / bucket
if not bucket_path.exists():
continue
for path in bucket_path.glob("*.md"):
age = self._age_minutes(path)
if age >= self.threshold_minutes:
records.append(
CommandStallRecord(
command_id=path.stem,
path=str(path),
bucket=bucket,
age_minutes=round(age, 2),
threshold_minutes=self.threshold_minutes,
severity="p0" if bucket == "running" else "high",
)
)
return records
def _age_minutes(self, path: Path) -> float:
stat = path.stat()
age_seconds = datetime.now(timezone.utc).timestamp() - stat.st_mtime
return age_seconds / 60
__all__ = ["CommandStallRecord", "CommandStallDetector"]
@@ -0,0 +1,141 @@
"""Git object permission watchdog and self-healing.
Prevents AIOS autonomous operation from being blocked by recurring
`.git/objects` ownership drift. The repair is intentionally conservative and
limited to the workspace Git object database.
"""
from __future__ import annotations
import os
import pwd
import grp
import subprocess
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class GitPermissionIssue:
path: str
issue: str
actual_uid: int | None = None
actual_gid: int | None = None
expected_user: str = "aiman"
expected_group: str = "gitops"
def to_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GitPermissionReport:
workspace: str
expected_user: str
expected_group: str
status: str
issues: list[GitPermissionIssue] = field(default_factory=list)
checked_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return {
"workspace": self.workspace,
"expected_user": self.expected_user,
"expected_group": self.expected_group,
"status": self.status,
"issues": [issue.to_dict() for issue in self.issues],
"checked_at": self.checked_at,
}
class GitPermissionWatchdog:
def __init__(
self,
workspace: str = "/data/aios-gitops/workspace",
expected_user: str = "aiman",
expected_group: str = "gitops",
max_scan_depth: int = 2,
):
self.workspace = Path(workspace)
self.expected_user = expected_user
self.expected_group = expected_group
self.max_scan_depth = max_scan_depth
@property
def objects_dir(self) -> Path:
return self.workspace / ".git" / "objects"
def scan(self) -> GitPermissionReport:
issues: list[GitPermissionIssue] = []
if not self.objects_dir.exists():
issues.append(GitPermissionIssue(str(self.objects_dir), "missing_objects_dir"))
return GitPermissionReport(str(self.workspace), self.expected_user, self.expected_group, "blocked", issues)
expected_uid = pwd.getpwnam(self.expected_user).pw_uid
expected_gid = grp.getgrnam(self.expected_group).gr_gid
for path in self._walk_limited(self.objects_dir):
try:
stat = path.stat()
except FileNotFoundError:
continue
if stat.st_uid != expected_uid:
issues.append(GitPermissionIssue(str(path), "owner_drift", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
if stat.st_gid != expected_gid:
issues.append(GitPermissionIssue(str(path), "group_drift", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
if not os.access(path, os.W_OK):
issues.append(GitPermissionIssue(str(path), "not_writable", stat.st_uid, stat.st_gid, self.expected_user, self.expected_group))
if len(issues) >= 200:
break
return GitPermissionReport(
workspace=str(self.workspace),
expected_user=self.expected_user,
expected_group=self.expected_group,
status="healthy" if not issues else "drift_detected",
issues=issues,
)
def repair(self, dry_run: bool = False) -> dict[str, Any]:
report = self.scan()
if report.status == "healthy":
return {"status": "healthy", "report": report.to_dict(), "repaired": False}
if dry_run:
return {"status": "dry_run", "report": report.to_dict(), "repaired": False}
commands = [
["chown", "-R", f"{self.expected_user}:{self.expected_group}", str(self.objects_dir)],
["chmod", "-R", "u+rwX,g+rwX", str(self.objects_dir)],
]
results = []
for command in commands:
proc = subprocess.run(command, text=True, capture_output=True, check=False)
results.append({"cmd": command, "returncode": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr})
if proc.returncode != 0:
return {"status": "repair_failed", "report": report.to_dict(), "results": results, "repaired": False}
post = self.scan()
return {"status": "repaired" if post.status == "healthy" else "repair_incomplete", "report": post.to_dict(), "results": results, "repaired": post.status == "healthy"}
def write_probe(self) -> dict[str, Any]:
probe = self.objects_dir / f".aios-write-probe-{datetime.now(timezone.utc).timestamp()}"
try:
probe.write_text("probe\n", encoding="utf-8")
probe.unlink(missing_ok=True)
return {"status": "ok", "path": str(probe)}
except Exception as exc: # pragma: no cover - depends on runtime perms
return {"status": "failed", "path": str(probe), "error": str(exc)}
def _walk_limited(self, root: Path):
yield root
base_depth = len(root.parts)
for path in root.rglob("*"):
if len(path.parts) - base_depth <= self.max_scan_depth:
yield path
__all__ = ["GitPermissionIssue", "GitPermissionReport", "GitPermissionWatchdog"]
@@ -0,0 +1,31 @@
"""CLI entrypoint for Git Permission Watchdog."""
from __future__ import annotations
import json
import os
from pathlib import Path
from .git_permission_watchdog import GitPermissionWatchdog
def main() -> int:
workspace = os.getenv("AIOS_GITOPS_WORKSPACE", "/data/aios-gitops/workspace")
expected_user = os.getenv("AIOS_GIT_EXPECTED_USER", "aiman")
expected_group = os.getenv("AIOS_GIT_EXPECTED_GROUP", "gitops")
audit_dir = Path(os.getenv("AIOS_GIT_PERMISSION_AUDIT_DIR", "runtime/git-permission-watchdog"))
audit_dir.mkdir(parents=True, exist_ok=True)
watchdog = GitPermissionWatchdog(workspace, expected_user, expected_group)
result = watchdog.repair(dry_run=os.getenv("AIOS_GIT_PERMISSION_DRY_RUN") == "1")
probe = watchdog.write_probe()
payload = {"repair": result, "write_probe": probe}
status = result.get("status")
out = audit_dir / "latest.json"
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False))
return 0 if status in {"healthy", "repaired"} and probe.get("status") == "ok" else 2
if __name__ == "__main__":
raise SystemExit(main())
-36
View File
@@ -1,36 +0,0 @@
"""Git repository location resolver with validation.
Prevents hard-coded repository names from silently pointing to stale paths.
"""
from __future__ import annotations
import os
from pathlib import Path
class GitRepoLocator:
def __init__(self, root: str | None = None):
self.root = Path(root or os.getenv("AIOS_GITOPS_ROOT", "/data/aios-gitops/runtime/gitea-bin/data/repositories"))
def candidates(self, owner: str = "aios", names: tuple[str, ...] = ("aios.git", "aios-v23.git")):
return [self.root / owner / name for name in names]
def locate(self, owner: str = "aios") -> dict:
found = [str(path) for path in self.candidates(owner) if path.exists()]
return {
"root": str(self.root),
"owner": owner,
"found": found,
"healthy": len(found) == 1,
"warning": "multiple repositories found" if len(found) > 1 else None,
}
def require(self, owner: str = "aios") -> Path:
result = self.locate(owner)
if not result["healthy"]:
raise RuntimeError(f"Git repo resolution failed: {result}")
return Path(result["found"][0])
__all__ = ["GitRepoLocator"]
@@ -1,148 +0,0 @@
"""Bridge governance agents into the unified Agent Event Bus.
This module emits normalized governance events for requirement changes,
architecture reviews, user acceptance, test gates and version decisions. It uses
duck typing so it can work with the unified AgentEventBus when available, while
remaining testable without a running bus.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Protocol
from uuid import uuid4
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class EventPublisher(Protocol):
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
...
@dataclass(frozen=True)
class GovernanceEventEnvelope:
id: str
kind: str
source: str
target: str
summary: str
payload: dict[str, Any]
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class InMemoryGovernanceEventBus:
"""Small fallback bus for tests and offline governance runs."""
def __init__(self) -> None:
self.events: list[dict[str, Any]] = []
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event = GovernanceEventEnvelope(
id=f"evt_{uuid4().hex[:12]}",
kind=kind,
source=source,
target=target,
summary=summary,
payload=payload,
correlation_id=correlation_id,
).to_dict()
self.events.append(event)
return event
class GovernanceEventBridge:
"""Publishes governance lifecycle events to the Agent Event Bus."""
def __init__(self, bus: EventPublisher | None = None):
self.bus = bus or InMemoryGovernanceEventBus()
def requirement_changed(self, requirement: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="requirement_changed",
source=requirement.get("source", "unknown"),
target="architect-agent",
summary=requirement.get("requirement", "requirement changed"),
payload=requirement,
correlation_id=requirement.get("id"),
)
def architecture_review_started(self, proposal: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_started",
source="architect-agent",
target="architecture-review-agent",
summary=proposal.get("title", "architecture review started"),
payload=proposal,
correlation_id=proposal.get("change_id"),
)
def architecture_review_completed(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="architecture_review_completed",
source="architecture-review-agent",
target="change-governance",
summary=f"architecture review {decision.get('status', 'completed')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
def user_acceptance_completed(self, report: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="user_acceptance_completed",
source="user-acceptance-agent",
target="change-governance",
summary=f"user acceptance {report.get('decision', 'completed')}",
payload=report,
correlation_id=report.get("change_id"),
)
def test_gate_completed(self, result: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="test_gate_completed",
source="test-gate",
target="change-governance",
summary=f"test gate {result.get('status', 'completed')}",
payload=result,
correlation_id=result.get("change_id"),
)
def version_decision_created(self, decision: dict[str, Any]) -> dict[str, Any]:
return self.bus.publish(
kind="version_decision_created",
source="architect-agent",
target="owner-gate",
summary=f"version bump recommendation: {decision.get('version_bump', 'unknown')}",
payload=decision,
correlation_id=decision.get("change_id"),
)
__all__ = [
"GovernanceEventEnvelope",
"InMemoryGovernanceEventBus",
"GovernanceEventBridge",
]
@@ -1,32 +0,0 @@
"""API for governance timeline visualization."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .governance_timeline_dashboard import GovernanceTimelineDashboard
dashboard = GovernanceTimelineDashboard()
def get_governance_timeline(events=None):
return dashboard.build(events)
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/governance", tags=["governance-timeline"])
@router.get("/timeline", operation_id="getGovernanceTimeline")
def timeline():
return get_governance_timeline()
return router
router = build_router()
__all__ = ["router", "get_governance_timeline"]
@@ -1,106 +0,0 @@
"""Governance timeline dashboard.
Turns governance events into a human-readable flow for Web UI so operators can
see why a change is blocked or ready for promotion.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
EVENT_ORDER = {
"requirement_changed": 10,
"architecture_review_started": 20,
"architecture_review_completed": 30,
"test_gate_completed": 40,
"user_acceptance_completed": 50,
"version_decision_created": 60,
"owner_gate_completed": 70,
"promotion_completed": 80,
}
@dataclass(frozen=True)
class GovernanceTimelineStep:
id: str
kind: str
title: str
actor: str
status: str
timestamp: str
correlation_id: str | None = None
detail: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class GovernanceTimelineDashboard:
def build(self, events: list[dict[str, Any]] | None = None) -> dict[str, Any]:
events = events or []
steps = [self._event_to_step(event) for event in events]
steps.sort(key=lambda step: (EVENT_ORDER.get(step.kind, 999), step.timestamp))
blockers = [step for step in steps if step.status in {"blocked", "fail", "attention_required"}]
return {
"module": "governance-timeline",
"route": "/admin/ai-evolution/governance/timeline",
"title": "治理事件时间线",
"status": "attention_required" if blockers else "healthy",
"generated_at": utc_now(),
"steps": [step.to_dict() for step in steps],
"blockers": [step.to_dict() for step in blockers],
"flow": self._flow_summary(steps),
}
def _event_to_step(self, event: dict[str, Any]) -> GovernanceTimelineStep:
payload = event.get("payload", {})
kind = event.get("kind", "unknown")
return GovernanceTimelineStep(
id=event.get("id", f"event-{len(str(event))}"),
kind=kind,
title=self._title(kind, payload),
actor=event.get("source", payload.get("actor", "unknown")),
status=payload.get("status") or payload.get("decision") or event.get("status", "ok"),
timestamp=event.get("created_at") or payload.get("created_at") or utc_now(),
correlation_id=event.get("correlation_id") or payload.get("change_id") or payload.get("id"),
detail=payload,
)
def _title(self, kind: str, payload: dict[str, Any]) -> str:
titles = {
"requirement_changed": "需求变化",
"architecture_review_started": "架构审查开始",
"architecture_review_completed": "架构审查完成",
"test_gate_completed": "测试 Gate 完成",
"user_acceptance_completed": "用户验收完成",
"version_decision_created": "版本建议产生",
"owner_gate_completed": "Owner Gate 完成",
"promotion_completed": "Promotion 完成",
}
return payload.get("title") or payload.get("summary") or titles.get(kind, kind)
def _flow_summary(self, steps: list[GovernanceTimelineStep]) -> dict[str, Any]:
present = {step.kind for step in steps}
required = [
"requirement_changed",
"architecture_review_completed",
"test_gate_completed",
"user_acceptance_completed",
"version_decision_created",
]
missing = [kind for kind in required if kind not in present]
return {
"required_steps": required,
"missing_steps": missing,
"ready_for_owner_gate": not missing,
}
__all__ = ["GovernanceTimelineStep", "GovernanceTimelineDashboard"]
@@ -1,59 +0,0 @@
"""Independent acceptance agents.
Simulates independent roles: user representative and architecture reviewer.
They provide acceptance evidence, not implementation decisions.
"""
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from typing import Any
def utc_now():
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class AcceptanceReport:
agent: str
role: str
decision: str
evidence: list[str]
created_at: str = utc_now()
def to_dict(self):
return asdict(self)
class UserAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"user_requirement_match",
"user_workflow_validation",
"permission_visibility_check",
]
return AcceptanceReport(
"user-acceptance-agent",
"user_representative",
"pass" if change.get("user_facing") else "not_required",
checks,
)
class ArchitectureAcceptanceAgent:
def review(self, change: dict[str, Any]) -> AcceptanceReport:
checks = [
"architecture_consistency",
"scalability_review",
"security_boundary_review",
"operability_review",
]
return AcceptanceReport(
"architecture-review-agent",
"system_architect",
"pass" if change.get("architecture_impact") else "not_required",
checks,
)
__all__ = ["AcceptanceReport", "UserAcceptanceAgent", "ArchitectureAcceptanceAgent"]
@@ -0,0 +1,52 @@
"""Unified AIOS operations command center view model."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class OperationsCommandCenter:
def build(
self,
tasks: list[dict[str, Any]] | None = None,
health: dict[str, Any] | None = None,
blockers: list[dict[str, Any]] | None = None,
searches: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
tasks = tasks or []
blockers = blockers or []
searches = searches or []
return {
"module": "operations-command-center",
"route": "/admin/ai-evolution/operations",
"title": "AIOS 自治运营驾驶舱",
"generated_at": utc_now(),
"health": health or {},
"tasks": {
"active": len([t for t in tasks if t.get("status") in {"running", "pending", "waiting_approval"}]),
"blocked": len([t for t in tasks if t.get("status") == "blocked"]),
"completed": len([t for t in tasks if t.get("status") == "completed"]),
},
"blockers": {
"total": len(blockers),
"p0": len([b for b in blockers if b.get("severity") in {"p0", "critical"}]),
"recurring": len([b for b in blockers if b.get("mechanism_gap")]),
},
"search": {
"recent": len(searches),
},
"loops": {
"task_loop": True,
"health_loop": True,
"optimization_loop": True,
"notification_loop": True,
},
}
__all__ = ["OperationsCommandCenter"]
@@ -58,8 +58,6 @@ pre { white-space: pre-wrap; word-break: break-word; }
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
<li onclick="showPanel('arch-owner')"><i>🔐</i> Owner Gate</li>
<li onclick="showPanel('arch-approve')"><i>✓</i> Approve Changes</li>
</ul>
</div>
<div class="main">
@@ -92,14 +90,6 @@ pre { white-space: pre-wrap; word-break: break-word; }
</div>
<div id="panel-agents" class="panel" style="display:none;">
<div class="main-header"><h2>Recent Agent Executions</h2></div>
<div id="panel-arch-owner" class="panel" style="display:none;">
<div class="main-header"><h2>🔐 Architecture Owner Gate</h2></div>
<div id="arch-owner-content">Loading...</div>
</div>
<div id="panel-arch-approve" class="panel" style="display:none;">
<div class="main-header"><h2>✓ Approve Architecture Changes</h2></div>
<div id="arch-approve-content">Loading...</div>
</div>
<div id="agents-list">Loading...</div>
</div>
</div>
@@ -107,159 +97,3 @@ pre { white-space: pre-wrap; word-break: break-word; }
<script>
async function fetchData() {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'none';
document.getElementById('content').style.display = '';
const hs = document.getElementById('header-status');
hs.className = 'status-badge ' + (data.healthy ? 'ok' : 'fail');
hs.textContent = data.healthy ? '▪ operational' : '⚠ degraded';
document.getElementById('base').textContent = data.baseline || '-';
document.getElementById('branch').textContent = data.main_branch || 'main';
document.getElementById('uptime').textContent = data.uptime || '-';
document.getElementById('bus-status').textContent = data.bus ? JSON.stringify(data.bus, null, 2) : 'no event bus data';
}
async function loadVersions() {
const el = document.getElementById('versions-list');
el.innerHTML = '<div class="loading">Loading versions...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/versions');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '<div class="grid">';
for (const v of data.versions || []) {
html += '<div class="card"><div class="card-title">' + v.tag + '</div><div class="card-value">' + (v.version || v.tag) + '</div></div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load versions: ' + e.message + '</div>';
}
}
async function loadCommands() {
const el = document.getElementById('commands-list');
el.innerHTML = '<div class="loading">Loading commands...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
let cmds = data.commands || [];
let html = '<div style="display:flex;gap:24px;margin-bottom:16px;">';
html += '<div><div class="card"><div class="card-title">Total</div><div class="card-value">' + cmds.length + '</div></div></div>';
html += '<div><div class="card"><div class="card-title">By Category</div>';
const cats = {};
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
for (const [k, v] of Object.entries(cats)) html += '<div style="font-size:14px;color:#8b949e;margin:4px 0;">' + k + ': ' + v + '</div>';
html += '</div></div></div>';
for (const cmd of cmds.slice().reverse()) {
html += '<div class="command-item"><div class="meta">' + cmd.category + ' | ' + cmd.filename + '</div><div class="name">' + cmd.title + '</div><div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + cmd.summary + '</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load commands: ' + e.message + '</div>';
}
}
async function loadAgents() {
const el = document.getElementById('agents-list');
el.innerHTML = '<div class="loading">Loading agent runs...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/agents');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '';
for (const agent of data.agents || []) {
html += '<div class="agent-item"><div class="agent-title">' + agent.name + ' <span class="state state-' + (agent.status === 'running' ? 'running' : (agent.status === 'ok' ? 'completed' : 'failed')) + '">' + agent.status + '</span></div><div class="agent-meta">' + (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-') + '</div></div>';
}
el.innerHTML = html || '<div style="padding:24px;color:#8b949e;">No agent runs recorded yet.</div>';
} catch(e) {
el.innerHTML = '<div class="error">Failed to load agents: ' + e.message + '</div>';
}
}
function showPanel(name) {
async function loadOwnerGate() {
const el = document.getElementById('arch-owner-content');
el.innerHTML = '<div class="loading">Loading owner gate data...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '<div class="row">';
html += '<div class="col"><div class="card"><div class="card-title">Pending Requests</div><div class="card-value">' + data.pending_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Approved</div><div class="card-value">' + data.approved_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Rejected</div><div class="card-value">' + data.rejected_count + '</div></div></div>';
html += '</div>';
html += '<div class="card"><div class="card-title">Pending Approval Requests</div>';
if (data.pending_requests && data.pending_requests.length) {
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">ID: ' + req.change_id + ' | Risk: ' + req.risk + '</div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + (req.reason || '') + '</div>';
html += '</div>';
}
} else {
html += '<div style="color:#8b949e;padding:12px;">No pending requests.</div>';
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
async function loadApprove() {
const el = document.getElementById('arch-approve-content');
el.innerHTML = '<div class="loading">Loading pending approvals...</div>';
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '';
if (data.pending_requests && data.pending_requests.length) {
html += '<div class="card"><div class="card-title">Pending changes requiring your approval</div>';
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">Risk: <strong>' + req.risk + '</strong></div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin:8px 0;">' + (req.reason || '') + '</div>';
html += '<div style="display:flex;gap:8px;">';
html += '<button onclick="ownerAction.classList.add('\'approved\');this.disabled=true" style="padding:6px 16px;background:#238636;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">✓ Approve</button>';
html += '</div></div>';
}
html += '</div>';
} else {
html += '<div class="card"><div style="padding:24px;text-align:center;color:#8b949e;">✅ All changes processed. No pending approval requests.</div></div>';
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
}
}
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
document.getElementById('panel-' + name).style.display = '';
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4}[name] || 1) + ')').classList.add('active');
if (name === 'overview') {
document.getElementById('panel-overview').innerHTML = '<div class="main-header"><h2>System Overview</h2><div id="header-status" class="status-badge">Loading...</div></div><div id="loading" class="loading">Loading...</div><div id="error" class="error" style="display:none;"></div><div id="content"><div class="row"><div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div></div><div class="card" style="margin-top:16px"><div class="card-title">Event Bus Status</div><pre id="bus-status">-</pre></div></div>';
fetchData();
}
const loaders = {
'arch-owner': loadOwnerGate,
'arch-approve': loadApprove,
versions: loadVersions,
commands: loadCommands,
agents: loadAgents,
};
const fn = loaders[name];
if (fn) fn();
}
fetchData();
</script>
</body>
</html>
+120
View File
@@ -0,0 +1,120 @@
"""System blocker memory and recurring issue detection.
Records blocking issues, long-no-response incidents and repeated operational
failures. Repeated blockers are escalated from incident handling into mechanism
optimization work.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class SystemBlockerRecord:
blocker_id: str
category: str
title: str
source: str
severity: str
status: str = "open"
first_seen_at: str = field(default_factory=utc_now)
last_seen_at: str = field(default_factory=utc_now)
occurrence_count: int = 1
evidence: list[str] = field(default_factory=list)
mechanism_gap: bool = False
recommended_owner: str = "autonomous-controller"
def fingerprint(self) -> str:
return f"{self.category}:{self.title}".lower().replace(" ", "_")
def to_dict(self) -> dict[str, Any]:
return asdict(self) | {"fingerprint": self.fingerprint()}
@dataclass(frozen=True)
class BlockerEscalationDecision:
status: str
priority: str
reason: str
action: str
assigned_to: str
create_task: bool = True
architect_review_required: bool = False
owner_gate_required: bool = False
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class SystemBlockerMemory:
"""In-memory decision logic; production storage can wrap this with persistence."""
def record(self, existing: list[dict[str, Any]], incoming: SystemBlockerRecord) -> dict[str, Any]:
records = [dict(item) for item in existing]
fp = incoming.fingerprint()
for item in records:
if item.get("fingerprint") == fp or f"{item.get('category')}:{item.get('title')}".lower().replace(" ", "_") == fp:
item["occurrence_count"] = int(item.get("occurrence_count", 1)) + 1
item["last_seen_at"] = utc_now()
item["status"] = "recurrent"
evidence = list(item.get("evidence", [])) + incoming.evidence
item["evidence"] = list(dict.fromkeys(evidence))
item["mechanism_gap"] = self.is_mechanism_gap(item)
item["fingerprint"] = fp
return item
item = incoming.to_dict()
item["mechanism_gap"] = self.is_mechanism_gap(item)
records.append(item)
return item
def decide(self, blocker: dict[str, Any]) -> BlockerEscalationDecision:
count = int(blocker.get("occurrence_count", 1))
severity = blocker.get("severity", "medium")
category = blocker.get("category", "unknown")
if severity in {"critical", "p0"} or category in {"git_permission", "runtime_deadlock", "autonomous_controller_stall"}:
return BlockerEscalationDecision(
status="escalate_to_mechanism_fix",
priority="P0",
reason="blocking infrastructure issue affects autonomous operation",
action="create_self_healing_mechanism",
assigned_to="openclaw",
architect_review_required=True,
)
if count >= 3:
return BlockerEscalationDecision(
status="escalate_to_mechanism_fix",
priority="P1",
reason="recurring issue exceeded threshold",
action="create_root_cause_and_prevention_patch",
assigned_to="autonomous-controller",
architect_review_required=True,
)
if category == "long_no_response":
return BlockerEscalationDecision(
status="create_monitoring_task",
priority="P1",
reason="long no response requires watchdog and notification",
action="create_stall_watchdog_task",
assigned_to="openclaw",
)
return BlockerEscalationDecision(
status="track_incident",
priority="P2",
reason="single incident tracked for recurrence",
action="record_and_monitor",
assigned_to="autonomous-controller",
create_task=False,
)
def is_mechanism_gap(self, blocker: dict[str, Any]) -> bool:
return int(blocker.get("occurrence_count", 1)) >= 2 or blocker.get("severity") in {"critical", "p0"}
__all__ = ["SystemBlockerRecord", "BlockerEscalationDecision", "SystemBlockerMemory"]
+32
View File
@@ -0,0 +1,32 @@
"""System Health Center API."""
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from .system_health_center import SystemHealthCenter
center = SystemHealthCenter()
def get_system_health_overview():
return center.build()
def build_router():
if APIRouter is None:
return None
router = APIRouter(prefix="/admin/ai-evolution/system-health", tags=["system-health"])
@router.get("/overview", operation_id="getSystemHealthOverview")
def overview():
return get_system_health_overview()
return router
router = build_router()
__all__ = ["router", "get_system_health_overview"]
@@ -0,0 +1,54 @@
"""Unified AIOS system health center model."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
class SystemHealthCenter:
def build(
self,
git_report: dict[str, Any] | None = None,
blockers: list[dict[str, Any]] | None = None,
tasks: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
blockers = blockers or []
tasks = tasks or []
return {
"module": "system-health-center",
"route": "/admin/ai-evolution/system-health",
"title": "AIOS 系统健康与自优化中心",
"generated_at": utc_now(),
"status": self._status(git_report, blockers),
"git": git_report or {},
"blockers": {
"total": len(blockers),
"recurrent": len([b for b in blockers if b.get("mechanism_gap")]),
"p0": len([b for b in blockers if b.get("severity") in {"p0", "critical"}]),
},
"tasks": {
"optimization_tasks": len([t for t in tasks if t.get("source") == "system_blocker"]),
"pending": len([t for t in tasks if t.get("status") in {"pending", "running", "waiting_approval"}]),
},
"optimization_loop": {
"detect": True,
"remember": True,
"optimize": True,
"verify": True,
},
}
def _status(self, git_report, blockers):
if git_report and git_report.get("status") not in {"healthy", "ok"}:
return "attention_required"
if any(b.get("severity") in {"p0", "critical"} for b in blockers):
return "attention_required"
return "healthy"
__all__ = ["SystemHealthCenter"]
@@ -1,36 +0,0 @@
# CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Command ID
CMD-20260708-fix-git-actions-write-and-enable-audit-plane
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
解决当前 AIOS Git Actions 写权限问题,恢复 ChatGPT Actions → AIOS GitOps API → Git Commit 链路,
同时建立 Agent 全生命周期审计体系。
## Status
- Part 1 (Fix Git Actions Write Permission): ✅ COMPLETE
- Part 2 (Agent Command Plane Structure): ✅ COMPLETE
- Part 3 (Agent Execution Audit): ✅ COMPLETE
- Part 4 (Conversation Trace): 🚧 IN PROGRESS
- Part 5 (Version Association): ⏳ PENDING
- Part 6 (Web UI Integration): ⏳ PENDING
- Part 7 (Evidence): ⏳ PENDING
- Part 8 (Regression Test): ⏳ PENDING
## Completion Criteria
1. Code committed: agents/, audit/, evidence/, web-ui/
2. Doc: docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
3. Report: evidence/CMD-20260708.../final-report.md
4. Output: AIOS_AGENT_AUDIT_PLANE_READY, AIOS_GIT_ACTION_WRITE_READY
5. Next command created: CMD-20260709-autonomous-controller-enable
@@ -1,35 +0,0 @@
# CMD-20260709-enable-autonomous-controller
## Command ID
CMD-20260709-enable-autonomous-controller
## Priority
P0
## Mode
autonomous
## Target
Primary: OpenClaw
Supporting: Hermes
Review: Codex
## Objective
Enable autonomous controller for AIOS GitOps:
- Auto-detect issues
- Auto-diagnose
- Auto-generate fixes
- Auto-test
- Auto-create patches
- Auto-request approval
## Preconditions
- Agent Command Plane initialized (CMD-BOOTSTRAP-001)
- Git Actions write permission fixed (CMD-20260708)
- Audit plane enabled (CMD-20260708)
- Notification service enabled (CMD-20260708)
## Completion Criteria
- Autonomous controller service running
- Can detect, diagnose, fix, and report
- Pipeline: detect → diagnose → fix → test → patch → approve → commit → notify
@@ -1,7 +0,0 @@
{
"nid": "n-1783527185",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:13:05+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783529900",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:20+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783529914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T00:58:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783530514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:08:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783531114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:18:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783531714",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:28:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783532314",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:38:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783532914",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:48:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783533514",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T01:58:34+0800"
}
@@ -1,7 +0,0 @@
{
"nid": "n-1783534114",
"title": "⚠️ Issue Detected: branch_sync",
"body": "10 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T02:08:34+0800"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783527185",
"created": "2026-07-09T00:13:05+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"test/alias-write-verify",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/gitops-write-auth-smoke-20260706T115230Z",
"patch/gpt-actions-auth-live-20260708"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783529900",
"created": "2026-07-09T00:58:20+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"test/alias-test-file-branch",
"patch/codex-online-sync-acceptance-20260706T102819Z",
"patch/V1R2C3-tag-execute-idempotent-permission",
"patch/actions-invalid-diag-smoke-20260706T140311Z",
"patch/V1R2C2-autonomous-recovery-loop"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783529914",
"created": "2026-07-09T00:58:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783530514",
"created": "2026-07-09T01:08:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783531114",
"created": "2026-07-09T01:18:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783531714",
"created": "2026-07-09T01:28:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783532314",
"created": "2026-07-09T01:38:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783532914",
"created": "2026-07-09T01:48:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783533514",
"created": "2026-07-09T01:58:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,16 +0,0 @@
{
"patch_id": "patch-1783534114",
"created": "2026-07-09T02:08:34+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "10 branches missing from Gitea DB",
"branches": [
"patch/codex-write-auth-smoke-20260706T115256Z",
"patch/v1r3c0-user-portal-rbac-ops-20260708",
"request/tag-V1R3C0-self-loop-autonomous-engine",
"patch/fix-tag-confirmation-state-machine",
"patch/gitops-write-auth-fix"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -1,12 +0,0 @@
{
"ts": "2026-07-09T02:08:34+0800",
"ok": false,
"checks": {
"gitea": false,
"api": true,
"branch_sync": false,
"stuck": true
},
"plans": 1,
"applied": 0
}
-11
View File
@@ -1,11 +0,0 @@
{
"ts": "2026-07-09T00:00:00+09:00",
"ok": true,
"checks": {
"git_object_permission": true,
"api": true,
"branch_sync": true,
"stale_runtime_state_cleaned": true
},
"note": "Updated during PR #2 cleanup before merge"
}
@@ -1,21 +0,0 @@
{
"ts": "2026-07-09T00:59:30+0800",
"action": "command_watcher_state_clean",
"description": "清除了 command watcher 目录中的 stale 重复副本",
"result": "clean",
"stale_removed": [
{
"name": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"removed_from": ["active/", "running/"],
"preserved_in": "completed/",
"archive_path": "agents/commands/archive/"
},
{
"name": "CMD-20260709-enable-autonomous-controller.md",
"removed_from": ["running/"],
"preserved_in": "active/",
"archive_path": "agents/commands/archive/"
}
],
"note": "已完成命令保留在 completed/,副本移动到 archive/;未完成命令仅保留 active/"
}
@@ -1,17 +0,0 @@
{
"ts": "2026-07-09T01:00:00+0800",
"action": "daily_wechat_cron_verification",
"description": "验证每日 08:00 WeChat 汇报 cron 配置并手动触发 dry-run",
"cron_name": "AIOS每日自治报告",
"cron_id": "987f8619-6ccc-4370-8720-87149479a222",
"schedule": "0 8 * * * Asia/Shanghai",
"delivery": {
"mode": "announce",
"channel": "openclaw-weixin",
"to": "o9cq80wSaJq6utQBkUC4fE1sJWxY@im.wechat",
"accountId": "ed3da13663c0-im-bot"
},
"dry_run_triggered": true,
"dry_run_result": "enqueued",
"status": "verified"
}
-19
View File
@@ -1,19 +0,0 @@
{
"run_id": "c-1783527185",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:13:05+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 184.6
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 185.8
}
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"run_id": "c-1783529900",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:58:20+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 229.8
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 231.0
}
]
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"run_id": "c-1783529914",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T00:58:34+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 230.1
},
{
"n": "CMD-20260708-fix-git-actions-write-and-enable-audit-plane.md",
"age": 231.3
}
]
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783530514",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:08:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783531114",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:18:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783531714",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:28:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783532314",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:38:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783532914",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:48:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783533514",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T01:58:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"run_id": "c-1783534114",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T02:08:34+0800",
"details": {
"ok": true,
"stuck": []
}
}
+1 -25
View File
@@ -1,25 +1 @@
{
"current_baseline": "V2R1C2",
"previous_baseline": "V2R1C1",
"promoted_at": "2026-07-22T01:40:51Z",
"git": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"promoted_by": "codex/owner-approved-gitops",
"promotion_reason": "Owner-authorized V2R1C2 Unified Portal production release after exact tag binding, complete evidence verification, and production acceptance PASS.",
"version": "V2R1C2",
"release_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
"status": "PRODUCTION_DEPLOYED",
"production_service": "aios-v2r1c2-production.service",
"production_port": 18920,
"rollback_version": "V2R1C1",
"constraints": {
"product_code_changed": true,
"production_redeployed": true,
"production_traffic_changed": true,
"tag_rewritten": false,
"force_push_allowed": false,
"rollback_assets_preserved": true
}
}
{"current_baseline":"V1R3C1","promoted_at":"2026-07-08T14:31:00+08:00","previous_baseline":"V1R2C3"}
@@ -1,33 +0,0 @@
architect_agent_governance:
enabled: true
responsibilities:
requirement_change_tracking: true
requirement_document_refresh: true
architecture_document_refresh: true
deployment_document_validation: true
test_case_document_generation: true
architecture_optimization_review: true
weekly_external_research: true
github_top_implementation_review: true
sources:
requirement_sources:
- custom_gpt
- codex
- openclaw
- hermes
- user_feedback
version_decision:
criteria:
C: bug_fix_or_small_compatible_change
R: architecture_or_capability_release
V: major_architecture_or_breaking_change
research_cycle:
frequency: weekly
targets:
- agent_framework_versions
- model_versions
- github_top_implementations
- security_changes
governance:
owner_gate_required: true
audit_required: true
-22
View File
@@ -1,22 +0,0 @@
architect_research:
enabled: true
schedule:
frequency: weekly
research_sources:
- agent_framework_releases
- model_ecosystem_updates
- github_top_implementations
- security_advisories
evaluation:
required:
- relevance
- architecture_impact
- security_impact
- migration_cost
output:
- optimization_recommendation
- version_recommendation
- architecture_review_event
governance:
owner_gate_required: true
audit_required: true
@@ -1,36 +0,0 @@
architecture_owner_gate:
enabled: true
principle: owner_approves_large_architecture_changes_only
classification:
small:
owner_required: false
architect_auto_implementation: true
requirements:
- architect_review
- test_gate
- audit
medium:
owner_required: false
architect_review_required: true
implementation_after_review: true
requirements:
- architect_review
- test_gate
- audit
large:
owner_required: true
architect_auto_implementation: false
requirements:
- architect_review
- owner_architecture_approval
- test_gate
- promotion_gate
mandatory_owner_cases:
- breaking_change
- V_version_change
- production_architecture_update
- security_boundary_change
- cross_tenant_change
- permission_model_change
audit:
required: true
-18
View File
@@ -1,18 +0,0 @@
change_governance:
enabled: true
feature_change:
require_tests: true
require_user_acceptance_if_user_facing: true
architecture_change:
require_architecture_review_agent: true
require_system_optimization_review: true
acceptance_agents:
user_agent: user-acceptance-agent
architect_agent: architecture-review-agent
merge_gate:
required:
- tests_passed
- acceptance_passed
- architecture_review_passed
audit:
enabled: true
@@ -0,0 +1,35 @@
git_permission_watchdog:
enabled: true
priority: P0
workspace: /data/aios-gitops/workspace
expected_identity:
user: aiman
group: gitops
checks:
- object_owner
- object_group
- object_write_permission
- git_write_probe
repair:
enabled: true
allowed_paths:
- .git/objects
forbidden:
- chmod_777
- system_wide_permission_change
runtime:
interval_minutes: 5
create_task_on_drift: true
send_notification_on_failure: true
audit:
enabled: true
record:
- detection_time
- affected_paths
- repair_action
- repair_result
-59
View File
@@ -1,59 +0,0 @@
version: 1
policy_id: gitops-isolated-worktree-governance-20260714T232948UTC8
runtime_identity:
user: aiman
group: gitops
umask: "0002"
prohibit_root_workspace_writes: true
repository:
source_worktree: /data/aios-gitops/workspace
bare_repo: /data/aios-gitops/repo.git
worktree_root: /data/aios-gitops/worktrees
remote: origin
worktrees:
controller: /data/aios-gitops/worktrees/controller
task_pattern: /data/aios-gitops/worktrees/{agent}/{task_id}
allowed_agents:
- openclaw
- chatgpt
- controller
require_clean_start: true
remove_after_successful_push: true
retain_on_failure: true
permissions:
owner: aiman
group: gitops
directory_mode: "2775"
file_mode: "0664"
default_acl:
user:aiman: rwx
group:gitops: rwx
apply_to:
- /data/aios-gitops
- /data/aios-gitops/worktrees
preflight:
required_user: aiman
required_group: gitops
required_umask: "0002"
require_writable_git_dir: true
require_writable_task_dir: true
require_writable_result_dir: true
reject_shared_source_worktree: true
reject_existing_git_lock: true
notifications:
owner_on_received: required
owner_on_terminal: required
preferred_channel: weixin
redact_secrets: true
rollback:
preserve_source_worktree: true
remove_created_worktrees: true
restore_service_dropins: true
restore_service_state: true
-22
View File
@@ -1,22 +0,0 @@
governance_event_bus:
enabled: true
event_types:
- requirement_changed
- architecture_review_started
- architecture_review_completed
- user_acceptance_completed
- test_gate_completed
- version_decision_created
producers:
- architect-agent
- user-acceptance-agent
- test-gate-agent
- change-governance-agent
consumers:
- web-ui
- audit-plane
- owner-gate
- reporting-agent
audit:
required: true
retention: versioned
-21
View File
@@ -1,21 +0,0 @@
models:
default:
- id: gpt-5.5
provider: openai
enabled: true
fallback: false
- id: qwen-3.6-35b
provider: qwen
enabled: true
fallback: true
- id: deepseek-v4-flash
provider: deepseek
enabled: true
fallback: true
runtime_behavior:
unavailable_model:
action: warn_and_fallback
hardcoded_model_list: false
health_check_required: true
audit_model_selection: true
@@ -0,0 +1,37 @@
system_blocker_optimization:
enabled: true
purpose: convert_recurring_failures_into_mechanism_improvements
tracked_categories:
- git_permission
- runtime_stall
- command_stuck
- provider_failure
- deployment_failure
- approval_wait
- notification_failure
- agent_timeout
thresholds:
repeat_count_for_mechanism_review: 3
critical_issue_immediate_review: true
long_no_response_minutes: 15
actions:
first_occurrence:
- record_incident
- monitor
repeated_occurrence:
- create_optimization_task
- assign_autonomous_controller
- require_architect_review
critical_occurrence:
- create_p0_task
- notify_owner
- self_healing_attempt
integration:
task_center: true
audit_plane: true
event_bus: true
architect_agent: true
@@ -1,14 +0,0 @@
# Install as a systemd drop-in for GitOps/OpenClaw writer services.
# Example: /etc/systemd/system/<service>.service.d/10-runtime-identity.conf
[Service]
User=aiman
Group=gitops
SupplementaryGroups=gitops
UMask=0002
Environment=AIOS_GITOPS_ROOT=/data/aios-gitops
Environment=AIOS_BARE_REPO=/data/aios-gitops/repo.git
Environment=AIOS_WORKTREE_ROOT=/data/aios-gitops/worktrees
Environment=AIOS_RUNTIME_USER=aiman
Environment=AIOS_RUNTIME_GROUP=gitops
Environment=AIOS_RUNTIME_UMASK=0002
@@ -0,0 +1,13 @@
[Unit]
Description=AIOS Git Permission Watchdog
After=network.target
[Service]
Type=oneshot
User=aiman
Group=gitops
WorkingDirectory=/data/aios-gitops/workspace
ExecStart=/usr/bin/python3 -m active_runtime.app.git_permission_watchdog_cli
[Install]
WantedBy=multi-user.target
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Run AIOS Git Permission Watchdog periodically
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
Unit=aios-git-permission-watchdog.service
[Install]
WantedBy=timers.target
@@ -1,27 +0,0 @@
# AIOS V2R1C2 Unified Portal production metadata.
# No secret values belong in this file; runtime integrations use references.
AIOS_V2_HOST=127.0.0.1
AIOS_V2_PORT=18920
AIOS_V2_STATE_DIR=/var/lib/aios-v2r1c2-production
AIOS_PROVENANCE_STORE_FILE=/var/lib/aios-v2r1c2-production/run-provenance.sqlite3
AIOS_EXTERNAL_TASK_STORE_FILE=/var/lib/aios-v2r1c2-production/external-task-truth.sqlite3
AIOS_EXECUTOR_CATALOG_FILE=/var/lib/aios-v2r1c2-production/executor-catalog.json
AIOS_EXECUTION_PLAN_FILES=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-RELEASE.json:/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
AIOS_EXECUTION_PLAN_FILE=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
AIOS_RELEASE_VERSION=V2R1C2
AIOS_TARGET_VERSION=V2R1C2
AIOS_BASELINE_VERSION=V2R1C1
AIOS_PREVIOUS_VERSION=V2R1C1
AIOS_RELEASE_STATUS=PRODUCTION
AIOS_RELEASE_SOURCE_OID=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
AIOS_RELEASE_OID=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
AIOS_RELEASE_TAG=V2R1C2
AIOS_RELEASE_EVIDENCE_SHA256=2e27bb73b8e6297bd3077238f73a96d697fc3c3611c601c4dc9357e5fdcd8eee
AIOS_OPENCLAW_CONTRACT_VERSION=aios.openclaw.command.v2
AIOS_OPENCLAW_SIGNATURE_ALGORITHM=Ed25519
AIOS_OPENCLAW_PLUGIN_VERSION=2.1.0-rc.4
AIOS_OPENCLAW_SIGNING_KEY_ID=openclaw-v2r1-candidate-ed25519
AIOS_OWNER_WECHAT_TARGET_REF=AIOS_OWNER_WECHAT_TARGET_REF
@@ -1,44 +0,0 @@
[Unit]
Description=AIOS V2R1C2 Unified Portal Production
After=network-online.target
Wants=network-online.target
Conflicts=aios-orchestrator-gateway.service
ConditionPathExists=/opt/aios-v2r1c2/active-runtime/app/aios_web/release_application.py
ConditionPathExists=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
[Service]
Type=simple
User=aios
Group=aios
WorkingDirectory=/opt/aios-v2r1c2/active-runtime/app
EnvironmentFile=/etc/aios-v2/aios-v2r1c2-production.env
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStartPre=/usr/bin/test "$AIOS_V2_PORT" = "18920"
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_SOURCE_OID" = "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24"
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_TAG" = "V2R1C2"
ExecStart=/usr/bin/python3 -m aios_web.release_application
Restart=on-failure
RestartSec=3
TimeoutStartSec=60
TimeoutStopSec=30
KillSignal=SIGTERM
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
ReadWritePaths=/var/lib/aios-v2r1c2-production
StateDirectory=aios-v2r1c2-production
StateDirectoryMode=0750
UMask=0027
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
-7
View File
@@ -1,7 +0,0 @@
# V1R4C6 Backup Result
- version: V1R4C6
- status: NOT_STARTED
- reason: No confirmed production Stack, persistent volume, database migration target, or V1R4C5 image digest is bound to this source release. A backup cannot be claimed or executed safely without an identified target datastore.
- prohibited_actions_not_performed: production database deletion; persistent-volume deletion; docker system prune --volumes; direct copy of live database files.
- required_before_resume: confirmed Stack identity and its documented backup/restore command.
-14
View File
@@ -1,14 +0,0 @@
# V1R4C6 Deployment Result
- version: V1R4C6
- source_commit: e204a90398bf646d46245b3088226749fa056edd
- tag_commit: d8cf6058e99f5c7f2dfd2b45a406c0a7edf955b2
- image_repository: UNRESOLVED
- image_digest: UNRESOLVED
- previous_image_digest: UNRESOLVED
- portainer_environment: UNRESOLVED
- portainer_stack: UNRESOLVED
- deployment_action: NOT_EXECUTED
- reason: No source-to-image-to-Portainer-Stack mapping exists. Existing Portainer containers and known Docker workloads do not expose a V1R4C6/e204 deployment target or Portainer stack labels. The active GitOps API is a direct systemd Python process, not a Portainer-managed Docker Stack.
- rollback_status: NOT_APPLICABLE_NO_CHANGE
- final_status: BLOCKED_MISSING_CONFIRMED_PRODUCTION_STACK
-15
View File
@@ -1,15 +0,0 @@
# V1R4C6 Production Preflight Result
- version: V1R4C6
- started_at_utc: 2026-07-11T16:57:04.0596039Z
- source_tag_object: 356a8a8c1860b94b7d61a456c29228c88e57d963
- source_tag_commit: d8cf6058e99f5c7f2dfd2b45a406c0a7edf955b2
- source_commit: e204a90398bf646d46245b3088226749fa056edd
- tag_to_source_relation: tag commit is direct parent of source commit (verified)
- production_host_identifier: cn-network-mkt (hw / 1.95.164.183)
- host_preflight: Docker 29.5.3; root filesystem 29 percent used; memory available approximately 56 GiB
- GitOps_runtime: active at /data/aios-gitops/workspace via aios-gitops-model-api.service
- runtime_base: current_baseline V1R4C6; previous_baseline V1R4C5; git e204a90398bf646d46245b3088226749fa056edd; service status ok
- Portainer discovery: Portainer instances are reachable on ubuntu-admin and ubuntu-aihost, but no confirmed Portainer Environment/Stack maps to V1R4C6 or e204a90398bf646d46245b3088226749fa056edd.
- source_build_contract: target source contains only deploy/gitea/docker-compose.yml and no application Dockerfile/Containerfile or V1R4C6 image repository declaration.
- status: BLOCKED_MISSING_CONFIRMED_PRODUCTION_STACK
-7
View File
@@ -1,7 +0,0 @@
# V1R4C6 Rollback Result
- version: V1R4C6
- rollback_required: No
- rollback_executed: No
- reason: No container, Stack, database, image, or production configuration was changed during this run.
- captured_v1r4c5_digest: UNRESOLVED because no matching current production Stack was confirmed.
-11
View File
@@ -1,11 +0,0 @@
# V1R4C6 Verification Result
- version: V1R4C6
- healthcheck_result: GitOps adapter /health = 200 and /api/base = 200; this verifies the control-plane API only, not a V1R4C6 production deployment.
- gate_a_result: NOT_RUN (production admin workbench target and authenticated test path unresolved)
- gate_b_result: NOT_RUN (production Search Gateway target/provider binding unresolved)
- gate_c_result: NOT_RUN (production Agent Platform target/runtime request path unresolved)
- runtime_acceptance_result: NOT_RUN (0/8; cannot reuse non-production results)
- critical_control_plane_finding: gitea-branch-consistency-sentinel is failed with five Git-ref-to-Gitea-metadata drift entries and autonomous safe mode emitted. This is a hard safety gate failure.
- no_unresolved_critical_errors: false
- final_status: BLOCKED_MISSING_CONFIRMED_PRODUCTION_STACK
+3 -14
View File
@@ -50,20 +50,9 @@ components:
message: {type: string}
TagRequest:
type: object
required: [version, target_oid]
required: [version]
properties:
version: {type: string, example: V1R1C1}
target_oid:
type: string
pattern: '^[0-9a-fA-F]{40}$'
description: Full approved commit OID. The tag is created explicitly at this commit, never implicitly at workspace HEAD.
evidence_closure_oid:
type: string
pattern: '^[0-9a-fA-F]{40}$'
description: Optional evidence closure commit. When supplied, target_oid must be its ancestor.
approval_plan_id:
type: string
description: Audited owner-approved release or tag plan identifier.
message: {type: string}
execute:
type: boolean
@@ -220,7 +209,7 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/TagRequest'}
responses: {'200': {description: Tag request committed or tag created}, '400': {description: Invalid or unresolved target OID}, '403': {description: Forbidden}, '409': {description: Existing tag target conflict or evidence lineage mismatch}}
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
x-openai-isConsequential: true
/api/rollback:
post:
@@ -256,4 +245,4 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/NLRequest'}
responses: {'
responses: {'
@@ -1,113 +0,0 @@
# 微信文本-图片缓存隔离事故报告
## 结论
本次事故已经按最新备份链路回滚并完成第二轮评审修复。`3d9782fc346d1e7defde090ab8637e62ab28ab8a` 进一步修复了英文诊断文本和未确认引用文本误入图片路径的问题,`706c0b0` 则是后续最小修复提交。当前文档只记录证据、部署和验收边界,不把“真实 owner inbound 已完成”写成结论。
## 现象
- 纯文本消息被误答成旧图片回复,属于 stale-image reply on pure text。
- 现场证据显示 `types=1` 且 `hasMedia=false`,但后续又进入 `message_type=image`、`image_count=0`、`implicit_image_message`,最终落到 `cached_image_expired`。
- 这说明“诊断文本”被误判成图片意图后,又继承了 sender 级旧图缓存。
## 根因
- `fixes/patch_openclaw_weixin_hermes.py:768-839` 把文本意图判断和缓存回退放在同一条链上。
- `looksLikeHermesImageFollowup()` 会把含图片关键字的诊断文本推进 image intent。
- `resolveHermesFollowupImageContext()` 随后会读取 sender 级缓存;一旦缓存命中失败或过期,就会落到 `cached_image_expired`,从而让纯文本沾上旧图上下文。
- `handleHermesImplicitImage()` 继续把消息送入 `handleHermes(..., "implicit_image_message")`,所以误判会沿着同一条图片处理通道传播。
## 护栏不变量
- 图片处理上下文必须来自当前消息图片、已确认的引用图片,或精确匹配的缓存图片 ID;缺少这三类证据时,纯文本不能进入 `handleHermesImplicitImage()`。
- 未确认的引用文本不能继承 sender 级旧图缓存,必须返回 `referenced_image_not_confirmed`。
- 只有精确缓存图片 ID 命中且缓存仍有效时,才允许走 `resolveHermesCachedImageEntry()` 的 `cached_image_matched` 路径。
- `/hermes` 这类 slash 路径保持原有分流,slash/channel 面不变。
- 这次修复的守卫目标,是把中英文诊断文本、未确认引用文本与图片缓存回退拆开,避免再把普通文本提升成图片意图。
## TDD 与幂等性
- `66d637fc0364a617c7b9fd8cd9baa68fb0368675`(`fix(weixin): isolate text from stale image cache`)引入了隔离修复和 `tests/test_weixin_image_followup_intent.py`。
- `2de29c59bb946cc3db3e14e8a06270391ede8401`(`fix(weixin): add process-only runtime patch mode`)继续收紧 process-only 运行模式,保持回放幂等。
- `3d9782fc346d1e7defde090ab8637e62ab28ab8a`(`fix(weixin): require confirmed image reference context`)补齐英文诊断文本和未确认引用文本用例,并把图片引用收紧到已确认上下文。
- 用户实测的 TDD 路径为 RED `8 PASS / 1 FAIL`,随后 GREEN `9 PASS`。
- 最小修复提交为 `706c0b0`,对应 quoted pure text 的最后一层收敛修复。
- 第二轮回归结果为 `8 PASS`;node check 同样通过,重复应用补丁保持幂等。
- 生产 npm 分发侧本身缺少 `tsconfig/tests`,baseline 与 patched 共享这一限制,所以这里不把分发目录的缺口当成回归点;验收以 node check 和 8 tests 为准。
## 用户实测补充
- 普通文本两项为 PASS。
- quoted pure text 为 FAIL,日志证据包含 `ref_msg.message_item.type=0/NONE`、quoted id、`implicit_image_message`、`referenced_image_unresolved`。
- 这组证据说明 quoted pure text 没有被当成真正的图片 follow-up,且没有越过引用确认边界。
## 回滚与备份
- 回滚按最新备份执行,基线指向 `/data/aios-gitops/runtime/latest-backup-dir.txt` 所示的最新备份目录。
- 备份路径链路按供给证据记录为 `T064506Z`、`T074212Z`、`T081658Z`。
- 第一轮恢复后的部署过程哈希记录为 `e9625d12` 和 `c00c58f8`。
- v2 部署备份为 `/var/backups/aios/weixin-text-cache-isolation-20260711T094908Z`。
- v2 部署文件哈希为 TS `024c6498284d1871b048f6157d5d3dbd4d34e54b53a05afd744b00458ceeb79f`、JS `d73b15d6752056192a1d1939b04107334419785b41175b39f1cbc1132ac65c47`;slash/channel 口径保持不变。
- 首次部署因为 `-- No entries --` 门禁误判而回滚,随后纠正部署在线,PID 为 `2496603`。
- 在线修复后的 hashes 为 `86dfe...` / `a9b854...`,`channel 4` accounts healthy,cache unchanged。
## 主线合并与生产发布
- 主线 fast-forward source rev 为 `9269e819972c2e709e0c27bed7eab590da6961c9`,本次不 tag、不 baseline。
- 生产发布结论为 PASS,release dir 为 `/var/backups/aios/weixin-mainline-release-20260711T181842Z`。
- old PID 为 `2655072`,new PID 为 `2656106`。
- TS hash 为 `86dfe4543c4414bc86778a4b218c244a742a139c66e3c9f19cf6f0de871234c3`,JS hash 为 `a9b854015108020458ce6f31b9d9187eff82e170c4c214259b0afb9be6a7336f`。
- `18789` 监听,`channel 4` accounts healthy,`error_count=0`,cache unchanged。
- 一次参数门禁失败已回滚,且不影响最终发布。
- owner quoted-text live acceptance 仍为 PENDING。
## 运行态验证
- 第一轮 gateway PID 由 `4025932` 变为 `1723576`;v2 部署后由 `1723576` 变为 `1876713`。
- v2 现场/供给证据为 `active`、监听 `18789`、`error_count=0`、`channel gate pass`、`cache preserved`。
- 最终 read-only gate 通过,说明部署后运行态未出现新的插件错误,channel 状态和缓存均保持预期。
- quoted pure text 的第二次 owner live acceptance 仍为 PENDING。
## 未完成的真实验收
- owner 纯文本
- diagnostic text
- genuine image-followup message
- quoted pure text 的第二次 owner live acceptance
以上四类消息仍需要由 owner 真实 inbound 到达,才能把这次“文本隔离”结论升格为完整的线上接受;本报告明确保留该项为 pending,不把它们写成已经完成。
## 引用文字语义修复与发布(2026-07-12)
- 后续 owner 实测证明图片误路由已消失,但模型在没有被引正文时根据 `chat_id/message_id/timestamp` 编造解释,因此图片隔离 PASS、语义验收 FAIL。
- 现场 payload 的 `ref_msg.message_item.type=0/NONE` 只有 quoted ID 与时间字段,没有 `text_item`;session transcript 又没有原始 Weixin 数字 message ID,不能精确回表。
- 出站 `clientId` 与引用的 19 位数字 `msg_id` 格式不同,发送 API 也不返回服务端 message ID。本次明确禁止 recent-message、时间窗口、session 顺序和近似 ID 猜测。
- `1136b4186d7578cd77f255297204b10152c47dab` 增加证据优先处理:payload 明确含 `text_item.text` 时才构造引用上下文;引用存在但正文缺失时,经标准 dispatcher 确定性回复“无法读取被引用文字,请复制原文后再提问。”,并在模型 dispatch 前返回。
- TDD 先出现两项预期 RED(quoted-text resolver 不存在),最终 GREEN 为 `17 passed`;`py_compile`、`git diff --check`、`node --check` 均通过。
- Scratch 双跑验证只改变 `src/messaging/process-message.ts` 与 `dist/src/messaging/process-message.js`,第二次应用 diff 为 0;slash/channel 文件保持不变。
- 主线为 `1136b4186d7578cd77f255297204b10152c47dab`,生产发布目录为 `/var/backups/aios/weixin-quoted-text-failclosed-release-20260712T054451Z`。
- 发布后 PID 从 `2656106` 变为 `3670157`;TS hash 为 `1a73e735e9c8fb40a1f82d2a66bc022396c8105ed989aac95e11b66326e9fdf1`,JS hash 为 `1c76bf6ec3b45021e68b932cbf52222ba2130a867b95e41d7a91f261b991a304`。
- `18789`、4 个 Weixin accounts、post-start error 0 和图片 cache unchanged 均通过。新的 owner live acceptance 仍为 PENDING。
## Owner 引用文字 Fail-Closed 验收(2026-07-12)
- Owner 引用一条微信未提供正文的文字消息并发送“是什么意思”,收到固定回复“无法读取被引用文字,请复制原文后再提问。”。
- 日志窗口为 `2026-07-12 14:06:23.424-14:06:23.893 +08:00`:入站 `types=1`、`bodyLen=6`、`hasMedia=false`,出站 `textLen=20` 且发送成功。
- 同一窗口 `model-fetch=0`、`implicit_image_message=0`、Hermes route=0,`quoted_text_unavailable=1` 且 `quoted_message_id=present`。
- 该证据确认无正文引用不会进入模型猜测或图片缓存路径。Owner quoted-text no-body live acceptance 为 **PASS**。
- 微信 payload 明确携带 `text_item.text` 时的 grounded explanation 和 genuine image follow-up 仍作为独立兼容性验收项保留。
## 可见引用摘要解析修复(2026-07-12)
- Owner 进一步确认微信客户端能够显示被引用正文,因此上一节的“未提供正文”只适用于旧 resolver 的观察结果,不代表协议 payload 没有可用文本。
- 根因是插件 `inbound.ts` 已将协议字段 `ref_msg.title` 定义为引用摘要并拼入 `ctx.Body`,而 `extractHermesReferencedText()` 只读取 `ref_msg.message_item.text_item.text`,导致客户端可见摘要被错误丢弃。
- `13c2a182860131abcad0f5a70ae8263477578588` 在真实 `text_item.text` 缺失时,仅从 reference envelope 读取 `ref_msg.title`;不会读取 `message_item.title` 或任意嵌套通用 `title`,也不改变图片、slash 或 channel 路由。
- TDD 先得到 `18 passed / 1 failed`,失败项为可见 `ref_msg.title` 仍落到 `quoted_text_unavailable`;修复后为 `19 passed`,`py_compile`、`node --check` 与 `git diff --check` 通过。
- 生产发布目录为 `/var/backups/aios/weixin-quoted-text-title-resolution-20260712T155112+0800`,PID 从 `3859376` 变为 `3860863`,post-start error 为 0,图片 cache unchanged。
- 部署后 TS hash 为 `8e77ce61214688a2507b07f0cf1ee85401b58198b8271bebd69efc2681872726`,JS hash 为 `663db8d30a5cd5eb80fe46cf941cce6e6f5d04cb7e4b8d9b4d0086d87faba290`。
- 可见正文引用的 owner live acceptance 尚待下一条真实微信消息确认;预期日志必须命中 `quoted_text_resolved`,且同一窗口不得出现 `quoted_text_unavailable` 或图片路由。
## 安全说明
- 本次仅更新本报告,没有修改代码、runtime、services 或 secrets。
- 报告里没有任何凭据、密钥或原始敏感 ID。
-212
View File
@@ -1,212 +0,0 @@
# V1R4C5 Acceptance Evidence
Date: 2026-07-11
Status: release candidate evidence in progress
Current active baseline: V1R4C3
V1R4C5 has not been promoted yet.
## Owner Decision
Owner approved continued V1R4C5 work under the following constraints:
```text
允许无 force-push 的分支 reconcile
memory search 保持关闭
先完成 SR-01/SR-02/数据隔离验收
再进入 merge/deploy/tag owner gate
```
## Scope
V1R4C5 release candidate scope:
1. SR-01 Gitea reliability and branch consistency guardrails.
2. SR-02 watcher/stuck command cleanup.
3. User data isolation redline and enforcement gates.
4. SR-04 admin workbench read-only visibility preparation only.
Explicitly excluded from V1R4C5 activation:
- memory search activation;
- full SR-04 portal production rollout;
- bulk merge of old test/smoke/auth branches;
- force push reconciliation;
- Gitea DB mutation without owner/admin approval.
## Runtime Acceptance Evidence
### Runtime status
Observed:
```json
{
"service": "aios-gitops-model-api",
"status": "active",
"workspace_branch": "patch/v1r4c5-runtime-acceptance-clean-20260711-01"
}
```
### Baseline
Observed:
```json
{
"current_baseline": "V1R4C3",
"previous_baseline": "V1R3C1",
"promoted_by": "openclaw"
}
```
### Clean createBranch acceptance
Branch:
```text
test/v1r4c5-runtime-acceptance-clean-20260711-01
```
Result:
```text
status: ok
commit: 6ae7058
sync: synced
```
### Clean createPatch acceptance
Patch:
```text
patch/v1r4c5-runtime-acceptance-clean-20260711-01
```
Patch id:
```text
patch-1783706123789-f5ca8b69
```
Commit:
```text
ba0391e
```
Result:
```text
synced
```
## SR-01 Evidence
Known accepted evidence:
- Gitea branch consistency sentinel deployed.
- Sentinel reports:
```json
{
"status": "healthy",
"unhealthy_checks": []
}
```
- systemd timer runs every 5 minutes.
- service runs with:
```text
--emit-safe-mode --json
```
- broken symlink issue resolved by replacing the external reliability symlink with workspace-local files.
Required before final promotion:
- confirm latest sentinel report remains healthy after acceptance writes.
## SR-02 Evidence
Known accepted evidence:
```text
11 / 11 tests pass
```
Fix summary:
- stuck command detection reads parsed timestamps through `data.get('ts', 0)`;
- `.md` fixture mtime is aligned with stored stuck timestamp via `os.utime(path, (ts, ts))`;
- behavior is consistent across `.json` and `.md` state formats.
Required before final promotion:
- re-run SR-02 tests on the final integration candidate.
## User Data Isolation Evidence
Implemented controls:
- user data isolation redline;
- memory scope guard;
- provider gate;
- activation gate;
- audit service/timer;
- CI acceptance scripts;
- memory search activation check.
Required safety state:
```text
AIOS_MEMORY_ENABLED=false
```
Required before memory/search activation:
- isolation audit healthy;
- no denied records;
- no invalid records;
- provider filters by scope before ranking;
- provenance required;
- global index disabled;
- activation gate returns allowed;
- explicit owner approval.
V1R4C5 does not approve memory search activation.
## SR-04 Admin Workbench Evidence
Current user-visible gap is confirmed:
- administrator cannot yet see task progress;
- administrator cannot yet see flow/process diagrams;
- SR-04 remains activation-ready but gated.
V1R4C5 scope allows only read-only visibility preparation and acceptance design, not full production rollout.
Required before SR-04 deployment:
- SR-01 green;
- SR-02 green;
- user data isolation gate accepted;
- admin RBAC read-only check green.
## Remaining Required Gates Before Promotion
- [ ] Latest Gitea sentinel report remains healthy.
- [ ] SR-02 watcher tests pass on final candidate.
- [ ] User data isolation acceptance passes.
- [ ] Admin workbench read-only RBAC acceptance passes or remains explicitly deferred.
- [ ] No force-push was used.
- [ ] Memory search remains disabled.
- [ ] Owner approves merge/deploy/tag/promotion.
## Promotion Rule
V1R4C5 may not be promoted until all required gates are green or explicitly deferred with owner approval.
@@ -1,54 +0,0 @@
# V1R4C5 Final Owner Approval
Date: 2026-07-11
Status: approved for final promotion
## Owner Decision
Owner approved final V1R4C5 promotion.
Memory search must remain disabled.
```text
允许最终晋升
继续保持 memory search 关闭
```
## Promotion Scope
Approved for V1R4C5:
1. SR-01 Gitea reliability and branch consistency guardrails.
2. SR-02 watcher/stuck command cleanup.
3. User data isolation redline and enforcement gates.
4. SR-04 admin workbench read-only visibility preparation only.
## Explicit Non-Activation
The following are not approved by this promotion:
```text
memory search activation
full cross-session memory recall
global vector index
full SR-04 production portal rollout beyond read-only/admin visibility preparation
bulk merge of old test/smoke/auth branches
force-push based reconciliation
```
## Required Runtime State
Memory remains disabled:
```text
AIOS_MEMORY_ENABLED=false
```
If a provider or deployment attempts to enable memory/search, it must be blocked unless a future owner approval explicitly allows it after the user-data isolation activation gate returns allowed.
## Final Promotion Rule
V1R4C5 may proceed to tag/promotion under owner gate.
If any final gate fails, promotion must stop and memory/search must remain disabled.

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