Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68eb88cf04 | ||
|
|
3218b89a2c | ||
|
|
6ac62c265b | ||
|
|
2322120f39 | ||
|
|
c27b91c708 |
@@ -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,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和回滚能力,生产业务未变化。
|
|
||||||
```
|
|
||||||
+7
-12
@@ -1,13 +1,8 @@
|
|||||||
{
|
{
|
||||||
"current_baseline": "V1R4C6",
|
"current_baseline": "V1R4C3",
|
||||||
"previous_baseline": "V1R4C5",
|
"previous_baseline": "V1R3C1",
|
||||||
"promoted_at": "2026-07-11T21:00:00+08:00",
|
"promoted_at": "2026-07-09T17:44:57+08:00",
|
||||||
"git": "e204a90398bf646d46245b3088226749fa056edd",
|
"git": "f5435e308794c737f3e39de3f6b79807486943c3",
|
||||||
"promoted_by": "openclaw/admin",
|
"promoted_by": "openclaw",
|
||||||
"promotion_reason": "V1R4C6 admin search agent integration, search gateway, agent platform, VERSION bump, merge tests deploy pipeline — not yet deployed to production",
|
"promotion_reason": "V1R4C3 autonomous operations, search gateway, task center, owner gate, and blocker self-optimization baseline promotion"
|
||||||
"constraints": {
|
}
|
||||||
"memory_search_enabled": false,
|
|
||||||
"force_push_allowed": false,
|
|
||||||
"gitea_db_mutation_requires_owner_approval": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
cmd_id: CMD-20260710-memory-index-rebuild
|
||||||
|
operation: memory.index.rebuild
|
||||||
|
risk: medium
|
||||||
|
permission: OWNER_NON_DANGEROUS_ALL
|
||||||
|
status: pending_owner_gate
|
||||||
|
reason: Memory search is disabled because existing embedding index metadata does not match the current embedding provider configuration.
|
||||||
|
requested_by: codex-chartgpt-bridge
|
||||||
|
requested_at: "2026-07-10T08:00:00+08:00"
|
||||||
|
command_key: openclaw.memory.index.force
|
||||||
|
args:
|
||||||
|
force: true
|
||||||
|
safety:
|
||||||
|
destructive: false
|
||||||
|
arbitrary_shell: false
|
||||||
|
credential_mutation: false
|
||||||
|
provider_mutation: false
|
||||||
|
external_network_write: false
|
||||||
|
production_data_delete: false
|
||||||
|
baseline_or_tag_mutation: false
|
||||||
|
main_branch_direct_write: false
|
||||||
|
owner_gate:
|
||||||
|
required: true
|
||||||
|
approval_model: protected_branch_merge_or_signed_owner_commit
|
||||||
|
approved_by: null
|
||||||
|
approved_at: null
|
||||||
|
approval_commit: null
|
||||||
|
execution:
|
||||||
|
runner: owner-maintenance-runner
|
||||||
|
lock_key: memory-index-rebuild
|
||||||
|
timeout_seconds: 1800
|
||||||
|
executed_by: null
|
||||||
|
executed_at: null
|
||||||
|
exit_code: null
|
||||||
|
acceptance:
|
||||||
|
commands:
|
||||||
|
- openclaw memory search "V1R4C3"
|
||||||
|
- openclaw memory search "Gitea Branch Sync"
|
||||||
|
- openclaw memory search "Autonomous Controller"
|
||||||
|
expected:
|
||||||
|
- no_embedding_metadata_mismatch
|
||||||
|
- memory_search_returns_results
|
||||||
|
- memory_index_uses_current_embedding_provider
|
||||||
|
success_signals:
|
||||||
|
- AIOS_OWNER_MAINTENANCE_MEMORY_INDEX_REBUILT
|
||||||
|
- AIOS_MEMORY_SEARCH_RESTORED
|
||||||
|
rollback:
|
||||||
|
destructive: false
|
||||||
|
strategy: rebuild_previous_index_if_snapshot_exists
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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。
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Owner Maintenance Gate
|
||||||
|
|
||||||
|
This mechanism allows owner-approved non-dangerous maintenance actions without giving agents arbitrary host CLI access.
|
||||||
|
|
||||||
|
## Permission tiers
|
||||||
|
|
||||||
|
- READ_ONLY: status, health, baseline, versions, search inspection.
|
||||||
|
- AUTO_PATCH: branch creation, repo file changes, patch commits, test/report artifacts.
|
||||||
|
- OWNER_NON_DANGEROUS_ALL: owner may approve all non-destructive allowlisted maintenance command_keys.
|
||||||
|
- OWNER_HIGH_RISK: main merge, baseline promotion, tag creation, credential/provider mutation, destructive cleanup, production restart, SSH/firewall changes.
|
||||||
|
|
||||||
|
## Execution rule
|
||||||
|
|
||||||
|
Agents can create requests. The runner can only execute fixed allowlist mappings:
|
||||||
|
|
||||||
|
```text
|
||||||
|
command_key: openclaw.memory.index.force
|
||||||
|
argv: /usr/local/bin/openclaw memory index --force
|
||||||
|
```
|
||||||
|
|
||||||
|
The runner rejects arbitrary shell strings such as `bash -c`, `sh -c`, `curl | bash`, or recursive destructive commands.
|
||||||
|
|
||||||
|
## Owner gate
|
||||||
|
|
||||||
|
A request becomes executable only after protected-branch merge or signed owner commit fills:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
owner_gate:
|
||||||
|
approved_by: <owner>
|
||||||
|
approved_at: <timestamp>
|
||||||
|
approval_commit: <signed-owner-commit-sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory index rebuild flow
|
||||||
|
|
||||||
|
1. Detect embedding metadata mismatch.
|
||||||
|
2. Create `commands/owner-requests/CMD-20260710-memory-index-rebuild.yaml`.
|
||||||
|
3. Owner approves through GitOps owner gate.
|
||||||
|
4. `runtime/owner_maintenance/allowlist_runner.py` validates safety metadata.
|
||||||
|
5. Runner executes the fixed command vector.
|
||||||
|
6. Runner records exit code and stdout/stderr hashes.
|
||||||
|
7. Acceptance checks verify memory search works again.
|
||||||
|
|
||||||
|
Expected signals:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AIOS_OWNER_MAINTENANCE_MEMORY_INDEX_REBUILT
|
||||||
|
AIOS_MEMORY_SEARCH_RESTORED
|
||||||
|
```
|
||||||
@@ -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.
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# V1R4C5 Release to Main Reconcile Plan
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
Status: reconcile preparation
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Bring the V1R4C5 release state back into `main` without rewriting history and without moving the immutable V1R4C5 tag.
|
|
||||||
|
|
||||||
## Source of Truth
|
|
||||||
|
|
||||||
Release baseline:
|
|
||||||
|
|
||||||
```text
|
|
||||||
V1R4C5
|
|
||||||
```
|
|
||||||
|
|
||||||
Promotion commit:
|
|
||||||
|
|
||||||
```text
|
|
||||||
a32c76a3c1baa49562e53fb8adce10a5ed0e6704
|
|
||||||
```
|
|
||||||
|
|
||||||
Tag:
|
|
||||||
|
|
||||||
```text
|
|
||||||
V1R4C5
|
|
||||||
```
|
|
||||||
|
|
||||||
Tag policy:
|
|
||||||
|
|
||||||
```text
|
|
||||||
immutable
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reconcile Rules
|
|
||||||
|
|
||||||
Allowed:
|
|
||||||
|
|
||||||
- normal merge commits;
|
|
||||||
- fast-forward where possible;
|
|
||||||
- conflict resolution with review;
|
|
||||||
- updating main to contain released C5 state.
|
|
||||||
|
|
||||||
Forbidden:
|
|
||||||
|
|
||||||
- force-push;
|
|
||||||
- tag movement;
|
|
||||||
- rewriting release history;
|
|
||||||
- importing V1R4C6 feature code into this reconcile.
|
|
||||||
|
|
||||||
## Expected Included C5 Content
|
|
||||||
|
|
||||||
- SR-01 Gitea reliability guardrails.
|
|
||||||
- Gitea branch consistency sentinel.
|
|
||||||
- SR-02 watcher state cleanup.
|
|
||||||
- User Data Isolation gate.
|
|
||||||
- Reliability governance documents.
|
|
||||||
- Admin Workbench read-only preparation records.
|
|
||||||
|
|
||||||
## Expected Excluded Content
|
|
||||||
|
|
||||||
Do not include:
|
|
||||||
|
|
||||||
- V1R4C6 Admin Workbench implementation code.
|
|
||||||
- SR-03 Search Gateway implementation.
|
|
||||||
- Agent Platform integration.
|
|
||||||
- Memory search activation.
|
|
||||||
- Global vector index.
|
|
||||||
|
|
||||||
## Validation Before Main Merge
|
|
||||||
|
|
||||||
Required:
|
|
||||||
|
|
||||||
```text
|
|
||||||
getBaseline.current_baseline == V1R4C5
|
|
||||||
V1R4C5 tag unchanged
|
|
||||||
memory search disabled
|
|
||||||
Gitea sentinel healthy
|
|
||||||
SR-02 tests pass
|
|
||||||
main merge does not include V1R4C6 branches
|
|
||||||
```
|
|
||||||
|
|
||||||
## Post Merge Validation
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```text
|
|
||||||
getRuntimeStatus
|
|
||||||
getBaseline
|
|
||||||
createBranch
|
|
||||||
createPatch
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
|
|
||||||
```text
|
|
||||||
main contains V1R4C5 release state
|
|
||||||
active baseline remains V1R4C5
|
|
||||||
```
|
|
||||||
|
|
||||||
## Owner Gate
|
|
||||||
|
|
||||||
Normal merge to `main` may proceed after validation.
|
|
||||||
|
|
||||||
No tag creation or baseline promotion is required because V1R4C5 is already promoted.
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# V1R4C6 Acceptance Checklist
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
Base baseline: V1R4C5
|
|
||||||
|
|
||||||
Promotion blocked until every mandatory gate is green.
|
|
||||||
|
|
||||||
## Global Preflight
|
|
||||||
|
|
||||||
- [ ] `getRuntimeStatus.status == active`
|
|
||||||
- [ ] `getBaseline.current_baseline == V1R4C5`
|
|
||||||
- [ ] `getBaseline.git == a32c76a3c1baa49562e53fb8adce10a5ed0e6704`
|
|
||||||
- [ ] `AIOS_GITOPS_BARE_REPO` points to `/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git`
|
|
||||||
- [ ] Gitea consistency sentinel reports `healthy`
|
|
||||||
- [ ] `AIOS_MEMORY_ENABLED=false`
|
|
||||||
- [ ] no force-push used
|
|
||||||
|
|
||||||
## Gate A — Admin Workbench / Portal AIOS
|
|
||||||
|
|
||||||
### Route/API
|
|
||||||
|
|
||||||
- [ ] `/portal/me/aios/manifest` returns authenticated module manifest
|
|
||||||
- [ ] `/portal/me/aios/dashboard` returns admin-authorized dashboard data
|
|
||||||
- [ ] `/portal/me/aios/health` returns system health data
|
|
||||||
- [ ] failed/empty backend data produces explicit empty/error state
|
|
||||||
|
|
||||||
### UI
|
|
||||||
|
|
||||||
- [ ] admin sees task progress
|
|
||||||
- [ ] admin sees process/flow diagram entry
|
|
||||||
- [ ] admin sees system health
|
|
||||||
- [ ] admin sees authorized agent observability
|
|
||||||
- [ ] user does not see admin-only modules
|
|
||||||
- [ ] admin does not see owner-only modules without owner role
|
|
||||||
|
|
||||||
### Isolation
|
|
||||||
|
|
||||||
- [ ] no cross-user data
|
|
||||||
- [ ] no cross-project data
|
|
||||||
- [ ] no cross-session data
|
|
||||||
- [ ] no cross-tenant/group data
|
|
||||||
|
|
||||||
Gate A result: `PASS / FAIL / DEFER`
|
|
||||||
|
|
||||||
## Gate B — SR-03 Search Gateway
|
|
||||||
|
|
||||||
### Provider Binding
|
|
||||||
|
|
||||||
- [ ] real provider configured and reachable
|
|
||||||
- [ ] provider errors return safe non-memory fallback
|
|
||||||
- [ ] timeout/retry/circuit-breaker behavior verified
|
|
||||||
|
|
||||||
### Scope and Provenance
|
|
||||||
|
|
||||||
- [ ] scope filter occurs before ranking
|
|
||||||
- [ ] provenance returned for accepted results
|
|
||||||
- [ ] denied/out-of-scope content never reaches ranking or summary
|
|
||||||
- [ ] global memory/vector index disabled
|
|
||||||
- [ ] memory search remains disabled
|
|
||||||
|
|
||||||
### Audit
|
|
||||||
|
|
||||||
- [ ] search request scope is logged without leaking content
|
|
||||||
- [ ] provider/fallback decision is auditable
|
|
||||||
- [ ] denied scope mismatch is auditable
|
|
||||||
|
|
||||||
Gate B result: `PASS / FAIL / DEFER`
|
|
||||||
|
|
||||||
## Gate C — Agent Platform
|
|
||||||
|
|
||||||
### Event Bus
|
|
||||||
|
|
||||||
- [ ] event envelope includes tenant/group, user, project, session and agent scope
|
|
||||||
- [ ] cross-scope subscribers cannot receive events
|
|
||||||
- [ ] failed delivery is observable and retry-bounded
|
|
||||||
|
|
||||||
### GPT Message Plane
|
|
||||||
|
|
||||||
- [ ] message envelope includes scope and provenance
|
|
||||||
- [ ] messages cannot cross user/project/session boundaries
|
|
||||||
- [ ] denied messages do not forward content
|
|
||||||
|
|
||||||
### Controller Dispatch
|
|
||||||
|
|
||||||
- [ ] dispatch stops when autonomous safe mode is active
|
|
||||||
- [ ] consequential actions require owner gate
|
|
||||||
- [ ] duplicate dispatch is idempotent or rejected
|
|
||||||
- [ ] stuck state cleanup remains green
|
|
||||||
|
|
||||||
### Observability
|
|
||||||
|
|
||||||
- [ ] admin can see platform status without unrestricted content access
|
|
||||||
- [ ] failures and queue state are visible
|
|
||||||
|
|
||||||
Gate C result: `PASS / FAIL / DEFER`
|
|
||||||
|
|
||||||
## Combined Runtime Acceptance
|
|
||||||
|
|
||||||
Run after integration and deployment:
|
|
||||||
|
|
||||||
```text
|
|
||||||
getRuntimeStatus
|
|
||||||
getBaseline
|
|
||||||
createBranch
|
|
||||||
createPatch
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected before promotion:
|
|
||||||
|
|
||||||
```text
|
|
||||||
runtime = active
|
|
||||||
baseline = V1R4C5
|
|
||||||
write branch = committed + synced
|
|
||||||
write patch = committed + synced
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rollback Validation
|
|
||||||
|
|
||||||
- [ ] Group A can disable workbench routes/menu entries
|
|
||||||
- [ ] Group B can disable provider binding and use safe non-memory fallback
|
|
||||||
- [ ] Group C can disable expanded dispatch
|
|
||||||
- [ ] all rollback modes preserve `AIOS_MEMORY_ENABLED=false`
|
|
||||||
|
|
||||||
## Release Decision
|
|
||||||
|
|
||||||
V1R4C6 is tag-ready only when:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Global preflight PASS
|
|
||||||
AND Gate A PASS
|
|
||||||
AND Gate B PASS
|
|
||||||
AND Gate C PASS
|
|
||||||
AND combined runtime acceptance PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
Any failed mandatory gate blocks tag, deployment promotion, and baseline promotion.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# V1R4C6 Acceptance Evidence
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
## Merge
|
|
||||||
|
|
||||||
- **PR #6** (merge commit `ae49bf0fa35b0f599a96a4d12644576447f39cec`): V1R4C6 release-candidate control artifacts merged to main.
|
|
||||||
- **Result**: PASS
|
|
||||||
- 10 files (1440 lines) added: release docs, patch records, ops commands, version metadata
|
|
||||||
- No V1R4C6 tag created
|
|
||||||
- No baseline promotion
|
|
||||||
- Memory search remains disabled
|
|
||||||
- Force-push remains disabled
|
|
||||||
|
|
||||||
## Verified State (post-merge)
|
|
||||||
|
|
||||||
| Check | Result |
|
|
||||||
|--------------------------|--------|
|
|
||||||
| main HEAD | ae49bf0fa35b0f599a96a4d12644576447f39cec |
|
|
||||||
| V1R4C5 tag | Still at d457ac396147043ed5d0a3fd8b44bcb10f48bceb (not moved) |
|
|
||||||
| V1R4C6 tag | absent |
|
|
||||||
| baseline/current.json | current_baseline = V1R4C5, git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704 |
|
|
||||||
| memory_search_enabled | false |
|
|
||||||
| force_push_allowed | false |
|
|
||||||
| versions/V1R4C5.json | exists |
|
|
||||||
| versions/V1R4C6.json | exists |
|
|
||||||
|
|
||||||
## Gates
|
|
||||||
|
|
||||||
| Gate | Status | Notes |
|
|
||||||
|------------------------------|---------|--------------------------------|
|
|
||||||
| control artifacts merge | PASS | PR #6 merged to main |
|
|
||||||
| Gate A — Admin Workbench | NOT RUN | Engineering integration pending|
|
|
||||||
| Gate B — SR-03 Search Gateway| NOT RUN | Engineering integration pending|
|
|
||||||
| Gate C — Agent Platform | NOT RUN | Engineering integration pending|
|
|
||||||
| tests | NOT RUN | No test suite executed |
|
|
||||||
| artifact digest | NOT CREATED | No build performed |
|
|
||||||
| deployment | NOT RUN | No deploy performed |
|
|
||||||
| production verification | NOT RUN | No production verification |
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
Revert command (creates new branch and PR, non-destructive):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git revert -m 1 ae49bf0fa35b0f599a96a4d12644576447f39cec
|
|
||||||
```
|
|
||||||
|
|
||||||
## Current Overall Status
|
|
||||||
|
|
||||||
**BLOCKED / INTEGRATION PENDING** — all engineering gates remain unexecuted.
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# V1R4C6 Blocker: Baseline Visibility Discrepancy
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
Status: blocking merge/deploy/tag
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
OpenClaw/operator reported V1R4C5 baseline promotion completed successfully, but the ChatGPT GitOps action view still reports the active baseline as V1R4C3.
|
|
||||||
|
|
||||||
V1R4C6 must not proceed to merge/deploy/tag until this view is reconciled.
|
|
||||||
|
|
||||||
## Operator Reported State
|
|
||||||
|
|
||||||
Reported by operator:
|
|
||||||
|
|
||||||
```text
|
|
||||||
current_baseline = V1R4C5
|
|
||||||
previous_baseline = V1R4C3
|
|
||||||
promotion_git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704
|
|
||||||
promoted_by = openclaw/admin
|
|
||||||
memory_search_enabled = false
|
|
||||||
force_push_allowed = false
|
|
||||||
gitea_db_mutation_requires_owner_approval = true
|
|
||||||
```
|
|
||||||
|
|
||||||
## ChatGPT GitOps Action Observed State
|
|
||||||
|
|
||||||
Observed through `getBaseline`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
current_baseline = V1R4C3
|
|
||||||
previous_baseline = V1R3C1
|
|
||||||
git = f5435e308794c737f3e39de3f6b79807486943c3
|
|
||||||
promoted_by = openclaw
|
|
||||||
```
|
|
||||||
|
|
||||||
Observed through `listVersions`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
V1R4C5 tag exists
|
|
||||||
versions_dir does not list canonical V1R4C5.json
|
|
||||||
workspace_branch = release/v1r4c6-admin-search-agent-integration-20260711
|
|
||||||
```
|
|
||||||
|
|
||||||
## Risk
|
|
||||||
|
|
||||||
If V1R4C6 proceeds while different controllers disagree on the active baseline, the release may be built against the wrong base and post-deployment verification may be invalid.
|
|
||||||
|
|
||||||
## Required Reconciliation
|
|
||||||
|
|
||||||
Before continuing V1R4C6:
|
|
||||||
|
|
||||||
1. Confirm the GitOps API used by ChatGPT reads the same `baseline/current.json` updated by OpenClaw.
|
|
||||||
2. Confirm the API service has reloaded or refreshed baseline state.
|
|
||||||
3. Confirm canonical `versions/V1R4C5.json` is visible to the action runtime or intentionally explain why it is not required.
|
|
||||||
4. Re-run:
|
|
||||||
|
|
||||||
```text
|
|
||||||
getBaseline
|
|
||||||
listVersions
|
|
||||||
getRuntimeStatus
|
|
||||||
```
|
|
||||||
|
|
||||||
Required result:
|
|
||||||
|
|
||||||
```text
|
|
||||||
getBaseline.current_baseline = V1R4C5
|
|
||||||
listVersions.tags contains V1R4C5
|
|
||||||
memory search remains disabled
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hard Stop
|
|
||||||
|
|
||||||
Do not create V1R4C6 tag.
|
|
||||||
Do not promote V1R4C6 baseline.
|
|
||||||
Do not deploy V1R4C6.
|
|
||||||
Do not enable memory search.
|
|
||||||
|
|
||||||
Proceed only after baseline visibility is reconciled.
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# V1R4C6 Context Isolation Recovery and Resume Decision
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
Status: approved to resume merge/deploy/verification; stop before tag/promotion
|
|
||||||
|
|
||||||
## Recovery Evidence
|
|
||||||
|
|
||||||
The unrelated OpenClaw/WeChat report was traced to notification/task isolation weaknesses rather than GitOps baseline corruption.
|
|
||||||
|
|
||||||
Actions completed:
|
|
||||||
|
|
||||||
1. The legacy `AIOS每日自治报告` cron was disabled and retained for forensic review.
|
|
||||||
2. The expired `2026-07-06-reminder` cron was archived and deleted after confirming no pending dependency.
|
|
||||||
3. Pending notification queue depth was verified as zero.
|
|
||||||
4. Unknown/unscoped queued message count was verified as zero.
|
|
||||||
5. A scoped V1R4C6 WeChat test notification was sent successfully.
|
|
||||||
|
|
||||||
The test notification included:
|
|
||||||
|
|
||||||
```text
|
|
||||||
task_id = V1R4C6-integration
|
|
||||||
release_id = V1R4C6
|
|
||||||
branch = release/v1r4c6-admin-search-agent-integration-20260711
|
|
||||||
baseline = V1R4C5
|
|
||||||
correlation_id = 70308f5e243ab81d
|
|
||||||
```
|
|
||||||
|
|
||||||
No recipient identifier, token, cookie, or unrelated message content is stored in this release record.
|
|
||||||
|
|
||||||
## Resume Decision
|
|
||||||
|
|
||||||
V1R4C6 may resume:
|
|
||||||
|
|
||||||
```text
|
|
||||||
merge
|
|
||||||
component tests
|
|
||||||
security/isolation tests
|
|
||||||
deployment
|
|
||||||
production verification
|
|
||||||
release evidence updates
|
|
||||||
```
|
|
||||||
|
|
||||||
V1R4C6 must still stop before:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Git tag creation
|
|
||||||
baseline promotion
|
|
||||||
memory search activation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Required Execution Context
|
|
||||||
|
|
||||||
Before every OpenClaw execution or WeChat notification, assert:
|
|
||||||
|
|
||||||
```text
|
|
||||||
task_id = V1R4C6-integration
|
|
||||||
release_id = V1R4C6
|
|
||||||
branch = release/v1r4c6-admin-search-agent-integration-20260711
|
|
||||||
baseline = V1R4C5
|
|
||||||
memory_search_enabled = false
|
|
||||||
```
|
|
||||||
|
|
||||||
Any mismatch re-enters safe mode and blocks execution/notification.
|
|
||||||
|
|
||||||
## Hard Constraints
|
|
||||||
|
|
||||||
```text
|
|
||||||
no force-push
|
|
||||||
no Gitea DB mutation
|
|
||||||
no V1R4C5 tag movement
|
|
||||||
no global memory/vector index
|
|
||||||
no cross-user/project/session notification or task data
|
|
||||||
```
|
|
||||||
|
|
||||||
## Owner Gate
|
|
||||||
|
|
||||||
After Gate A, Gate B, Gate C, deployment verification, runtime acceptance, and Gitea sentinel checks pass, OpenClaw must request explicit owner approval through the scoped WeChat channel for both:
|
|
||||||
|
|
||||||
1. Create V1R4C6 Git tag.
|
|
||||||
2. Promote active baseline from V1R4C5 to V1R4C6.
|
|
||||||
|
|
||||||
Ambiguous acknowledgements do not count as approval.
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# V1R4C6 Integration Merge Matrix
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
Status: integration-ready
|
|
||||||
|
|
||||||
Base baseline: V1R4C5
|
|
||||||
|
|
||||||
Base baseline git: `a32c76a3c1baa49562e53fb8adce10a5ed0e6704`
|
|
||||||
|
|
||||||
## Release Objective
|
|
||||||
|
|
||||||
Deliver three capability groups in one controlled release train:
|
|
||||||
|
|
||||||
1. Admin Workbench and Portal AIOS read-only rollout.
|
|
||||||
2. SR-03 Search Gateway real provider binding.
|
|
||||||
3. Agent Platform Integration.
|
|
||||||
|
|
||||||
This is a bundled release train, not an uncontrolled bulk merge. Each group has an independent acceptance gate and rollback switch.
|
|
||||||
|
|
||||||
## Group A — Admin Workbench and Portal AIOS
|
|
||||||
|
|
||||||
| Branch | Capability | Decision |
|
|
||||||
|---|---|---|
|
|
||||||
| `feature/v1r4c4-sr04-admin-workbench-visibility-20260711` | Admin workbench task progress and process visibility | integrate |
|
|
||||||
| `feature/v1r4c4-sr04-portal-aios-visibility-20260710` | Portal AIOS route/menu visibility | integrate |
|
|
||||||
| `patch/user-portal-me-route-diagnostics-20260709` | `/portal/me/aios/manifest`, dashboard and health route contract | integrate after route review |
|
|
||||||
| `patch/user-portal-rbac-ops-dashboard` | Admin/group-admin RBAC dashboard | integrate after RBAC review |
|
|
||||||
| `patch/agent-observability-webui` | Agent observability UI | integrate read-only |
|
|
||||||
| `patch/web-ui-complete-gitops-dashboard-20260706` | GitOps dashboard UI | integrate only if not superseded by the newer workbench implementation |
|
|
||||||
|
|
||||||
### Required user-visible result
|
|
||||||
|
|
||||||
Administrators can see:
|
|
||||||
|
|
||||||
- task progress;
|
|
||||||
- task/flow/process diagram entry;
|
|
||||||
- system health;
|
|
||||||
- authorized agent observability;
|
|
||||||
- explicit empty/error states.
|
|
||||||
|
|
||||||
### Security constraints
|
|
||||||
|
|
||||||
- Read-only first.
|
|
||||||
- RBAC is enforced server-side and client-side.
|
|
||||||
- No cross-user, cross-project, cross-session, or cross-tenant data exposure.
|
|
||||||
- Owner-only modules remain hidden from non-owner administrators.
|
|
||||||
|
|
||||||
## Group B — SR-03 Search Gateway Real Provider Binding
|
|
||||||
|
|
||||||
| Branch | Capability | Decision |
|
|
||||||
|---|---|---|
|
|
||||||
| `feature/v1r4c4-sr02-search-gateway-resilience-20260710` | Gateway resilience and safe fallback | integrate |
|
|
||||||
| `patch/search-gateway-governed-general-capability` | Governed provider binding and policy surface | integrate after scope review |
|
|
||||||
| `patch/gitops-change-archive-audit` | Change/archive audit evidence | integrate or retain as supporting audit module |
|
|
||||||
|
|
||||||
### Required result
|
|
||||||
|
|
||||||
- Real provider binding works.
|
|
||||||
- Query scope is enforced before ranking.
|
|
||||||
- Accepted records include provenance.
|
|
||||||
- Provider failure falls back to `text-only`, `no-memory`, or `scoped-only`.
|
|
||||||
- Global memory/vector indexes are not used.
|
|
||||||
|
|
||||||
### Security constraints
|
|
||||||
|
|
||||||
```text
|
|
||||||
AIOS_MEMORY_ENABLED=false
|
|
||||||
```
|
|
||||||
|
|
||||||
Search Gateway provider binding must not implicitly enable memory search.
|
|
||||||
|
|
||||||
## Group C — Agent Platform Integration
|
|
||||||
|
|
||||||
| Branch | Capability | Decision |
|
|
||||||
|---|---|---|
|
|
||||||
| `patch/agent-event-bus-unified` | Unified scoped agent event bus | integrate |
|
|
||||||
| `patch/agent-gpt-message-plane` | Scoped Agent/GPT message plane | integrate |
|
|
||||||
| `patch/autonomous-controller-dispatch-20260709` | Autonomous controller dispatch | integrate behind safe-mode gate |
|
|
||||||
| `patch/self-evolution-agent-orchestration-policy` | Agent orchestration and governance policy | integrate |
|
|
||||||
| `patch/aios-autonomous-orchestrator-architecture-review-v1` | Architecture record | integrate as documentation or mark superseded after review |
|
|
||||||
|
|
||||||
### Required result
|
|
||||||
|
|
||||||
- Agent event and message envelopes include user/project/session scope.
|
|
||||||
- Controller dispatch stops when autonomous safe mode is active.
|
|
||||||
- Consequential actions remain owner-gated.
|
|
||||||
- Observability is available before autonomous expansion.
|
|
||||||
|
|
||||||
## Explicitly Excluded
|
|
||||||
|
|
||||||
Do not merge or activate in V1R4C6:
|
|
||||||
|
|
||||||
- memory search activation;
|
|
||||||
- global vector index;
|
|
||||||
- cross-session/global recall;
|
|
||||||
- unscoped search or message delivery;
|
|
||||||
- old test/smoke/auth-only branches;
|
|
||||||
- old V1R2/V1R3 promotion requests;
|
|
||||||
- force-push reconciliation;
|
|
||||||
- Gitea database mutation.
|
|
||||||
|
|
||||||
## Merge and Deployment Order
|
|
||||||
|
|
||||||
```text
|
|
||||||
1. Verify V1R4C5 baseline and bare-repo configuration.
|
|
||||||
2. Integrate Group A read-only workbench/portal surface.
|
|
||||||
3. Run Group A RBAC and visibility acceptance.
|
|
||||||
4. Integrate Group B provider binding under isolation gate.
|
|
||||||
5. Run Group B scope/provenance/fallback acceptance.
|
|
||||||
6. Integrate Group C event/message/dispatch capabilities.
|
|
||||||
7. Run Group C scope/safe-mode/owner-gate acceptance.
|
|
||||||
8. Run combined runtime acceptance.
|
|
||||||
9. Prepare V1R4C6 tag and promotion request.
|
|
||||||
10. Stop for owner confirmation before tag/promotion.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Combined Acceptance
|
|
||||||
|
|
||||||
Required:
|
|
||||||
|
|
||||||
```text
|
|
||||||
getRuntimeStatus -> active
|
|
||||||
getBaseline -> V1R4C5 before promotion
|
|
||||||
createBranch -> committed and synced
|
|
||||||
createPatch -> committed and synced
|
|
||||||
Gitea branch sentinel -> healthy
|
|
||||||
memory search -> disabled
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rollback Switches
|
|
||||||
|
|
||||||
- Group A: disable new workbench routes/menu entries.
|
|
||||||
- Group B: disable real provider binding and use safe non-memory fallback.
|
|
||||||
- Group C: disable expanded dispatch and keep event/message plane observational.
|
|
||||||
- Memory search remains disabled in all rollback modes.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# V1R4C6 Release Acceptance Checklist
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
## Gate A — Admin Workbench / Portal AIOS
|
|
||||||
|
|
||||||
| Check | Status | Notes |
|
|
||||||
|--------------------------------|---------|--------------------------------|
|
|
||||||
| Admin workbench RBAC read-only | NOT RUN | |
|
|
||||||
| Portal AIOS manifest route | NOT RUN | |
|
|
||||||
| Portal AIOS dashboard route | NOT RUN | |
|
|
||||||
| Flow/process diagram renders | NOT RUN | |
|
|
||||||
| Portal AIOS health route | NOT RUN | |
|
|
||||||
|
|
||||||
## Gate B — SR-03 Search Gateway
|
|
||||||
|
|
||||||
| Check | Status | Notes |
|
|
||||||
|------------------------------------|---------|--------------------------------|
|
|
||||||
| Provider binding correctness | NOT RUN | |
|
|
||||||
| Scope-before-ranking validation | NOT RUN | |
|
|
||||||
| Provenance correctness | NOT RUN | |
|
|
||||||
| Fallback to disabled provider | NOT RUN | |
|
|
||||||
|
|
||||||
## Gate C — Agent Platform
|
|
||||||
|
|
||||||
| Check | Status | Notes |
|
|
||||||
|------------------------------------|---------|--------------------------------|
|
|
||||||
| Event scope isolation | NOT RUN | |
|
|
||||||
| Message plane scope isolation | NOT RUN | |
|
|
||||||
| Dispatch safe-mode respects control | NOT RUN | |
|
|
||||||
| Owner gate for consequential ops | NOT RUN | |
|
|
||||||
|
|
||||||
## Final Deployment Gates
|
|
||||||
|
|
||||||
| Check | Status | Notes |
|
|
||||||
|---------------------------|-----------|--------------------------------|
|
|
||||||
| Tag V1R4C6 | NO | Requires owner approval |
|
|
||||||
| Baseline promotion | NO | Requires owner approval |
|
|
||||||
| Memory search enabled | NO | Requires future owner approval |
|
|
||||||
|
|
||||||
## Active Constraints
|
|
||||||
|
|
||||||
- AIOS_MEMORY_ENABLED: false
|
|
||||||
- Force-push: forbidden
|
|
||||||
- Gitea DB mutation: owner approval required
|
|
||||||
- Memory search: disabled (future owner approval required for enable)
|
|
||||||
|
|
||||||
## Artifact
|
|
||||||
|
|
||||||
- **PR #6 merge commit**: `ae49bf0fa35b0f599a96a4d12644576447f39cec`
|
|
||||||
- **Source baseline**: V1R4C5 (commit a32c76a3c1baa49562e53fb8adce10a5ed0e6704)
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# V1R4C6 Release Gate Status
|
|
||||||
|
|
||||||
Date: 2026-07-11
|
|
||||||
|
|
||||||
Status: integration pending — release-candidate control artifacts merged, engineering gates not yet executed
|
|
||||||
|
|
||||||
## Completed
|
|
||||||
|
|
||||||
- V1R4C5 baseline verified as active base.
|
|
||||||
- GitOps API and public API return:
|
|
||||||
- current_baseline: V1R4C5
|
|
||||||
- previous_baseline: V1R4C3
|
|
||||||
- git: a32c76a3c1baa49562e53fb8adce10a5ed0e6704
|
|
||||||
- GitOps bare repository path corrected to active `aios.git`.
|
|
||||||
- V1R4C6 merge matrix created.
|
|
||||||
- V1R4C6 acceptance checklist created.
|
|
||||||
- V1R4C6 acceptance evidence created.
|
|
||||||
- PR #6: control artifacts merge — PASS
|
|
||||||
|
|
||||||
## Result Summary
|
|
||||||
|
|
||||||
| Item | Status |
|
|
||||||
|-------------------------|-----------|
|
|
||||||
| control artifacts merge | PASS |
|
|
||||||
| Gate A | NOT RUN |
|
|
||||||
| Gate B | NOT RUN |
|
|
||||||
| Gate C | NOT RUN |
|
|
||||||
| tests | NOT RUN |
|
|
||||||
| artifact | NOT CREATED |
|
|
||||||
| deployment | NOT RUN |
|
|
||||||
| production verification | NOT RUN |
|
|
||||||
| tag created | NO |
|
|
||||||
| baseline promoted | NO |
|
|
||||||
| memory search enabled | NO |
|
|
||||||
| **overall** | **BLOCKED / INTEGRATION PENDING** |
|
|
||||||
|
|
||||||
## Verified Artifact
|
|
||||||
|
|
||||||
- **PR #6 merge commit**: `ae49bf0fa35b0f599a96a4d12644576447f39cec`
|
|
||||||
- **Current baseline**: V1R4C5
|
|
||||||
- **V1R4C6 tag**: absent
|
|
||||||
- **Memory search**: disabled
|
|
||||||
- **Force-push**: disabled
|
|
||||||
|
|
||||||
## Remaining Gates (not yet executed)
|
|
||||||
|
|
||||||
### Gate A — Admin Workbench / Portal AIOS
|
|
||||||
|
|
||||||
- merge candidate review;
|
|
||||||
- RBAC acceptance;
|
|
||||||
- task progress visibility;
|
|
||||||
- flow diagram visibility;
|
|
||||||
- health/observability validation.
|
|
||||||
|
|
||||||
### Gate B — SR-03 Search Gateway
|
|
||||||
|
|
||||||
- provider binding validation;
|
|
||||||
- scope-before-ranking validation;
|
|
||||||
- provenance validation;
|
|
||||||
- fallback validation.
|
|
||||||
|
|
||||||
### Gate C — Agent Platform
|
|
||||||
|
|
||||||
- event scope validation;
|
|
||||||
- message plane scope validation;
|
|
||||||
- dispatch safe-mode validation;
|
|
||||||
- owner gate validation.
|
|
||||||
|
|
||||||
## Release Stop Point
|
|
||||||
|
|
||||||
After Gates A/B/C pass:
|
|
||||||
|
|
||||||
```text
|
|
||||||
prepare V1R4C6 tag
|
|
||||||
↓
|
|
||||||
owner confirmation required
|
|
||||||
↓
|
|
||||||
tag creation
|
|
||||||
↓
|
|
||||||
baseline promotion
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hard Constraints
|
|
||||||
|
|
||||||
Remain:
|
|
||||||
|
|
||||||
```text
|
|
||||||
AIOS_MEMORY_ENABLED=false
|
|
||||||
memory search disabled
|
|
||||||
force-push forbidden
|
|
||||||
gitea DB mutation requires owner approval
|
|
||||||
```
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# Weixin Quoted Text Fail-Closed Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Resolve explicit quoted text safely and fail closed without a model call when Weixin omits the quoted body.
|
|
||||||
|
|
||||||
**Architecture:** Add pure JS/TS reference-text helpers and one process hook guard to the existing patcher. Keep the patch surface limited to generated `process-message.ts/js`; do not introduce a persistent cache or modify send/monitor/API modules.
|
|
||||||
|
|
||||||
**Tech Stack:** Python patch generator, TypeScript/JavaScript, Node test harness, pytest.
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Sub-agents use `gpt-5.3-codex-spark`.
|
|
||||||
- No force push, tag, baseline promotion, credential persistence, recent-message fallback, or approximate text matching.
|
|
||||||
- Existing confirmed image and ordinary text behavior must remain unchanged.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Add Quoted Text Resolver With TDD
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `tests/test_weixin_image_followup_intent.py`
|
|
||||||
- Modify: `fixes/patch_openclaw_weixin_hermes.py`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: `extractHermesReferencedText(items)` and `resolveHermesQuotedTextContext(items, currentText)` in TS and JS helper templates.
|
|
||||||
|
|
||||||
- [ ] Add a Node-backed failing test with `ref_msg.message_item.type=1` and explicit `text_item.text="被引用的正文"`; assert `reason="quoted_text_resolved"`, `handled=false`, and a prompt containing only delimited quoted/current text.
|
|
||||||
- [ ] Add a Node-backed failing test with `type=0`, quoted ID, no text, and no confirmed image; assert `reason="quoted_text_unavailable"`, `handled=true`, and exact deterministic `replyText`.
|
|
||||||
- [ ] Run `pytest -q tests/test_weixin_image_followup_intent.py -k 'quoted_text'` and record expected RED because the resolver is absent.
|
|
||||||
- [ ] Implement the smallest recursive explicit-text extractor. Accept only known referenced text fields; cap each extracted string at 4000 characters and reject blank values.
|
|
||||||
- [ ] Implement the resolver in both TS and JS templates. Do not read session history, image latest cache, timestamps, or metadata values.
|
|
||||||
- [ ] Re-run the focused tests and record GREEN.
|
|
||||||
- [ ] Commit with `fix(weixin): fail closed on unavailable quoted text`.
|
|
||||||
|
|
||||||
### Task 2: Guard The Real Process Hook
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `tests/test_weixin_image_followup_intent.py`
|
|
||||||
- Modify: `fixes/patch_openclaw_weixin_hermes.py`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `resolveHermesQuotedTextContext(items, currentText)`.
|
|
||||||
- Produces: process hook behavior that replies deterministically or replaces only the ordinary text prompt.
|
|
||||||
|
|
||||||
- [ ] Add a failing JS hook test that stubs the ordinary model dispatch and reply function. For type 0 without text, assert reply called once and model called zero times.
|
|
||||||
- [ ] Add a failing JS hook test for explicit quoted text. Assert model called once with delimited context and no message metadata.
|
|
||||||
- [ ] Add regression assertions for plain text, current image, and exact quoted image.
|
|
||||||
- [ ] Run focused tests and record RED for the missing guard.
|
|
||||||
- [ ] Insert the resolver after reference/image evidence extraction and before ordinary model dispatch. Confirmed image references must bypass the text fail-closed branch.
|
|
||||||
- [ ] Emit structured reason-only diagnostics without message bodies.
|
|
||||||
- [ ] Run focused tests and record GREEN.
|
|
||||||
- [ ] Commit with `fix(weixin): guard quoted text model dispatch`.
|
|
||||||
|
|
||||||
### Task 3: Idempotence, Review, And Release
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `docs/incidents/WEIXIN_TEXT_IMAGE_CACHE_ISOLATION_20260711.md`
|
|
||||||
|
|
||||||
- [ ] Run `pytest -q tests/test_weixin_image_followup_intent.py` and require all tests pass.
|
|
||||||
- [ ] Run `python3 -m py_compile fixes/patch_openclaw_weixin_hermes.py tests/test_weixin_image_followup_intent.py`.
|
|
||||||
- [ ] Run `git diff --check`.
|
|
||||||
- [ ] Apply `--process-only` twice to a scratch production-plugin copy and require identical second-run hashes; require slash/channel files unchanged.
|
|
||||||
- [ ] Obtain independent spec and code review.
|
|
||||||
- [ ] Update the incident document with RED/GREEN evidence, main revision, production hashes, rollback directory, and owner live acceptance state.
|
|
||||||
- [ ] Merge by non-force fast-forward from a clean main-based branch, deploy only the two generated process-message files, restart only `openclaw-gateway.service`, and verify service, port 18789, four accounts, zero post-start errors, and unchanged image cache.
|
|
||||||
- [ ] Owner live acceptance: quote a type 0 text message and verify deterministic no-body response; quote a payload that includes explicit text and verify grounded explanation.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Weixin Quoted Text Fail-Closed Design
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Prevent quoted-text questions from being answered with invented message metadata when Weixin does not provide the quoted body. Preserve normal text and confirmed image behavior.
|
|
||||||
|
|
||||||
## Evidence
|
|
||||||
|
|
||||||
- A quoted text event can arrive as `ref_msg.message_item.type=0` with only `msg_id` and timestamps.
|
|
||||||
- Session transcripts retain text but not the original Weixin numeric message ID.
|
|
||||||
- Outbound `clientId` values are not the same format as quoted numeric `msg_id` values, and the send API does not expose a server message ID.
|
|
||||||
- Therefore recent-message, timestamp, session, and outbound-client-ID matching are not exact evidence and must not be used.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
Use an evidence-only resolver in `process-message`:
|
|
||||||
|
|
||||||
1. If reference payload contains an explicit text item, extract that text and build a bounded quoted-text context for the ordinary text model path.
|
|
||||||
2. If reference context exists but has no explicit text and is not a confirmed image, return a deterministic response: `无法读取被引用文字,请复制原文后再提问。`
|
|
||||||
3. The unresolved branch is handled before ordinary model dispatch and before Hermes image dispatch.
|
|
||||||
4. Do not infer quoted text from metadata, recent messages, session order, timestamps, or approximate IDs.
|
|
||||||
|
|
||||||
No persistent text cache is introduced in this change. Outbound ID aliasing remains a separate observation task because an exact mapping has not been proved.
|
|
||||||
|
|
||||||
## Interfaces
|
|
||||||
|
|
||||||
- `extractHermesReferencedText(items): string` returns only explicit referenced text, otherwise an empty string.
|
|
||||||
- `resolveHermesQuotedTextContext(items, currentText)` returns one of:
|
|
||||||
- `{ referenced: false, handled: false, prompt: currentText, reason: "none" }`
|
|
||||||
- `{ referenced: true, handled: false, prompt, reason: "quoted_text_resolved" }`
|
|
||||||
- `{ referenced: true, handled: true, replyText: "无法读取被引用文字,请复制原文后再提问。", reason: "quoted_text_unavailable" }`
|
|
||||||
- Confirmed referenced images bypass this text resolver and keep the existing exact image path.
|
|
||||||
|
|
||||||
## Privacy And Logging
|
|
||||||
|
|
||||||
- Do not log quoted or current text.
|
|
||||||
- Log only `reason`, account ID, a one-way bounded hash of quoted ID when available, and boolean evidence flags.
|
|
||||||
- Do not persist tokens, credentials, full payloads, or message text.
|
|
||||||
|
|
||||||
## Acceptance
|
|
||||||
|
|
||||||
- Explicit quoted text is supplied to the ordinary text model with a clear delimiter.
|
|
||||||
- Type 0 reference without text returns the deterministic Chinese response and does not call the model.
|
|
||||||
- Plain text, current image, and exact quoted image behavior remain unchanged.
|
|
||||||
- Generated TS and JS remain byte-idempotent under `--process-only`.
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
apiVersion: aios.gitops/v1
|
|
||||||
kind: ExecutionOrder
|
|
||||||
metadata:
|
|
||||||
name: openclaw-v1r4c6-merge-tests-deploy
|
|
||||||
date: 2026-07-11
|
|
||||||
owner_approval:
|
|
||||||
branch: request/openclaw-v1r4c6-merge-deploy-20260711
|
|
||||||
file: approvals/openclaw-v1r4c6-merge-deploy-owner-approval.yaml
|
|
||||||
commit: 4653f92
|
|
||||||
spec:
|
|
||||||
objective: >-
|
|
||||||
Integrate the approved V1R4C6 Admin Workbench, Search Gateway, and Agent Platform
|
|
||||||
changes into a clean candidate based on the latest main, execute all mandatory gates,
|
|
||||||
merge to main only after success, then deploy and verify production.
|
|
||||||
confirmation_policy: no_additional_confirmation_required
|
|
||||||
source:
|
|
||||||
primary_branch: release/v1r4c6-admin-search-agent-integration-20260711
|
|
||||||
supporting_domains:
|
|
||||||
- admin-workbench-portal-aios
|
|
||||||
- search-gateway-governance
|
|
||||||
- agent-platform-integration
|
|
||||||
target:
|
|
||||||
integration_branch: integration/v1r4c6-merge-deploy-20260711-01
|
|
||||||
final_branch: main
|
|
||||||
release_version: V1R4C6
|
|
||||||
execution:
|
|
||||||
- id: preflight
|
|
||||||
actions:
|
|
||||||
- fetch_all_refs_and_tags
|
|
||||||
- verify_workspace_clean
|
|
||||||
- reset_integration_candidate_to_latest_main
|
|
||||||
- verify_baseline_is_V1R4C5
|
|
||||||
- verify_memory_search_enabled_is_false
|
|
||||||
- verify_V1R4C5_tag_unchanged
|
|
||||||
- record_main_head_and_source_head
|
|
||||||
stop_conditions:
|
|
||||||
- dirty_workspace
|
|
||||||
- missing_source_branch
|
|
||||||
- baseline_not_V1R4C5
|
|
||||||
- protected_setting_changed
|
|
||||||
- id: semantic-diff-review
|
|
||||||
actions:
|
|
||||||
- compute_three_dot_diff_from_main_to_primary_branch
|
|
||||||
- classify_changes_by_admin_search_agent_domains
|
|
||||||
- detect_duplicate_control_artifacts_already_merged_by_PR_6
|
|
||||||
- preserve_PR_7_NOT_RUN_status_until_tests_execute
|
|
||||||
- reject_reversion_of_V1R4C5_baseline_metadata
|
|
||||||
- reject_enablement_of_memory_search
|
|
||||||
output:
|
|
||||||
- execution/evidence/v1r4c6-semantic-diff-report.md
|
|
||||||
- id: merge-candidate
|
|
||||||
actions:
|
|
||||||
- merge_primary_branch_into_integration_candidate_without_fast_forward
|
|
||||||
- resolve_text_conflicts_by_preserving_latest_main_control_metadata
|
|
||||||
- preserve_source_business_functionality_when_not_conflicting_with_safety_controls
|
|
||||||
- commit_resolved_candidate
|
|
||||||
forbidden:
|
|
||||||
- force_push
|
|
||||||
- direct_commit_to_main
|
|
||||||
- bypass_branch_protection
|
|
||||||
- bypass_owner_gate
|
|
||||||
- weaken_rbac_or_scope_isolation
|
|
||||||
- mark_unexecuted_tests_as_passed
|
|
||||||
- id: gate-a-admin
|
|
||||||
required_checks:
|
|
||||||
- admin_workbench_routes_and_assets_load
|
|
||||||
- portal_aios_manifest_dashboard_health_endpoints
|
|
||||||
- server_side_and_frontend_rbac
|
|
||||||
- empty_error_and_loading_states
|
|
||||||
- tenant_user_project_session_isolation
|
|
||||||
- id: gate-b-search
|
|
||||||
required_checks:
|
|
||||||
- real_provider_binding_or_explicit_safe_fallback
|
|
||||||
- timeout_limited_retry_and_safe_fallback
|
|
||||||
- scope_filter_before_ranking
|
|
||||||
- provenance_in_results
|
|
||||||
- provider_and_fallback_audit
|
|
||||||
- unauthorized_results_never_enter_ranking_or_summary
|
|
||||||
- memory_search_remains_disabled
|
|
||||||
- id: gate-c-agent
|
|
||||||
required_checks:
|
|
||||||
- unified_agent_event_bus
|
|
||||||
- scoped_agent_gpt_message_plane
|
|
||||||
- autonomous_controller_dispatch
|
|
||||||
- safe_mode_stops_dispatch_and_change
|
|
||||||
- high_risk_actions_remain_owner_gated
|
|
||||||
- duplicate_dispatch_is_idempotent_or_rejected
|
|
||||||
- queue_failure_retry_observability
|
|
||||||
- id: runtime-and-regression
|
|
||||||
actions:
|
|
||||||
- run_getRuntimeStatus
|
|
||||||
- run_getBaseline
|
|
||||||
- run_createBranch_smoke_test
|
|
||||||
- run_createPatch_smoke_test
|
|
||||||
- run_full_regression_suite
|
|
||||||
- run_deployment_preflight_health_checks
|
|
||||||
expected:
|
|
||||||
reads: "200 OK with service, status, workspace_branch"
|
|
||||||
writes: "200 OK with branch, commit, rev, sync_status"
|
|
||||||
- id: promote-main
|
|
||||||
conditions:
|
|
||||||
- gate_a_passed
|
|
||||||
- gate_b_passed
|
|
||||||
- gate_c_passed
|
|
||||||
- runtime_acceptance_passed
|
|
||||||
- regression_passed
|
|
||||||
- deployment_preflight_passed
|
|
||||||
actions:
|
|
||||||
- open_or_update_merge_request_from_integration_candidate_to_main
|
|
||||||
- merge_without_rewriting_history
|
|
||||||
- record_new_main_head
|
|
||||||
- update_versions_V1R4C6_json_to_release_ready_or_active_baseline_as_policy_requires
|
|
||||||
- update_baseline_current_json_only_after_successful_promotion
|
|
||||||
- create_V1R4C6_tag_according_to_repository_tag_semantics
|
|
||||||
- id: deploy-and-verify
|
|
||||||
actions:
|
|
||||||
- trigger_direct_deployment_from_new_main
|
|
||||||
- verify_service_health
|
|
||||||
- verify_admin_search_agent_smoke_paths
|
|
||||||
- verify_runtime_status_and_baseline_after_deploy
|
|
||||||
- verify_rollback_artifact_and_previous_baseline_are_available
|
|
||||||
- publish_production_verification_report
|
|
||||||
failure_policy:
|
|
||||||
mode: fail_closed
|
|
||||||
on_any_failure:
|
|
||||||
- stop_pipeline
|
|
||||||
- do_not_update_main
|
|
||||||
- do_not_create_or_move_release_tag
|
|
||||||
- do_not_deploy
|
|
||||||
- preserve_integration_branch_for_forensics
|
|
||||||
- commit_failure_evidence_and_exact_blocker
|
|
||||||
reporting:
|
|
||||||
required_files:
|
|
||||||
- execution/evidence/v1r4c6-semantic-diff-report.md
|
|
||||||
- execution/evidence/gate-a-admin-result.md
|
|
||||||
- execution/evidence/gate-b-search-result.md
|
|
||||||
- execution/evidence/gate-c-agent-result.md
|
|
||||||
- execution/evidence/runtime-acceptance-result.md
|
|
||||||
- execution/evidence/regression-result.md
|
|
||||||
- execution/evidence/deployment-result.md
|
|
||||||
- execution/evidence/production-verification-result.md
|
|
||||||
final_summary_fields:
|
|
||||||
- source_head
|
|
||||||
- integration_head
|
|
||||||
- main_head_before
|
|
||||||
- main_head_after
|
|
||||||
- merge_commit
|
|
||||||
- release_tag
|
|
||||||
- deployment_id
|
|
||||||
- gate_results
|
|
||||||
- production_health
|
|
||||||
- rollback_ready
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
TS_HELPER = '''// AIOS Weixin lossless getUpdates JSON begin
|
|
||||||
const WEIXIN_EXACT_ID_FIELDS = new Set(["message_id", "msg_id", "root_id", "parent_id", "server_msg_id", "new_msg_id"]);
|
|
||||||
function parseWeixinGetUpdatesJson(rawText: string): GetUpdatesResp {
|
|
||||||
return JSON.parse(rawText, (key: string, value: unknown, context?: { source?: string }) => {
|
|
||||||
if (!WEIXIN_EXACT_ID_FIELDS.has(key) || value == null || typeof value === "string") return value;
|
|
||||||
if (typeof value !== "number") throw new Error(`invalid ${key} type in getUpdates response`);
|
|
||||||
const source = context?.source;
|
|
||||||
if (!source || !/^(?:0|[1-9]\\d*)$/.test(source)) throw new Error(`unsafe numeric ${key} in getUpdates response`);
|
|
||||||
return source;
|
|
||||||
}) as GetUpdatesResp;
|
|
||||||
}
|
|
||||||
// AIOS Weixin lossless getUpdates JSON end'''
|
|
||||||
|
|
||||||
JS_HELPER = '''// AIOS Weixin lossless getUpdates JSON begin
|
|
||||||
const WEIXIN_EXACT_ID_FIELDS = new Set(["message_id", "msg_id", "root_id", "parent_id", "server_msg_id", "new_msg_id"]);
|
|
||||||
function parseWeixinGetUpdatesJson(rawText) {
|
|
||||||
return JSON.parse(rawText, (key, value, context) => {
|
|
||||||
if (!WEIXIN_EXACT_ID_FIELDS.has(key) || value == null || typeof value === "string")
|
|
||||||
return value;
|
|
||||||
if (typeof value !== "number")
|
|
||||||
throw new Error(`invalid ${key} type in getUpdates response`);
|
|
||||||
const source = context?.source;
|
|
||||||
if (!source || !/^(?:0|[1-9]\\d*)$/.test(source))
|
|
||||||
throw new Error(`unsafe numeric ${key} in getUpdates response`);
|
|
||||||
return source;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// AIOS Weixin lossless getUpdates JSON end'''
|
|
||||||
|
|
||||||
|
|
||||||
def insert_once(text: str, marker: str, addition: str) -> str:
|
|
||||||
if addition in text:
|
|
||||||
return text
|
|
||||||
if marker not in text:
|
|
||||||
raise RuntimeError(f'marker not found: {marker}')
|
|
||||||
return text.replace(marker, marker + '\n' + addition, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def patch_api_ts(text: str) -> str:
|
|
||||||
text = insert_once(text, 'import { redactBody, redactUrl } from "../util/redact.js";', '\n' + TS_HELPER)
|
|
||||||
old = 'const resp: GetUpdatesResp = JSON.parse(rawText);'
|
|
||||||
new = 'const resp: GetUpdatesResp = parseWeixinGetUpdatesJson(rawText);'
|
|
||||||
if new not in text:
|
|
||||||
if old not in text:
|
|
||||||
raise RuntimeError('TypeScript getUpdates parser marker not found')
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def patch_api_js(text: str) -> str:
|
|
||||||
text = insert_once(text, 'import { redactBody, redactUrl } from "../util/redact.js";', '\n' + JS_HELPER)
|
|
||||||
old = 'const resp = JSON.parse(rawText);'
|
|
||||||
new = 'const resp = parseWeixinGetUpdatesJson(rawText);'
|
|
||||||
if new not in text:
|
|
||||||
if old not in text:
|
|
||||||
raise RuntimeError('JavaScript getUpdates parser marker not found')
|
|
||||||
text = text.replace(old, new, 1)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def patch_types_ts(text: str) -> str:
|
|
||||||
new = 'message_id?: string | number;'
|
|
||||||
if new in text:
|
|
||||||
return text
|
|
||||||
old = 'message_id?: number;'
|
|
||||||
if old not in text:
|
|
||||||
raise RuntimeError('message_id type marker not found')
|
|
||||||
return text.replace(old, new, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def write_if_changed(path: Path, content: str) -> bool:
|
|
||||||
original = path.read_text(encoding='utf-8')
|
|
||||||
if original == content:
|
|
||||||
return False
|
|
||||||
path.write_text(content, encoding='utf-8')
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def apply(plugin_root: Path) -> dict[str, bool]:
|
|
||||||
paths = {
|
|
||||||
'src/api/api.ts': patch_api_ts,
|
|
||||||
'dist/src/api/api.js': patch_api_js,
|
|
||||||
'src/api/types.ts': patch_types_ts,
|
|
||||||
}
|
|
||||||
changed: dict[str, bool] = {}
|
|
||||||
for relative, transform in paths.items():
|
|
||||||
path = plugin_root / relative
|
|
||||||
if not path.exists():
|
|
||||||
raise RuntimeError(f'missing plugin file: {path}')
|
|
||||||
original = path.read_text(encoding='utf-8')
|
|
||||||
changed[relative] = write_if_changed(path, transform(original))
|
|
||||||
return changed
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('plugin_root', type=Path)
|
|
||||||
args = parser.parse_args()
|
|
||||||
changed = apply(args.plugin_root.resolve())
|
|
||||||
print(json.dumps({'plugin_root': str(args.plugin_root.resolve()), 'changed': changed}, ensure_ascii=False))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
raise SystemExit(main())
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def replace_required(text: str, old: str, new: str, label: str) -> str:
|
|
||||||
if new in text:
|
|
||||||
return text
|
|
||||||
if old not in text:
|
|
||||||
raise RuntimeError(f'{label} marker not found')
|
|
||||||
return text.replace(old, new, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def patch_send_ts(text: str) -> str:
|
|
||||||
text = replace_required(
|
|
||||||
text,
|
|
||||||
'}): Promise<{ messageId: string }> {\n const { to, text, opts } = params;\n',
|
|
||||||
'}): Promise<{ messageId: string; createTimeMs: number }> {\n const { to, text, opts } = params;\n',
|
|
||||||
'TS send return type',
|
|
||||||
)
|
|
||||||
text = replace_required(
|
|
||||||
text,
|
|
||||||
' const clientId = generateClientId();\n const req = buildSendMessageReq({',
|
|
||||||
' const clientId = generateClientId();\n const createTimeMs = Date.now();\n const req = buildSendMessageReq({',
|
|
||||||
'TS create time',
|
|
||||||
)
|
|
||||||
text = replace_required(
|
|
||||||
text,
|
|
||||||
' });\n try {\n await sendMessageApi({',
|
|
||||||
' });\n if (!req.msg?.item_list?.[0]) throw new Error("sendMessageWeixin: text item missing");\n req.msg.item_list[0].create_time_ms = createTimeMs;\n try {\n await sendMessageApi({',
|
|
||||||
'TS request timestamp',
|
|
||||||
)
|
|
||||||
text = replace_required(
|
|
||||||
text,
|
|
||||||
' return { messageId: clientId };\n}\n\n/** Send a single structured MessageItem downstream. */',
|
|
||||||
' return { messageId: clientId, createTimeMs };\n}\n\n/** Send a single structured MessageItem downstream. */',
|
|
||||||
'TS send result',
|
|
||||||
)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def patch_send_js(text: str) -> str:
|
|
||||||
text = replace_required(
|
|
||||||
text,
|
|
||||||
' const clientId = generateClientId();\n const req = buildSendMessageReq({',
|
|
||||||
' const clientId = generateClientId();\n const createTimeMs = Date.now();\n const req = buildSendMessageReq({',
|
|
||||||
'JS create time',
|
|
||||||
)
|
|
||||||
text = replace_required(
|
|
||||||
text,
|
|
||||||
' });\n try {\n await sendMessageApi({',
|
|
||||||
' });\n if (!req.msg?.item_list?.[0])\n throw new Error("sendMessageWeixin: text item missing");\n req.msg.item_list[0].create_time_ms = createTimeMs;\n try {\n await sendMessageApi({',
|
|
||||||
'JS request timestamp',
|
|
||||||
)
|
|
||||||
text = replace_required(
|
|
||||||
text,
|
|
||||||
' return { messageId: clientId };\n}\n/** Send a single structured MessageItem downstream. */',
|
|
||||||
' return { messageId: clientId, createTimeMs };\n}\n/** Send a single structured MessageItem downstream. */',
|
|
||||||
'JS send result',
|
|
||||||
)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def patch_process_ts(text: str) -> str:
|
|
||||||
old = '''await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
}});'''
|
|
||||||
new = '''const sentText = await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
}});
|
|
||||||
rememberHermesOutboundTextMessage(deps.accountId, ctx.To, text, sentText.createTimeMs);'''
|
|
||||||
text = replace_required(text, old, new, 'TS fallback text send')
|
|
||||||
old = '''await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
}});'''
|
|
||||||
new = '''const sentText = await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
}});
|
|
||||||
rememberHermesOutboundTextMessage(deps.accountId, ctx.To, text, sentText.createTimeMs);'''
|
|
||||||
return replace_required(text, old, new, 'TS normal text send')
|
|
||||||
|
|
||||||
|
|
||||||
def patch_process_js(text: str) -> str:
|
|
||||||
old = '''await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
} });'''
|
|
||||||
new = '''const sentText = await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
} });
|
|
||||||
rememberHermesOutboundTextMessage(deps.accountId, ctx.To, text, sentText.createTimeMs);'''
|
|
||||||
text = replace_required(text, old, new, 'JS fallback text send')
|
|
||||||
old = '''await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
} });'''
|
|
||||||
new = '''const sentText = await sendMessageWeixin({ to: ctx.To, text, opts: {
|
|
||||||
baseUrl: deps.baseUrl,
|
|
||||||
token: deps.token,
|
|
||||||
contextToken,
|
|
||||||
runId,
|
|
||||||
} });
|
|
||||||
rememberHermesOutboundTextMessage(deps.accountId, ctx.To, text, sentText.createTimeMs);'''
|
|
||||||
return replace_required(text, old, new, 'JS normal text send')
|
|
||||||
|
|
||||||
|
|
||||||
def apply(root: Path) -> dict[str, bool]:
|
|
||||||
transforms = {
|
|
||||||
'src/messaging/send.ts': patch_send_ts,
|
|
||||||
'dist/src/messaging/send.js': patch_send_js,
|
|
||||||
'src/messaging/process-message.ts': patch_process_ts,
|
|
||||||
'dist/src/messaging/process-message.js': patch_process_js,
|
|
||||||
}
|
|
||||||
changed = {}
|
|
||||||
for relative, transform in transforms.items():
|
|
||||||
path = root / relative
|
|
||||||
original = path.read_text(encoding='utf-8')
|
|
||||||
updated = transform(original)
|
|
||||||
changed[relative] = updated != original
|
|
||||||
if updated != original:
|
|
||||||
path.write_text(updated, encoding='utf-8')
|
|
||||||
return changed
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('plugin_root', type=Path)
|
|
||||||
args = parser.parse_args()
|
|
||||||
root = args.plugin_root.resolve()
|
|
||||||
print(json.dumps({'plugin_root': str(root), 'changed': apply(root)}, ensure_ascii=False))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
# CMD-20260711-v1r4c6-integrate-deploy-verify
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Perform the actual controlled integration, deployment, and verification for V1R4C6.
|
|
||||||
|
|
||||||
V1R4C6 includes:
|
|
||||||
|
|
||||||
1. Admin Workbench / Portal AIOS read-only rollout.
|
|
||||||
2. SR-03 Search Gateway real provider binding.
|
|
||||||
3. Agent Platform Integration.
|
|
||||||
|
|
||||||
Stop before V1R4C6 Git tag creation and baseline promotion. Those operations require owner confirmation.
|
|
||||||
|
|
||||||
## Hard Constraints
|
|
||||||
|
|
||||||
```text
|
|
||||||
AIOS_MEMORY_ENABLED=false
|
|
||||||
memory search remains disabled
|
|
||||||
no global vector index
|
|
||||||
no force-push
|
|
||||||
no V1R4C5 tag movement
|
|
||||||
no Gitea DB mutation
|
|
||||||
no secret plaintext in Git, logs, or reports
|
|
||||||
```
|
|
||||||
|
|
||||||
If any mandatory security or reliability check fails, stop deployment and report the blocker. Do not bypass the gate.
|
|
||||||
|
|
||||||
## Verified Base
|
|
||||||
|
|
||||||
Required before starting:
|
|
||||||
|
|
||||||
```text
|
|
||||||
current_baseline = V1R4C5
|
|
||||||
previous_baseline = V1R4C3
|
|
||||||
baseline_git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704
|
|
||||||
AIOS_GITOPS_BARE_REPO = /data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git
|
|
||||||
runtime = active
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 0 — Preflight and Backup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT=/data/aios-gitops/workspace
|
|
||||||
SOURCE_RELEASE=release/v1r4c6-admin-search-agent-integration-20260711
|
|
||||||
EXEC_BRANCH=release/v1r4c6-integration-exec-20260711
|
|
||||||
|
|
||||||
cd "$ROOT"
|
|
||||||
git fetch origin --tags --prune
|
|
||||||
|
|
||||||
git status --short
|
|
||||||
|
|
||||||
test -f baseline/current.json
|
|
||||||
python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
p = Path('baseline/current.json')
|
|
||||||
d = json.loads(p.read_text(encoding='utf-8'))
|
|
||||||
assert d['current_baseline'] == 'V1R4C5', d
|
|
||||||
assert d['previous_baseline'] == 'V1R4C3', d
|
|
||||||
assert d['git'] == 'a32c76a3c1baa49562e53fb8adce10a5ed0e6704', d
|
|
||||||
assert d.get('constraints', {}).get('memory_search_enabled') is False, d
|
|
||||||
print('baseline preflight: PASS')
|
|
||||||
PY
|
|
||||||
|
|
||||||
test -d /data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git
|
|
||||||
|
|
||||||
BACKUP=/data/aios-gitops/runtime/backups/v1r4c6-predeploy-$(date +%Y%m%d-%H%M%S)
|
|
||||||
mkdir -p "$BACKUP"
|
|
||||||
cp -a baseline/current.json "$BACKUP/current.json"
|
|
||||||
cp -a versions/V1R4C5.json "$BACKUP/V1R4C5.json"
|
|
||||||
|
|
||||||
if git show-ref --verify --quiet "refs/heads/$EXEC_BRANCH"; then
|
|
||||||
git checkout "$EXEC_BRANCH"
|
|
||||||
git pull --ff-only origin "$EXEC_BRANCH"
|
|
||||||
else
|
|
||||||
git checkout -b "$EXEC_BRANCH" "origin/$SOURCE_RELEASE"
|
|
||||||
git push -u origin "$EXEC_BRANCH"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
Never use `git push --force` or `git tag -f`.
|
|
||||||
|
|
||||||
## Phase 1 — Merge Group A: Admin Workbench / Portal AIOS
|
|
||||||
|
|
||||||
Merge in this order:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git merge --no-ff origin/feature/v1r4c4-sr04-admin-workbench-visibility-20260711 \
|
|
||||||
-m 'merge(v1r4c6): admin workbench visibility preparation'
|
|
||||||
|
|
||||||
git merge --no-ff origin/feature/v1r4c4-sr04-portal-aios-visibility-20260710 \
|
|
||||||
-m 'merge(v1r4c6): portal AIOS visibility preparation'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/user-portal-me-route-diagnostics-20260709 \
|
|
||||||
-m 'merge(v1r4c6): portal AIOS routes and RBAC manifest'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/user-portal-rbac-ops-dashboard \
|
|
||||||
-m 'merge(v1r4c6): portal RBAC operations dashboard'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/agent-observability-webui \
|
|
||||||
-m 'merge(v1r4c6): agent observability web UI'
|
|
||||||
```
|
|
||||||
|
|
||||||
Before merging `patch/web-ui-complete-gitops-dashboard-20260706`, compare overlap:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git diff --stat HEAD..origin/patch/web-ui-complete-gitops-dashboard-20260706
|
|
||||||
```
|
|
||||||
|
|
||||||
Merge only if it adds required functionality not already present. Otherwise record it as superseded.
|
|
||||||
|
|
||||||
### Conflict Policy
|
|
||||||
|
|
||||||
Do not auto-resolve conflicts in:
|
|
||||||
|
|
||||||
```text
|
|
||||||
auth/RBAC configuration
|
|
||||||
user/session/tenant isolation
|
|
||||||
search provider configuration
|
|
||||||
safe-mode/controller dispatch
|
|
||||||
systemd units
|
|
||||||
baseline/current.json
|
|
||||||
versions/*.json
|
|
||||||
```
|
|
||||||
|
|
||||||
For those files, stop and report conflict paths.
|
|
||||||
|
|
||||||
### Group A Tests
|
|
||||||
|
|
||||||
Run at minimum:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m pytest -q \
|
|
||||||
tests/test_portal_aios_frontend_contract.py \
|
|
||||||
tests/test_portal_aios_module_registry.py \
|
|
||||||
tests/test_portal_aios_navigation.py \
|
|
||||||
tests/test_portal_integration_health.py \
|
|
||||||
tests/test_portal_system_health_adapter.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Run any additional portal/RBAC/dashboard tests discovered under `tests/`.
|
|
||||||
|
|
||||||
## Phase 2 — Merge Group B: SR-03 Search Gateway
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git merge --no-ff origin/feature/v1r4c4-sr02-search-gateway-resilience-20260710 \
|
|
||||||
-m 'merge(v1r4c6): search gateway resilience metadata'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/search-gateway-governed-general-capability \
|
|
||||||
-m 'merge(v1r4c6): governed search gateway provider binding'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/gitops-change-archive-audit \
|
|
||||||
-m 'merge(v1r4c6): search and GitOps audit evidence'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Group B Security Review
|
|
||||||
|
|
||||||
Verify before deployment:
|
|
||||||
|
|
||||||
```text
|
|
||||||
scope filtering occurs before ranking
|
|
||||||
provenance is attached to accepted results
|
|
||||||
out-of-scope records never reach ranking or summarization
|
|
||||||
fallback is text-only, no-memory, or scoped-only
|
|
||||||
global memory/vector index is disabled
|
|
||||||
memory search remains disabled
|
|
||||||
provider credentials are secret references only
|
|
||||||
```
|
|
||||||
|
|
||||||
Reject any code path that silently broadens search scope.
|
|
||||||
|
|
||||||
### Group B Tests
|
|
||||||
|
|
||||||
Discover and run all search/provider/isolation tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find tests -maxdepth 2 -type f \
|
|
||||||
\( -iname '*search*' -o -iname '*provider*' -o -iname '*scope*' -o -iname '*isolation*' \) \
|
|
||||||
-print
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the discovered files with `python3 -m pytest -q`.
|
|
||||||
|
|
||||||
Also run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash ops/ci/run-user-data-isolation-acceptance.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
If that script is not present after merge, stop: the data-isolation gate is mandatory.
|
|
||||||
|
|
||||||
## Phase 3 — Merge Group C: Agent Platform
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git merge --no-ff origin/patch/agent-event-bus-unified \
|
|
||||||
-m 'merge(v1r4c6): unified scoped agent event bus'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/agent-gpt-message-plane \
|
|
||||||
-m 'merge(v1r4c6): scoped agent GPT message plane'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/autonomous-controller-dispatch-20260709 \
|
|
||||||
-m 'merge(v1r4c6): autonomous controller dispatch with gates'
|
|
||||||
|
|
||||||
git merge --no-ff origin/patch/self-evolution-agent-orchestration-policy \
|
|
||||||
-m 'merge(v1r4c6): self-evolution orchestration policy'
|
|
||||||
```
|
|
||||||
|
|
||||||
Review `patch/aios-autonomous-orchestrator-architecture-review-v1`:
|
|
||||||
|
|
||||||
- merge if it contains a current architecture record not already present;
|
|
||||||
- otherwise mark it superseded in the release report.
|
|
||||||
|
|
||||||
### Group C Security Review
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
|
|
||||||
```text
|
|
||||||
event envelope includes tenant/group, user, project, session, agent, source and provenance
|
|
||||||
message plane rejects cross-scope delivery
|
|
||||||
denied messages never forward body/content
|
|
||||||
controller dispatch checks autonomous safe mode before mutation
|
|
||||||
consequential actions remain owner-gated
|
|
||||||
duplicate dispatch is idempotent or rejected
|
|
||||||
queue/retry behavior is bounded and observable
|
|
||||||
```
|
|
||||||
|
|
||||||
### Group C Tests
|
|
||||||
|
|
||||||
Discover and run agent/event/message/dispatch/controller tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find tests -maxdepth 2 -type f \
|
|
||||||
\( -iname '*agent*' -o -iname '*event*' -o -iname '*message*' -o -iname '*dispatch*' -o -iname '*autonomous_controller*' \) \
|
|
||||||
-print
|
|
||||||
```
|
|
||||||
|
|
||||||
Run all discovered files with `python3 -m pytest -q`.
|
|
||||||
|
|
||||||
Re-run the SR-02 watcher/controller suite and require all tests to pass.
|
|
||||||
|
|
||||||
## Phase 4 — Full Test and Static Validation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m compileall -q active-runtime scripts tests
|
|
||||||
python3 -m pytest -q
|
|
||||||
```
|
|
||||||
|
|
||||||
If the full suite has unrelated historical failures, report them separately, but all V1R4C6-touched component tests must pass.
|
|
||||||
|
|
||||||
Validate JSON/YAML configuration without printing secrets.
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git diff --check
|
|
||||||
git status --short
|
|
||||||
```
|
|
||||||
|
|
||||||
Commit integration result normally:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add -A
|
|
||||||
git commit -m 'release: integrate V1R4C6 admin search and agent platform'
|
|
||||||
git push origin "$EXEC_BRANCH"
|
|
||||||
```
|
|
||||||
|
|
||||||
If no uncommitted changes remain because merge commits already captured everything, just push normally.
|
|
||||||
|
|
||||||
## Phase 5 — Deploy to Staging/Production Runtime
|
|
||||||
|
|
||||||
Use the existing deployment mechanism for this repository. Do not invent a new deployment path.
|
|
||||||
|
|
||||||
Before restart/reload, identify changed services and frontend bundles from the merged diff.
|
|
||||||
|
|
||||||
At minimum:
|
|
||||||
|
|
||||||
1. Deploy backend modules under `active-runtime/app/`.
|
|
||||||
2. Deploy portal frontend/navigation/proxy changes.
|
|
||||||
3. Deploy Search Gateway provider binding using secret references.
|
|
||||||
4. Deploy Agent Platform event/message/dispatch changes.
|
|
||||||
5. Preserve existing Gitea sentinel and user-data isolation systemd controls.
|
|
||||||
6. Keep `AIOS_MEMORY_ENABLED=false`.
|
|
||||||
|
|
||||||
Restart only affected services. Do not restart Gitea unless it independently fails health checks.
|
|
||||||
|
|
||||||
After deployment, verify affected services are active and no restart loop exists.
|
|
||||||
|
|
||||||
## Phase 6 — Actual Deployment Verification
|
|
||||||
|
|
||||||
### Global
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsS http://127.0.0.1:18890/api/base | python3 -m json.tool
|
|
||||||
curl -fsS https://cnnetmkt.top/api/base | python3 -m json.tool
|
|
||||||
```
|
|
||||||
|
|
||||||
Before V1R4C6 promotion both must still report V1R4C5.
|
|
||||||
|
|
||||||
Verify Gitea sentinel remains healthy and memory search remains disabled.
|
|
||||||
|
|
||||||
### Gate A — Portal/Admin Workbench
|
|
||||||
|
|
||||||
Use an existing authenticated test mechanism or secret-backed admin session. Do not log cookies/tokens.
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/portal/me/aios/manifest
|
|
||||||
/portal/me/aios/dashboard
|
|
||||||
/portal/me/aios/health
|
|
||||||
```
|
|
||||||
|
|
||||||
For `user`, `group_admin/admin`, and `owner`, record only module names/status, not personal data.
|
|
||||||
|
|
||||||
Required admin-visible functions:
|
|
||||||
|
|
||||||
```text
|
|
||||||
task progress
|
|
||||||
process/flow diagram entry
|
|
||||||
system health
|
|
||||||
agent observability
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify forbidden owner modules are hidden from non-owner admins.
|
|
||||||
|
|
||||||
### Gate B — Search Gateway
|
|
||||||
|
|
||||||
Execute scoped test queries using non-sensitive fixtures.
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
|
|
||||||
```text
|
|
||||||
real provider selected
|
|
||||||
scope applied before ranking
|
|
||||||
provenance returned
|
|
||||||
out-of-scope fixture denied
|
|
||||||
safe fallback works
|
|
||||||
memory search remains disabled
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gate C — Agent Platform
|
|
||||||
|
|
||||||
Using synthetic non-sensitive events/messages:
|
|
||||||
|
|
||||||
```text
|
|
||||||
same-scope delivery succeeds
|
|
||||||
cross-user/project/session delivery is denied
|
|
||||||
denied payload body is not forwarded
|
|
||||||
safe-mode blocks controller mutation
|
|
||||||
owner-gated action remains pending approval
|
|
||||||
duplicate dispatch is idempotent/rejected
|
|
||||||
```
|
|
||||||
|
|
||||||
## Phase 7 — Release Evidence
|
|
||||||
|
|
||||||
Update:
|
|
||||||
|
|
||||||
```text
|
|
||||||
docs/releases/V1R4C6-acceptance-checklist.md
|
|
||||||
docs/releases/V1R4C6-acceptance-evidence.md
|
|
||||||
docs/releases/V1R4C6-release-gate-status.md
|
|
||||||
versions/V1R4C6.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Set each gate to PASS/FAIL with concrete test commands and results.
|
|
||||||
|
|
||||||
Required final state before requesting owner confirmation:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Gate A = PASS
|
|
||||||
Gate B = PASS
|
|
||||||
Gate C = PASS
|
|
||||||
full touched-component tests = PASS
|
|
||||||
runtime write acceptance = PASS
|
|
||||||
Gitea sentinel = healthy
|
|
||||||
memory search = disabled
|
|
||||||
force-push used = no
|
|
||||||
V1R4C6 tag = absent
|
|
||||||
active baseline = V1R4C5
|
|
||||||
```
|
|
||||||
|
|
||||||
Commit and push evidence normally to `$EXEC_BRANCH`.
|
|
||||||
|
|
||||||
## Stop Point — Owner Confirmation Required
|
|
||||||
|
|
||||||
Do not create `V1R4C6` tag.
|
|
||||||
Do not promote baseline to V1R4C6.
|
|
||||||
Do not enable memory search.
|
|
||||||
|
|
||||||
When all gates pass, notify the owner through the configured notification mechanism, preferably WeChat if configured. The notification must contain no secrets and must request confirmation for:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Create V1R4C6 Git tag
|
|
||||||
Promote active baseline from V1R4C5 to V1R4C6
|
|
||||||
```
|
|
||||||
|
|
||||||
Required report fields:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Status
|
|
||||||
Integration branch and HEAD
|
|
||||||
Merged branches
|
|
||||||
Skipped/superseded branches
|
|
||||||
Gate A result
|
|
||||||
Gate B result
|
|
||||||
Gate C result
|
|
||||||
Test totals
|
|
||||||
Deployment services/build versions
|
|
||||||
Portal/admin verification result
|
|
||||||
Search provider verification result
|
|
||||||
Agent platform verification result
|
|
||||||
Gitea sentinel status
|
|
||||||
Memory search status
|
|
||||||
Active baseline
|
|
||||||
V1R4C6 tag status
|
|
||||||
Rollback readiness
|
|
||||||
Owner confirmation required
|
|
||||||
```
|
|
||||||
|
|
||||||
If WeChat notification is not configured, write the exact notification message to:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/data/aios-gitops/runtime/state/V1R4C6-owner-confirmation-message.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
and report that path.
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
# CMD-20260711-v1r4c6-run-to-owner-gate-wechat
|
|
||||||
|
|
||||||
## Execution Mode
|
|
||||||
|
|
||||||
Execute immediately using the existing V1R4C6 integration command:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ops/openclaw/CMD-20260711-v1r4c6-integrate-deploy-verify.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Continue through actual merge, tests, deployment, and production verification.
|
|
||||||
|
|
||||||
Stop only at the owner gate for:
|
|
||||||
|
|
||||||
```text
|
|
||||||
V1R4C6 Git tag creation
|
|
||||||
V1R4C6 baseline promotion
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not ask the owner to perform routine engineering or deployment work.
|
|
||||||
|
|
||||||
## Verified Starting State
|
|
||||||
|
|
||||||
Required before starting:
|
|
||||||
|
|
||||||
```text
|
|
||||||
runtime = active
|
|
||||||
current_baseline = V1R4C5
|
|
||||||
previous_baseline = V1R4C3
|
|
||||||
baseline_git = a32c76a3c1baa49562e53fb8adce10a5ed0e6704
|
|
||||||
AIOS_GITOPS_BARE_REPO = /data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git
|
|
||||||
memory_search_enabled = false
|
|
||||||
force_push_allowed = false
|
|
||||||
```
|
|
||||||
|
|
||||||
If any starting condition differs, stop and send a WeChat blocker notification.
|
|
||||||
|
|
||||||
## Required Work
|
|
||||||
|
|
||||||
Complete all phases in the referenced command:
|
|
||||||
|
|
||||||
1. Create/update the normal non-force-pushed integration execution branch.
|
|
||||||
2. Merge and validate Admin Workbench / Portal AIOS.
|
|
||||||
3. Merge and validate SR-03 Search Gateway real provider binding.
|
|
||||||
4. Merge and validate Agent Platform Integration.
|
|
||||||
5. Run component and full touched-scope test suites.
|
|
||||||
6. Deploy using the repository's existing deployment mechanism.
|
|
||||||
7. Verify production routes, RBAC, scope isolation, provenance, safe mode, and owner gates.
|
|
||||||
8. Update V1R4C6 release evidence and version metadata.
|
|
||||||
9. Push all normal commits to origin.
|
|
||||||
10. Keep V1R4C6 tag absent and baseline at V1R4C5 until owner approval.
|
|
||||||
|
|
||||||
## Hard Constraints
|
|
||||||
|
|
||||||
```text
|
|
||||||
AIOS_MEMORY_ENABLED=false
|
|
||||||
memory search remains disabled
|
|
||||||
no global memory/vector index
|
|
||||||
no force-push
|
|
||||||
no tag rewrite
|
|
||||||
no V1R4C5 tag movement
|
|
||||||
no Gitea DB mutation
|
|
||||||
no plaintext secrets in Git, logs, reports, or WeChat
|
|
||||||
```
|
|
||||||
|
|
||||||
Never bypass a failed security, isolation, RBAC, safe-mode, or owner gate.
|
|
||||||
|
|
||||||
## WeChat Progress Notifications
|
|
||||||
|
|
||||||
Use the configured OpenClaw WeChat notification channel.
|
|
||||||
|
|
||||||
Send concise progress notifications at these milestones:
|
|
||||||
|
|
||||||
### Notification 1 — Integration Started
|
|
||||||
|
|
||||||
```text
|
|
||||||
V1R4C6 集成已启动。
|
|
||||||
范围:管理员工作台、SR-03 Search Gateway、Agent Platform。
|
|
||||||
当前基线:V1R4C5。
|
|
||||||
memory search 保持关闭。
|
|
||||||
下一步:完成合并、测试和部署验证。
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notification 2 — Gate A Complete
|
|
||||||
|
|
||||||
Include only:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Gate A result: PASS/FAIL
|
|
||||||
Admin task progress visibility: PASS/FAIL
|
|
||||||
Flow diagram visibility: PASS/FAIL
|
|
||||||
RBAC/isolation: PASS/FAIL
|
|
||||||
Blockers, if any
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not include user records, tokens, cookies, or sensitive task content.
|
|
||||||
|
|
||||||
### Notification 3 — Gate B Complete
|
|
||||||
|
|
||||||
Include only:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Gate B result: PASS/FAIL
|
|
||||||
Real provider binding: PASS/FAIL
|
|
||||||
Scope-before-ranking: PASS/FAIL
|
|
||||||
Provenance: PASS/FAIL
|
|
||||||
Safe fallback: PASS/FAIL
|
|
||||||
Memory search: disabled
|
|
||||||
Blockers, if any
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notification 4 — Gate C Complete
|
|
||||||
|
|
||||||
Include only:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Gate C result: PASS/FAIL
|
|
||||||
Event scope: PASS/FAIL
|
|
||||||
Message plane scope: PASS/FAIL
|
|
||||||
Safe-mode dispatch block: PASS/FAIL
|
|
||||||
Owner gate: PASS/FAIL
|
|
||||||
Blockers, if any
|
|
||||||
```
|
|
||||||
|
|
||||||
## Final WeChat Owner Authorization Request
|
|
||||||
|
|
||||||
Send only after all mandatory gates and deployment verification pass.
|
|
||||||
|
|
||||||
Use this message:
|
|
||||||
|
|
||||||
```text
|
|
||||||
V1R4C6 已完成实际合并、测试、部署和生产验证,现等待最终发布授权。
|
|
||||||
|
|
||||||
版本范围:
|
|
||||||
1. 管理员工作台 / Portal AIOS 上线
|
|
||||||
2. SR-03 Search Gateway Real Provider Binding
|
|
||||||
3. Agent Platform Integration
|
|
||||||
|
|
||||||
验收结果:
|
|
||||||
- Gate A(管理员工作台 / RBAC / 隔离):PASS
|
|
||||||
- Gate B(Provider / scope / provenance / fallback):PASS
|
|
||||||
- Gate C(Event Bus / Message Plane / Dispatch / Owner Gate):PASS
|
|
||||||
- Runtime 写链路:PASS
|
|
||||||
- Gitea consistency sentinel:healthy
|
|
||||||
- memory search:仍为 disabled
|
|
||||||
- force-push:未使用
|
|
||||||
- 当前 active baseline:V1R4C5
|
|
||||||
- V1R4C6 tag:尚未创建
|
|
||||||
|
|
||||||
请明确回复以下其中一项:
|
|
||||||
A. 批准创建 V1R4C6 Git tag,并将 active baseline 从 V1R4C5 晋升至 V1R4C6。
|
|
||||||
B. 暂不批准,保持 V1R4C5,并说明需要处理的问题。
|
|
||||||
```
|
|
||||||
|
|
||||||
## Authorization Rules
|
|
||||||
|
|
||||||
Do not treat reactions, emojis, acknowledgements, or ambiguous replies as approval.
|
|
||||||
|
|
||||||
Valid approval must explicitly authorize both:
|
|
||||||
|
|
||||||
```text
|
|
||||||
create V1R4C6 Git tag
|
|
||||||
promote baseline V1R4C5 -> V1R4C6
|
|
||||||
```
|
|
||||||
|
|
||||||
If the owner approves only one operation, execute only the approved operation and stop before the other.
|
|
||||||
|
|
||||||
Before executing a consequential operation, record authorization in:
|
|
||||||
|
|
||||||
```text
|
|
||||||
docs/releases/V1R4C6-owner-approval.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Record only:
|
|
||||||
|
|
||||||
```text
|
|
||||||
approved operation(s)
|
|
||||||
approved_at
|
|
||||||
approval channel = wechat
|
|
||||||
message/conversation identifier if available
|
|
||||||
owner identity reference without copying personal message content
|
|
||||||
memory search remains disabled
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not store access tokens, cookies, phone numbers, or unrelated chat content.
|
|
||||||
|
|
||||||
## WeChat Failure Handling
|
|
||||||
|
|
||||||
If the configured WeChat channel is unavailable:
|
|
||||||
|
|
||||||
1. Do not claim that a message was sent.
|
|
||||||
2. Stop before tag/promotion.
|
|
||||||
3. Write the exact final authorization request to:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/data/aios-gitops/runtime/state/V1R4C6-owner-confirmation-message.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Write notification failure details to:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/data/aios-gitops/runtime/state/V1R4C6-wechat-notification-failure.json
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Report the failure through the active GitOps task/status channel.
|
|
||||||
|
|
||||||
## Final Stop Condition
|
|
||||||
|
|
||||||
Before explicit owner authorization, final state must remain:
|
|
||||||
|
|
||||||
```text
|
|
||||||
current_baseline = V1R4C5
|
|
||||||
V1R4C6 tag = absent
|
|
||||||
memory search = disabled
|
|
||||||
```
|
|
||||||
|
|
||||||
After owner authorization, follow the existing tag confirmation and baseline promotion controls. Do not bypass consequential-operation confirmation layers.
|
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"type": "branch",
|
"type": "branch",
|
||||||
"branch": "deploy/v1r4c6-production-20260711",
|
"branch": "feature/v1r4c4-owner-maintenance-gate-v2-20260710",
|
||||||
"base": "main",
|
"base": "main",
|
||||||
"created_by": "chatgpt-gitops-bot",
|
"created_by": "chatgpt-gitops-bot",
|
||||||
"created_at": "2026-07-11T13:10:51Z",
|
"created_at": "2026-07-10T10:32:35Z",
|
||||||
"direct_main_push": false,
|
"direct_main_push": false,
|
||||||
"owner_approval_required": true
|
"owner_approval_required": true
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "patch-1784043007770-e97206c3",
|
|
||||||
"patch_name": "gitops-isolated-worktree-governance-20260714T232948UTC8",
|
|
||||||
"branch": "patch/gitops-isolated-worktree-governance-20260714T232948UTC8",
|
|
||||||
"risk": "high",
|
|
||||||
"description": "Root-cause remediation for recurring ACL and shared-worktree failures: unified aiman:gitops runtime identity, UMask=0002, inherited default ACLs, per-task isolated Git worktrees, startup preflight, migration/rollback scripts, systemd drop-in templates, audit evidence, and an OpenClaw execution task. No production business deployment in this patch.",
|
|
||||||
"created_by": "chatgpt-gitops-bot",
|
|
||||||
"created_at": "2026-07-14T15:30:07Z"
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "branch",
|
|
||||||
"branch": "reconcile/v1r4c5-release-to-main-20260711",
|
|
||||||
"base": "main",
|
|
||||||
"created_by": "chatgpt-gitops-bot",
|
|
||||||
"created_at": "2026-07-10T23:56:41Z",
|
|
||||||
"direct_main_push": false,
|
|
||||||
"owner_approval_required": true
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "branch",
|
|
||||||
"branch": "task/codex/gitops-isolated-worktree-migration-20260715T001652UTC8",
|
|
||||||
"base": "patch/gitops-isolated-worktree-governance-20260714T232948UTC8",
|
|
||||||
"created_by": "chatgpt-gitops-bot",
|
|
||||||
"created_at": "2026-07-14T16:17:28Z",
|
|
||||||
"direct_main_push": false,
|
|
||||||
"owner_approval_required": true
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "patch-1783707996243-33717dfe",
|
|
||||||
"patch_name": "v1r4c6-admin-search-agent-integration-20260711",
|
|
||||||
"branch": "release/v1r4c6-admin-search-agent-integration-20260711",
|
|
||||||
"risk": "high",
|
|
||||||
"description": "V1R4C6 bundled integration release: Admin Workbench visibility, SR-03 Search Gateway real provider binding, and Agent Platform Integration. Memory search remains disabled and user data isolation gates remain mandatory.",
|
|
||||||
"created_by": "chatgpt-gitops-bot",
|
|
||||||
"created_at": "2026-07-10T18:26:36Z"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
version: 1
|
||||||
|
name: owner-maintenance-gate
|
||||||
|
baseline: V1R4C3
|
||||||
|
created_at: "2026-07-10"
|
||||||
|
purpose: Owner-controlled non-dangerous maintenance permissions without arbitrary host CLI access.
|
||||||
|
|
||||||
|
principles:
|
||||||
|
- agents_may_detect_and_request
|
||||||
|
- owner_may_approve_non_dangerous_actions
|
||||||
|
- runner_executes_only_allowlisted_command_keys
|
||||||
|
- no_arbitrary_shell
|
||||||
|
- protected_branch_merge_is_default_owner_gate
|
||||||
|
- every_execution_must_be_audited
|
||||||
|
|
||||||
|
permission_tiers:
|
||||||
|
READ_ONLY:
|
||||||
|
agent_allowed: true
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: false
|
||||||
|
AUTO_PATCH:
|
||||||
|
agent_allowed: true
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: false
|
||||||
|
OWNER_NON_DANGEROUS_ALL:
|
||||||
|
description: Owner may execute all non-destructive allowlisted maintenance command_keys after owner gate approval.
|
||||||
|
agent_allowed: false
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: true
|
||||||
|
approval_model: protected_branch_merge_or_signed_owner_commit
|
||||||
|
OWNER_HIGH_RISK:
|
||||||
|
description: main merge, baseline promotion, tag creation, credential/provider mutation, destructive cleanup, production restart, SSH/firewall mutation.
|
||||||
|
agent_allowed: false
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: true
|
||||||
|
extra_review_required: true
|
||||||
|
|
||||||
|
safety_classification:
|
||||||
|
non_dangerous_requirements:
|
||||||
|
destructive: false
|
||||||
|
arbitrary_shell: false
|
||||||
|
credential_mutation: false
|
||||||
|
provider_mutation: false
|
||||||
|
external_network_write: false
|
||||||
|
production_data_delete: false
|
||||||
|
baseline_or_tag_mutation: false
|
||||||
|
main_branch_direct_write: false
|
||||||
|
command_key_must_be_allowlisted: true
|
||||||
|
always_high_risk:
|
||||||
|
- merge_to_main
|
||||||
|
- baseline_promotion
|
||||||
|
- tag_creation
|
||||||
|
- credential_rotation
|
||||||
|
- embedding_provider_change
|
||||||
|
- database_delete_or_drop
|
||||||
|
- rm_recursive_or_force
|
||||||
|
- service_restart_production
|
||||||
|
- firewall_or_ssh_config_change
|
||||||
|
- arbitrary_bash_or_shell
|
||||||
|
|
||||||
|
allowlist:
|
||||||
|
openclaw.memory.index.force:
|
||||||
|
permission: OWNER_NON_DANGEROUS_ALL
|
||||||
|
risk: medium
|
||||||
|
destructive: false
|
||||||
|
command_vector:
|
||||||
|
- /usr/local/bin/openclaw
|
||||||
|
- memory
|
||||||
|
- index
|
||||||
|
- --force
|
||||||
|
timeout_seconds: 1800
|
||||||
|
lock_key: memory-index-rebuild
|
||||||
|
acceptance:
|
||||||
|
- openclaw memory search "V1R4C3"
|
||||||
|
- openclaw memory search "Gitea Branch Sync"
|
||||||
|
- openclaw memory search "Autonomous Controller"
|
||||||
|
success_signals:
|
||||||
|
- AIOS_OWNER_MAINTENANCE_MEMORY_INDEX_REBUILT
|
||||||
|
- AIOS_MEMORY_SEARCH_RESTORED
|
||||||
|
|
||||||
|
status: proposed
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Owner maintenance allowlist runner.
|
||||||
|
|
||||||
|
Executes only approved command_key mappings. It never runs arbitrary shell strings.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml # type: ignore
|
||||||
|
except Exception:
|
||||||
|
yaml = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AllowedCommand:
|
||||||
|
command_key: str
|
||||||
|
argv: tuple[str, ...]
|
||||||
|
permission: str
|
||||||
|
risk: str
|
||||||
|
destructive: bool
|
||||||
|
timeout_seconds: int
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_COMMANDS: dict[str, AllowedCommand] = {
|
||||||
|
"openclaw.memory.index.force": AllowedCommand(
|
||||||
|
command_key="openclaw.memory.index.force",
|
||||||
|
argv=("/usr/local/bin/openclaw", "memory", "index", "--force"),
|
||||||
|
permission="OWNER_NON_DANGEROUS_ALL",
|
||||||
|
risk="medium",
|
||||||
|
destructive=False,
|
||||||
|
timeout_seconds=1800,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
DENY_FLAGS = {
|
||||||
|
"arbitrary_shell",
|
||||||
|
"credential_mutation",
|
||||||
|
"provider_mutation",
|
||||||
|
"external_network_write",
|
||||||
|
"production_data_delete",
|
||||||
|
"baseline_or_tag_mutation",
|
||||||
|
"main_branch_direct_write",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_text(value: str) -> str:
|
||||||
|
return hashlib.sha256(value.encode("utf-8", errors="replace")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def load_request(path: Path) -> dict[str, Any]:
|
||||||
|
if yaml is None:
|
||||||
|
raise RuntimeError("PyYAML is required to load owner request YAML")
|
||||||
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("owner request must be a YAML object")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def validate_request(req: dict[str, Any]) -> AllowedCommand:
|
||||||
|
command_key = req.get("command_key")
|
||||||
|
if command_key not in ALLOWED_COMMANDS:
|
||||||
|
raise PermissionError(f"command_key is not allowlisted: {command_key}")
|
||||||
|
allowed = ALLOWED_COMMANDS[command_key]
|
||||||
|
if req.get("permission") != allowed.permission:
|
||||||
|
raise PermissionError("request permission does not match allowlist")
|
||||||
|
safety = req.get("safety", {})
|
||||||
|
if not isinstance(safety, dict):
|
||||||
|
raise PermissionError("safety metadata is required")
|
||||||
|
if bool(safety.get("destructive", True)) != allowed.destructive:
|
||||||
|
raise PermissionError("destructive flag does not match allowlist")
|
||||||
|
denied = [flag for flag in DENY_FLAGS if safety.get(flag) is True]
|
||||||
|
if denied:
|
||||||
|
raise PermissionError(f"denied safety flags: {denied}")
|
||||||
|
gate = req.get("owner_gate", {})
|
||||||
|
if not isinstance(gate, dict) or gate.get("required") is not True:
|
||||||
|
raise PermissionError("owner_gate.required must be true")
|
||||||
|
if not gate.get("approved_by") or not gate.get("approved_at") or not gate.get("approval_commit"):
|
||||||
|
raise PermissionError("owner approval fields are required before execution")
|
||||||
|
return allowed
|
||||||
|
|
||||||
|
|
||||||
|
def run_allowed_command(allowed: AllowedCommand, dry_run: bool) -> dict[str, Any]:
|
||||||
|
if dry_run:
|
||||||
|
return {"dry_run": True, "argv": list(allowed.argv), "exit_code": None}
|
||||||
|
proc = subprocess.run(
|
||||||
|
list(allowed.argv),
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=allowed.timeout_seconds,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"dry_run": False,
|
||||||
|
"argv": list(allowed.argv),
|
||||||
|
"exit_code": proc.returncode,
|
||||||
|
"executed_at": utc_now(),
|
||||||
|
"stdout_sha256": sha256_text(proc.stdout),
|
||||||
|
"stderr_sha256": sha256_text(proc.stderr),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Run an owner-approved allowlisted maintenance request")
|
||||||
|
parser.add_argument("request", type=Path)
|
||||||
|
parser.add_argument("--dry-run", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
req = load_request(args.request)
|
||||||
|
allowed = validate_request(req)
|
||||||
|
result = run_allowed_command(allowed, dry_run=args.dry_run)
|
||||||
|
print(json.dumps({"status": "ok", "command_key": allowed.command_key, "result": result}, indent=2))
|
||||||
|
return 0 if result.get("exit_code") in (None, 0) else int(result["exit_code"])
|
||||||
|
except Exception as exc:
|
||||||
|
print(json.dumps({"status": "error", "error": str(exc)}, indent=2), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
|
|
||||||
SOURCE="${AIOS_SOURCE_WORKTREE:-$ROOT/workspace}"
|
|
||||||
BARE="${AIOS_BARE_REPO:-$ROOT/repo.git}"
|
|
||||||
WORKTREES="${AIOS_WORKTREE_ROOT:-$ROOT/worktrees}"
|
|
||||||
RUNTIME_USER="${AIOS_RUNTIME_USER:-aiman}"
|
|
||||||
RUNTIME_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
|
|
||||||
|
|
||||||
if [[ "$(id -u)" -ne 0 ]]; then
|
|
||||||
echo "ERROR: bootstrap must run as root" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
getent passwd "$RUNTIME_USER" >/dev/null
|
|
||||||
getent group "$RUNTIME_GROUP" >/dev/null
|
|
||||||
[[ -d "$SOURCE/.git" || -f "$SOURCE/.git" ]] || {
|
|
||||||
echo "ERROR: source worktree not found: $SOURCE" >&2
|
|
||||||
exit 3
|
|
||||||
}
|
|
||||||
|
|
||||||
install -d -o "$RUNTIME_USER" -g "$RUNTIME_GROUP" -m 2775 "$ROOT" "$WORKTREES"
|
|
||||||
install -d -o "$RUNTIME_USER" -g "$RUNTIME_GROUP" -m 2775 \
|
|
||||||
"$WORKTREES/openclaw" "$WORKTREES/chatgpt" "$WORKTREES/controller"
|
|
||||||
|
|
||||||
if [[ ! -d "$BARE" ]]; then
|
|
||||||
sudo -u "$RUNTIME_USER" -g "$RUNTIME_GROUP" git clone --bare "$SOURCE" "$BARE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chown -R "$RUNTIME_USER:$RUNTIME_GROUP" "$BARE" "$WORKTREES"
|
|
||||||
find "$BARE" "$WORKTREES" -type d -exec chmod 2775 {} +
|
|
||||||
find "$BARE" "$WORKTREES" -type f -exec chmod ug+rw {} +
|
|
||||||
|
|
||||||
setfacl -R -m "u:${RUNTIME_USER}:rwX,g:${RUNTIME_GROUP}:rwX,m::rwX" "$BARE" "$WORKTREES"
|
|
||||||
find "$BARE" "$WORKTREES" -type d -exec \
|
|
||||||
setfacl -m "d:u:${RUNTIME_USER}:rwx,d:g:${RUNTIME_GROUP}:rwx,d:m::rwx" {} +
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
bootstrap_status: PASS
|
|
||||||
root: $ROOT
|
|
||||||
source_worktree: $SOURCE
|
|
||||||
bare_repo: $BARE
|
|
||||||
worktree_root: $WORKTREES
|
|
||||||
runtime_user: $RUNTIME_USER
|
|
||||||
runtime_group: $RUNTIME_GROUP
|
|
||||||
EOF
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
EXPECTED_USER="${AIOS_RUNTIME_USER:-aiman}"
|
|
||||||
EXPECTED_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
|
|
||||||
EXPECTED_UMASK="${AIOS_RUNTIME_UMASK:-0002}"
|
|
||||||
TASK_DIR="${AIOS_TASK_DIR:-.agent/tasks}"
|
|
||||||
RESULT_DIR="${AIOS_RESULT_DIR:-.agent/results}"
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "preflight_status: FAIL"
|
|
||||||
echo "reason: $1"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
USER_NOW="$(id -un)"
|
|
||||||
GROUPS_NOW="$(id -Gn)"
|
|
||||||
UMASK_NOW="$(umask)"
|
|
||||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
|
|
||||||
BRANCH="$(git branch --show-current 2>/dev/null || true)"
|
|
||||||
|
|
||||||
[[ "$USER_NOW" == "$EXPECTED_USER" ]] || fail "runtime user is $USER_NOW, expected $EXPECTED_USER"
|
|
||||||
[[ " $GROUPS_NOW " == *" $EXPECTED_GROUP "* ]] || fail "runtime user is not in $EXPECTED_GROUP"
|
|
||||||
[[ "$UMASK_NOW" == "$EXPECTED_UMASK" || "$UMASK_NOW" == "002" ]] || fail "umask is $UMASK_NOW, expected $EXPECTED_UMASK"
|
|
||||||
[[ -n "$ROOT" ]] || fail "not inside a git worktree"
|
|
||||||
[[ "$ROOT" == /data/aios-gitops/worktrees/* ]] || fail "shared source worktree is prohibited: $ROOT"
|
|
||||||
[[ -w "$ROOT" ]] || fail "worktree root is not writable"
|
|
||||||
[[ -w "$GIT_DIR" ]] || fail "git dir is not writable: $GIT_DIR"
|
|
||||||
[[ ! -e "$GIT_DIR/index.lock" ]] || fail "git index lock exists"
|
|
||||||
install -d -m 2775 "$TASK_DIR" "$RESULT_DIR"
|
|
||||||
[[ -w "$TASK_DIR" ]] || fail "task directory is not writable: $TASK_DIR"
|
|
||||||
[[ -w "$RESULT_DIR" ]] || fail "result directory is not writable: $RESULT_DIR"
|
|
||||||
[[ -z "$(git status --porcelain)" ]] || fail "worktree is not clean at task start"
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
preflight_status: PASS
|
|
||||||
runtime_user: $USER_NOW
|
|
||||||
runtime_groups: $GROUPS_NOW
|
|
||||||
umask: $UMASK_NOW
|
|
||||||
worktree: $ROOT
|
|
||||||
git_dir: $GIT_DIR
|
|
||||||
branch: $BRANCH
|
|
||||||
workspace_clean: true
|
|
||||||
task_path_writable: true
|
|
||||||
result_path_writable: true
|
|
||||||
git_lock_present: false
|
|
||||||
EOF
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ACTION="${1:-}"
|
|
||||||
AGENT="${2:-}"
|
|
||||||
TASK_ID="${3:-}"
|
|
||||||
BASE_REF="${4:-origin/main}"
|
|
||||||
|
|
||||||
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
|
|
||||||
BARE="${AIOS_BARE_REPO:-$ROOT/repo.git}"
|
|
||||||
WORKTREES="${AIOS_WORKTREE_ROOT:-$ROOT/worktrees}"
|
|
||||||
RUNTIME_USER="${AIOS_RUNTIME_USER:-aiman}"
|
|
||||||
RUNTIME_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "usage: $0 create|remove <openclaw|chatgpt|controller> <task-id> [base-ref]" >&2
|
|
||||||
exit 64
|
|
||||||
}
|
|
||||||
|
|
||||||
[[ "$ACTION" =~ ^(create|remove)$ ]] || usage
|
|
||||||
[[ "$AGENT" =~ ^(openclaw|chatgpt|controller)$ ]] || usage
|
|
||||||
[[ "$TASK_ID" =~ ^[A-Za-z0-9._-]+$ ]] || {
|
|
||||||
echo "ERROR: invalid task id" >&2
|
|
||||||
exit 65
|
|
||||||
}
|
|
||||||
[[ "$(id -un)" == "$RUNTIME_USER" ]] || {
|
|
||||||
echo "ERROR: must run as $RUNTIME_USER" >&2
|
|
||||||
exit 66
|
|
||||||
}
|
|
||||||
[[ "$(id -gn)" == "$RUNTIME_GROUP" || " $(id -Gn) " == *" $RUNTIME_GROUP "* ]] || {
|
|
||||||
echo "ERROR: user must be in $RUNTIME_GROUP" >&2
|
|
||||||
exit 67
|
|
||||||
}
|
|
||||||
[[ "$(umask)" == "0002" || "$(umask)" == "002" ]] || {
|
|
||||||
echo "ERROR: umask must be 0002" >&2
|
|
||||||
exit 68
|
|
||||||
}
|
|
||||||
[[ -d "$BARE" ]] || {
|
|
||||||
echo "ERROR: bare repository missing: $BARE" >&2
|
|
||||||
exit 69
|
|
||||||
}
|
|
||||||
|
|
||||||
PATHNAME="$WORKTREES/$AGENT/$TASK_ID"
|
|
||||||
BRANCH="task/$AGENT/$TASK_ID"
|
|
||||||
|
|
||||||
case "$ACTION" in
|
|
||||||
create)
|
|
||||||
[[ ! -e "$PATHNAME" ]] || {
|
|
||||||
echo "ERROR: worktree already exists: $PATHNAME" >&2
|
|
||||||
exit 70
|
|
||||||
}
|
|
||||||
git --git-dir="$BARE" fetch --all --prune
|
|
||||||
git --git-dir="$BARE" worktree add -b "$BRANCH" "$PATHNAME" "$BASE_REF"
|
|
||||||
install -d -m 2775 "$PATHNAME/.agent/tasks/$AGENT" "$PATHNAME/.agent/results/$AGENT"
|
|
||||||
setfacl -m "u:${RUNTIME_USER}:rwx,g:${RUNTIME_GROUP}:rwx,m::rwx" "$PATHNAME"
|
|
||||||
find "$PATHNAME" -type d -exec chmod g+s {} +
|
|
||||||
find "$PATHNAME" -type d -exec \
|
|
||||||
setfacl -m "d:u:${RUNTIME_USER}:rwx,d:g:${RUNTIME_GROUP}:rwx,d:m::rwx" {} +
|
|
||||||
printf 'worktree: %s\nbranch: %s\nbase: %s\n' "$PATHNAME" "$BRANCH" "$BASE_REF"
|
|
||||||
;;
|
|
||||||
remove)
|
|
||||||
[[ -d "$PATHNAME" ]] || {
|
|
||||||
echo "ERROR: worktree not found: $PATHNAME" >&2
|
|
||||||
exit 71
|
|
||||||
}
|
|
||||||
[[ -z "$(git -C "$PATHNAME" status --porcelain)" ]] || {
|
|
||||||
echo "ERROR: worktree is not clean; retain for investigation" >&2
|
|
||||||
exit 72
|
|
||||||
}
|
|
||||||
git --git-dir="$BARE" worktree remove "$PATHNAME"
|
|
||||||
printf 'removed: %s\n' "$PATHNAME"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
PATCHER_PATH = Path(__file__).resolve().parents[1] / 'fixes' / 'patch_openclaw_weixin_bigint.py'
|
|
||||||
|
|
||||||
|
|
||||||
def load_patcher():
|
|
||||||
spec = importlib.util.spec_from_file_location('patch_openclaw_weixin_bigint', PATCHER_PATH)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_patch_is_idempotent_and_scoped() -> None:
|
|
||||||
patcher = load_patcher()
|
|
||||||
ts = '\n'.join([
|
|
||||||
'import { fileURLToPath } from "node:url";',
|
|
||||||
'import { redactBody, redactUrl } from "../util/redact.js";',
|
|
||||||
'const resp: GetUpdatesResp = JSON.parse(rawText);',
|
|
||||||
])
|
|
||||||
js = '\n'.join([
|
|
||||||
'import { fileURLToPath } from "node:url";',
|
|
||||||
'import { redactBody, redactUrl } from "../util/redact.js";',
|
|
||||||
'const resp = JSON.parse(rawText);',
|
|
||||||
])
|
|
||||||
types = 'export interface WeixinMessage {\n message_id?: number;\n}\n'
|
|
||||||
|
|
||||||
ts_once = patcher.patch_api_ts(ts)
|
|
||||||
js_once = patcher.patch_api_js(js)
|
|
||||||
types_once = patcher.patch_types_ts(types)
|
|
||||||
|
|
||||||
assert patcher.patch_api_ts(ts_once) == ts_once
|
|
||||||
assert patcher.patch_api_js(js_once) == js_once
|
|
||||||
assert patcher.patch_types_ts(types_once) == types_once
|
|
||||||
assert 'parseWeixinGetUpdatesJson(rawText)' in ts_once
|
|
||||||
assert 'parseWeixinGetUpdatesJson(rawText)' in js_once
|
|
||||||
assert 'message_id?: string | number;' in types_once
|
|
||||||
|
|
||||||
|
|
||||||
def test_node_reviver_preserves_only_weixin_exact_ids() -> None:
|
|
||||||
script = '''
|
|
||||||
const ids = new Set(["message_id", "msg_id", "root_id", "parent_id", "server_msg_id", "new_msg_id"]);
|
|
||||||
const parsed = JSON.parse('{"ret":0,"seq":42,"msgs":[{"message_id":7482038281766381234,"create_time_ms":1783856936191,"item_list":[{"msg_id":"3123141975110186863"}]}]}', (key, value, context) => {
|
|
||||||
if (!ids.has(key) || value == null || typeof value === "string") return value;
|
|
||||||
if (typeof value !== "number" || !context?.source || !/^(?:0|[1-9]\\d*)$/.test(context.source)) throw new Error("unsafe id");
|
|
||||||
return context.source;
|
|
||||||
});
|
|
||||||
console.log(JSON.stringify({ messageId: parsed.msgs[0].message_id, msgId: parsed.msgs[0].item_list[0].msg_id, seqType: typeof parsed.seq, timestampType: typeof parsed.msgs[0].create_time_ms }));
|
|
||||||
'''
|
|
||||||
completed = subprocess.run(['node', '-e', script], check=True, capture_output=True, text=True)
|
|
||||||
result = json.loads(completed.stdout)
|
|
||||||
assert result == {
|
|
||||||
'messageId': '7482038281766381234',
|
|
||||||
'msgId': '3123141975110186863',
|
|
||||||
'seqType': 'number',
|
|
||||||
'timestampType': 'number',
|
|
||||||
}
|
|
||||||
@@ -1,563 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def load_js_process_helpers() -> str:
|
|
||||||
patcher_path = Path(__file__).resolve().parents[1] / 'fixes' / 'patch_openclaw_weixin_hermes.py'
|
|
||||||
assert patcher_path.exists(), f'missing patcher: {patcher_path}'
|
|
||||||
spec = importlib.util.spec_from_file_location('patch_openclaw_weixin_hermes', patcher_path)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module.JS_PROCESS_HELPERS
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_patcher_module() -> object:
|
|
||||||
patcher_path = Path(__file__).resolve().parents[1] / 'fixes' / 'patch_openclaw_weixin_hermes.py'
|
|
||||||
assert patcher_path.exists(), f'missing patcher: {patcher_path}'
|
|
||||||
spec = importlib.util.spec_from_file_location('patch_openclaw_weixin_hermes', patcher_path)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_js_process_helpers(tmp_path: Path, body: str) -> dict:
|
|
||||||
js_process_helpers = load_js_process_helpers()
|
|
||||||
node_prelude = '\n'.join(
|
|
||||||
[
|
|
||||||
'const hermesTestFs = require("node:fs");',
|
|
||||||
'globalThis.existsSync = hermesTestFs.existsSync;',
|
|
||||||
'globalThis.readFileSync = hermesTestFs.readFileSync;',
|
|
||||||
'globalThis.writeFileSync = hermesTestFs.writeFileSync;',
|
|
||||||
'globalThis.mkdirSync = hermesTestFs.mkdirSync;',
|
|
||||||
'globalThis.appendFileSync = hermesTestFs.appendFileSync;',
|
|
||||||
'globalThis.path = require("node:path");',
|
|
||||||
'globalThis.logger = { info() {}, warn() {}, error() {} };',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
node_script = tmp_path / 'hermes_image_followup_check.js'
|
|
||||||
node_script.write_text(node_prelude + '\n\n' + js_process_helpers + '\n\n' + body, encoding='utf-8')
|
|
||||||
completed = subprocess.run(['node', str(node_script)], check=True, capture_output=True, text=True)
|
|
||||||
return json.loads(completed.stdout.strip())
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('is_ts', [True, False], ids=['ts', 'js'])
|
|
||||||
def test_replace_or_insert_process_helpers_is_byte_idempotent(is_ts: bool) -> None:
|
|
||||||
patcher = load_patcher_module()
|
|
||||||
marker = 'export async function processOneMessage'
|
|
||||||
source = marker + '(message) {\n return message;\n}\n'
|
|
||||||
|
|
||||||
once = patcher.replace_or_insert_process_helpers(source, is_ts=is_ts)
|
|
||||||
twice = patcher.replace_or_insert_process_helpers(once, is_ts=is_ts)
|
|
||||||
helpers = patcher.TS_PROCESS_HELPERS if is_ts else patcher.JS_PROCESS_HELPERS
|
|
||||||
|
|
||||||
assert helpers + '\n\n' + marker in once
|
|
||||||
assert twice == once
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
('is_ts', 'hook_name'),
|
|
||||||
[(True, 'TS_PROCESS_HOOK'), (False, 'JS_PROCESS_HOOK')],
|
|
||||||
ids=['ts', 'js'],
|
|
||||||
)
|
|
||||||
def test_remove_and_reinsert_process_hook_is_byte_idempotent(is_ts: bool, hook_name: str) -> None:
|
|
||||||
patcher = load_patcher_module()
|
|
||||||
hook = getattr(patcher, hook_name)
|
|
||||||
indent = ' ' if is_ts else ' '
|
|
||||||
needle = indent + 'ctx.CommandAuthorized = commandAuthorized;\n'
|
|
||||||
next_nonblank = indent + 'continueProcessing();\n'
|
|
||||||
|
|
||||||
def cycle(text: str) -> str:
|
|
||||||
without_hook = patcher.remove_existing_process_hermes_hook(text)
|
|
||||||
return without_hook.replace(needle, needle + hook + '\n', 1)
|
|
||||||
|
|
||||||
once = cycle(needle + next_nonblank)
|
|
||||||
twice = cycle(once)
|
|
||||||
|
|
||||||
assert hook + '\n' + next_nonblank in once
|
|
||||||
assert twice == once
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_looks_like_hermes_image_followup_distinguishes_diagnostics_from_real_image_questions(tmp_path: Path) -> None:
|
|
||||||
cases = {
|
|
||||||
'diag_cache_expired': ('\u56fe\u7247\u7f13\u5b58\u5df2\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u56fe\u7247\u3002', False),
|
|
||||||
'diag_cache_error_en': ('image cache error', False),
|
|
||||||
'diag_cache_expired_en': ('the image cache expired, please resend', False),
|
|
||||||
'diag_copying_text_shows_cache_error': ('\u4e3a\u4ec0\u4e48\u6211\u590d\u5236\u53d1\u7ed9\u4f60\u7684\u6587\u5b57\uff0c\u4f60\u90fd\u663e\u793a\u56fe\u7247\u7f13\u5b58\u5df2\u5931\u6548\uff0c\u8bf7\u91cd\u65b0\u53d1\u9001\u56fe\u7247\u3002', False),
|
|
||||||
'diag_not_image': ('\u8fd9\u4e0d\u662f\u56fe\u7247\uff0c\u662f\u6587\u5b57\u6d88\u606f', False),
|
|
||||||
'diag_text_not_image_en': ('this is a text message, not an image', False),
|
|
||||||
'diag_no_image_sent': ('\u6211\u6ca1\u6709\u53d1\u56fe\u7247', False),
|
|
||||||
'diag_system_logs': ('\u8bf7\u68c0\u67e5\u7cfb\u7edf\u65e5\u5fd7\uff0c\u4ece\u673a\u5236\u4e0a\u4fee\u590d\u9519\u8bef', False),
|
|
||||||
'image_cn_1': ('\u8bf7\u770b\u4e00\u4e0b\u8fd9\u5f20\u56fe\u7247', True),
|
|
||||||
'image_cn_2': ('\u8fd9\u56fe\u662f\u4ec0\u4e48', True),
|
|
||||||
'image_cn_3': ('\u8bc6\u522b\u56fe\u7247\u5185\u5bb9', True),
|
|
||||||
'image_en': ('look at this image', True),
|
|
||||||
}
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'const cases = '
|
|
||||||
+ json.dumps({key: text for key, (text, _) in cases.items()}, ensure_ascii=False)
|
|
||||||
+ ';\nconst results = {};\n'
|
|
||||||
+ 'for (const [key, text] of Object.entries(cases)) {\n'
|
|
||||||
+ ' results[key] = looksLikeHermesImageFollowup(text);\n'
|
|
||||||
+ '}\n'
|
|
||||||
+ 'console.log(JSON.stringify(results));\n',
|
|
||||||
)
|
|
||||||
for key, (_, expected) in cases.items():
|
|
||||||
assert results[key] is expected, f'{key}: expected {expected}, got {results[key]!r}'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_hermes_referenced_image_followup_requires_exact_cache(tmp_path: Path) -> None:
|
|
||||||
cache_path = tmp_path / 'hermes-last-image-cache.json'
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const fs = require("fs");',
|
|
||||||
f'const cachePath = {json.dumps(str(cache_path))};',
|
|
||||||
'const accountId = "account-a";',
|
|
||||||
'const senderId = "sender-b";',
|
|
||||||
'const exactQuotedId = "quoted-123";',
|
|
||||||
'const cache = {};',
|
|
||||||
'cache[hermesLastImageCacheKey(accountId, senderId)] = {',
|
|
||||||
' mediaUrl: "data:image/png;base64,iVBORw0KGgo=",',
|
|
||||||
' timestampMs: Date.now(),',
|
|
||||||
' messageId: "latest-999",',
|
|
||||||
' mediaType: "image/png",',
|
|
||||||
'};',
|
|
||||||
'cache[hermesExactImageCacheKey(accountId, senderId, exactQuotedId)] = {',
|
|
||||||
' mediaUrl: "data:image/png;base64,iVBORw0KGgo=",',
|
|
||||||
' timestampMs: Date.now(),',
|
|
||||||
' messageId: exactQuotedId,',
|
|
||||||
' mediaType: "image/png",',
|
|
||||||
'};',
|
|
||||||
'fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), "utf-8");',
|
|
||||||
'process.env.AIOS_HERMES_WEIXIN_LAST_IMAGE_CACHE = cachePath;',
|
|
||||||
'const generic = resolveHermesReferencedImageFollowup(accountId, senderId, "plain quoted text", "quoted-999");',
|
|
||||||
'const exact = resolveHermesReferencedImageFollowup(accountId, senderId, "plain quoted text", exactQuotedId);',
|
|
||||||
'const forced = resolveHermesFollowupImageContext(accountId, senderId, "plain quoted text", exactQuotedId, false, true);',
|
|
||||||
'console.log(JSON.stringify({ generic, exact, forced }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['generic']['imageIntent'] is False
|
|
||||||
assert results['generic']['matched'] is False
|
|
||||||
assert results['generic']['reason'] == 'referenced_image_not_confirmed'
|
|
||||||
assert results['exact']['imageIntent'] is True
|
|
||||||
assert results['exact']['matched'] is True
|
|
||||||
assert results['exact']['reason'] == 'cached_image_matched'
|
|
||||||
assert results['forced']['imageIntent'] is True
|
|
||||||
assert results['forced']['matched'] is True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_referenced_text_without_media_evidence_stays_out_of_image_cache(tmp_path: Path) -> None:
|
|
||||||
cache_path = tmp_path / 'hermes-last-image-cache.json'
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const fs = require("fs");',
|
|
||||||
f'const cachePath = {json.dumps(str(cache_path))};',
|
|
||||||
'const accountId = "account-a";',
|
|
||||||
'const senderId = "sender-b";',
|
|
||||||
'const cache = {};',
|
|
||||||
'cache[hermesLastImageCacheKey(accountId, senderId)] = {',
|
|
||||||
' mediaUrl: "data:image/png;base64,iVBORw0KGgo=",',
|
|
||||||
' timestampMs: Date.now(),',
|
|
||||||
' messageId: "latest-999",',
|
|
||||||
' mediaType: "image/png",',
|
|
||||||
'};',
|
|
||||||
'fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), "utf-8");',
|
|
||||||
'process.env.AIOS_HERMES_WEIXIN_LAST_IMAGE_CACHE = cachePath;',
|
|
||||||
'const full = { item_list: [{ ref_msg: { message_item: { type: 0, id: "quoted-999" } } }] };',
|
|
||||||
'const hermesContent = "\u8fd9\u662f\u4ec0\u4e48";',
|
|
||||||
'const hermesReferenceContextPresent = hasHermesReferenceContext(full.item_list);',
|
|
||||||
'const hermesQuotedMessageId = findHermesReferencedMessageId(full.item_list);',
|
|
||||||
'const refMediaItem = undefined;',
|
|
||||||
'const hermesReferencedMediaIsImage = Boolean(refMediaItem && hermesCurrentMessageIsImage({}, refMediaItem));',
|
|
||||||
'const hermesReferencedImageFollowupIntent = hermesReferenceContextPresent && !refMediaItem;',
|
|
||||||
'const hermesCurrentImage = false;',
|
|
||||||
'const hermesFollowup = hermesCurrentImage',
|
|
||||||
' ? { imageIntent: false, matched: false, reason: "current_image_message", cacheStrategy: "current", quotedMessageId: hermesQuotedMessageId }',
|
|
||||||
' : (hermesReferencedMediaIsImage',
|
|
||||||
' ? resolveHermesFollowupImageContext(accountId, senderId, hermesContent, hermesQuotedMessageId, false, true)',
|
|
||||||
' : (hermesReferencedImageFollowupIntent',
|
|
||||||
' ? resolveHermesReferencedImageFollowup(accountId, senderId, hermesContent, hermesQuotedMessageId)',
|
|
||||||
' : resolveHermesFollowupImageContext(accountId, senderId, hermesContent)));',
|
|
||||||
'console.log(JSON.stringify({ hermesReferenceContextPresent, hermesQuotedMessageId, hermesReferencedMediaIsImage, hermesReferencedImageFollowupIntent, hermesFollowup }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['hermesReferenceContextPresent'] is True
|
|
||||||
assert results['hermesQuotedMessageId'] == 'quoted-999'
|
|
||||||
assert results['hermesReferencedMediaIsImage'] is False
|
|
||||||
assert results['hermesReferencedImageFollowupIntent'] is True
|
|
||||||
assert results['hermesFollowup']['imageIntent'] is False
|
|
||||||
assert results['hermesFollowup']['matched'] is False
|
|
||||||
assert results['hermesFollowup']['reason'] == 'referenced_image_not_confirmed'
|
|
||||||
|
|
||||||
|
|
||||||
def test_explicit_quoted_text_builds_grounded_plain_text_context(tmp_path: Path) -> None:
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const available = typeof resolveHermesQuotedTextContext === "function";',
|
|
||||||
'const items = [{ type: 1, ref_msg: { message_item: { type: 1, text_item: { text: "被引用的正文" } } } }];',
|
|
||||||
'const result = available ? resolveHermesQuotedTextContext(items, "这是什么意思?") : null;',
|
|
||||||
'console.log(JSON.stringify({ available, result }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['available'] is True
|
|
||||||
assert results['result'] == {
|
|
||||||
'referenced': True,
|
|
||||||
'handled': False,
|
|
||||||
'prompt': '[被引用文字]\n被引用的正文\n[/被引用文字]\n\n[当前问题]\n这是什么意思?\n[/当前问题]',
|
|
||||||
'reason': 'quoted_text_resolved',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_visible_ref_title_builds_grounded_plain_text_context(tmp_path: Path) -> None:
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const items = [{ type: 1, ref_msg: { title: "客户端可见的引用正文", message_item: { type: 0, msg_id: "quoted-123" } } }];',
|
|
||||||
'const result = resolveHermesQuotedTextContext(items, "这是什么意思?");',
|
|
||||||
'console.log(JSON.stringify(result));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results == {
|
|
||||||
'referenced': True,
|
|
||||||
'handled': False,
|
|
||||||
'prompt': '[被引用文字]\n客户端可见的引用正文\n[/被引用文字]\n\n[当前问题]\n这是什么意思?\n[/当前问题]',
|
|
||||||
'reason': 'quoted_text_resolved',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_nested_generic_title_is_not_treated_as_quoted_text(tmp_path: Path) -> None:
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const items = [{ type: 1, ref_msg: { message_item: { type: 0, title: "非协议引用摘要" } } }];',
|
|
||||||
'const result = resolveHermesQuotedTextContext(items, "这是什么意思?");',
|
|
||||||
'console.log(JSON.stringify(result));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results == {
|
|
||||||
'referenced': True,
|
|
||||||
'handled': True,
|
|
||||||
'replyText': '无法读取被引用文字,请复制原文后再提问。',
|
|
||||||
'reason': 'quoted_text_unavailable',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_exact_message_id_cache_resolves_server_reference_without_guessing(tmp_path: Path) -> None:
|
|
||||||
cache_path = tmp_path / 'hermes-message-text-cache.json'
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
f'process.env.AIOS_HERMES_WEIXIN_TEXT_MESSAGE_CACHE = {json.dumps(str(cache_path))};',
|
|
||||||
'const accountId = "account-a";',
|
|
||||||
'const senderId = "sender-b";',
|
|
||||||
'const original = { item_list: [{ type: 1, msg_id: "7481729105240418312", text_item: { text: "客户端已经显示的原始正文" } }] };',
|
|
||||||
'rememberHermesTextMessage(accountId, senderId, original, "客户端已经显示的原始正文");',
|
|
||||||
'const quotedItems = [{ type: 1, ref_msg: { message_item: { type: 0, msg_id: "7481729105240418312" } }, text_item: { text: "这是什么意思?" } }];',
|
|
||||||
'const quotedId = findHermesReferencedMessageId(quotedItems);',
|
|
||||||
'const exact = resolveHermesCachedQuotedText(accountId, senderId, quotedId);',
|
|
||||||
'const wrong = resolveHermesCachedQuotedText(accountId, senderId, "7481729105240418313");',
|
|
||||||
'const resolved = resolveHermesQuotedTextContext(quotedItems, "这是什么意思?", exact);',
|
|
||||||
'const mode = (hermesTestFs.statSync(process.env.AIOS_HERMES_WEIXIN_TEXT_MESSAGE_CACHE).mode & 0o777).toString(8);',
|
|
||||||
'console.log(JSON.stringify({ quotedId, exact, wrong, resolved, mode }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['quotedId'] == '7481729105240418312'
|
|
||||||
assert results['exact'] == '客户端已经显示的原始正文'
|
|
||||||
assert results['wrong'] == ''
|
|
||||||
assert results['resolved'] == {
|
|
||||||
'referenced': True,
|
|
||||||
'handled': False,
|
|
||||||
'prompt': '[被引用文字]\n客户端已经显示的原始正文\n[/被引用文字]\n\n[当前问题]\n这是什么意思?\n[/当前问题]',
|
|
||||||
'reason': 'quoted_text_resolved',
|
|
||||||
}
|
|
||||||
assert results['mode'] == '600'
|
|
||||||
|
|
||||||
|
|
||||||
def test_expired_message_id_cache_entry_is_not_resolved(tmp_path: Path) -> None:
|
|
||||||
cache_path = tmp_path / 'hermes-message-text-cache.json'
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
f'process.env.AIOS_HERMES_WEIXIN_TEXT_MESSAGE_CACHE = {json.dumps(str(cache_path))};',
|
|
||||||
'const key = hermesTextMessageCacheKey("account-a", "sender-b", "quoted-old");',
|
|
||||||
'hermesTestFs.writeFileSync(process.env.AIOS_HERMES_WEIXIN_TEXT_MESSAGE_CACHE, JSON.stringify({ [key]: { text: "过期正文", timestampMs: Date.now() - (8 * 24 * 60 * 60 * 1000) } }), { encoding: "utf-8", mode: 0o600 });',
|
|
||||||
'const result = resolveHermesCachedQuotedText("account-a", "sender-b", "quoted-old");',
|
|
||||||
'console.log(JSON.stringify({ result }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['result'] == ''
|
|
||||||
|
|
||||||
|
|
||||||
def test_unsafe_numeric_message_id_is_rejected_before_cache_keying(tmp_path: Path) -> None:
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const exactString = extractHermesMessageId({ message_id: "7482038281766381234" });',
|
|
||||||
'const unsafeNumber = extractHermesMessageId({ message_id: 7482038281766381234 });',
|
|
||||||
'const safeNumber = extractHermesMessageId({ message_id: 12345 });',
|
|
||||||
'console.log(JSON.stringify({ exactString, unsafeNumber: unsafeNumber ?? null, safeNumber }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results == {
|
|
||||||
'exactString': '7482038281766381234',
|
|
||||||
'unsafeNumber': None,
|
|
||||||
'safeNumber': '12345',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_exact_outbound_create_time_resolves_quoted_bot_text(tmp_path: Path) -> None:
|
|
||||||
cache_path = tmp_path / 'hermes-message-text-cache.json'
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
f'process.env.AIOS_HERMES_WEIXIN_TEXT_MESSAGE_CACHE = {json.dumps(str(cache_path))};',
|
|
||||||
'const accountId = "account-a";',
|
|
||||||
'const senderId = "sender-b";',
|
|
||||||
'const createTimeMs = 1783857900123;',
|
|
||||||
'rememberHermesOutboundTextMessage(accountId, senderId, "机器人出站正文", createTimeMs);',
|
|
||||||
'const quotedItems = [{ type: 1, ref_msg: { message_item: { type: 0, msg_id: "server-quoted", create_time_ms: createTimeMs } }, text_item: { text: "进展汇报" } }];',
|
|
||||||
'const referencedTime = findHermesReferencedCreateTimeMs(quotedItems);',
|
|
||||||
'const exact = resolveHermesCachedQuotedText(accountId, senderId, "server-quoted", referencedTime);',
|
|
||||||
'const wrong = resolveHermesCachedQuotedText(accountId, senderId, "server-quoted", createTimeMs + 1);',
|
|
||||||
'const resolved = resolveHermesQuotedTextContext(quotedItems, "进展汇报", exact);',
|
|
||||||
'console.log(JSON.stringify({ referencedTime, exact, wrong, resolved }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['referencedTime'] == '1783857900123'
|
|
||||||
assert results['exact'] == '机器人出站正文'
|
|
||||||
assert results['wrong'] == ''
|
|
||||||
assert results['resolved']['reason'] == 'quoted_text_resolved'
|
|
||||||
assert '机器人出站正文' in results['resolved']['prompt']
|
|
||||||
|
|
||||||
|
|
||||||
def test_outbound_create_time_collision_fails_closed(tmp_path: Path) -> None:
|
|
||||||
cache_path = tmp_path / 'hermes-message-text-cache.json'
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
f'process.env.AIOS_HERMES_WEIXIN_TEXT_MESSAGE_CACHE = {json.dumps(str(cache_path))};',
|
|
||||||
'const createTimeMs = 1783857900123;',
|
|
||||||
'rememberHermesOutboundTextMessage("account-a", "sender-b", "第一条", createTimeMs);',
|
|
||||||
'rememberHermesOutboundTextMessage("account-a", "sender-b", "第二条", createTimeMs);',
|
|
||||||
'const resolved = resolveHermesCachedQuotedText("account-a", "sender-b", undefined, createTimeMs);',
|
|
||||||
'console.log(JSON.stringify({ resolved }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['resolved'] == ''
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_quoted_text_fails_closed_without_model_prompt(tmp_path: Path) -> None:
|
|
||||||
results = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const available = typeof resolveHermesQuotedTextContext === "function";',
|
|
||||||
'const items = [{ type: 1, ref_msg: { message_item: { type: 0, msg_id: "7481729105240418312" } } }];',
|
|
||||||
'const result = available ? resolveHermesQuotedTextContext(items, "这是什么意思?") : null;',
|
|
||||||
'let modelCalls = 0;',
|
|
||||||
'if (result && !result.handled) modelCalls += 1;',
|
|
||||||
'console.log(JSON.stringify({ available, result, modelCalls }));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results['available'] is True
|
|
||||||
assert results['result'] == {
|
|
||||||
'referenced': True,
|
|
||||||
'handled': True,
|
|
||||||
'replyText': '无法读取被引用文字,请复制原文后再提问。',
|
|
||||||
'reason': 'quoted_text_unavailable',
|
|
||||||
}
|
|
||||||
assert results['modelCalls'] == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('hook_name', ['TS_PROCESS_HOOK', 'JS_PROCESS_HOOK'])
|
|
||||||
def test_process_hook_stops_unavailable_quote_before_model_dispatch(hook_name: str) -> None:
|
|
||||||
patcher = load_patcher_module()
|
|
||||||
hook = getattr(patcher, hook_name)
|
|
||||||
guard_name = 'TS_QUOTED_TEXT_DISPATCH_GUARD' if hook_name.startswith('TS_') else 'JS_QUOTED_TEXT_DISPATCH_GUARD'
|
|
||||||
guard = getattr(patcher, guard_name)
|
|
||||||
|
|
||||||
assert 'rememberHermesTextMessage(deps.accountId, hermesSenderId, full, textBody)' in hook
|
|
||||||
assert 'findHermesReferencedCreateTimeMs(full.item_list)' in hook
|
|
||||||
assert 'resolveHermesCachedQuotedText(deps.accountId, hermesSenderId, hermesQuotedMessageId, hermesQuotedCreateTimeMs)' in hook
|
|
||||||
assert 'resolveHermesQuotedTextContext(full.item_list, hermesContent, hermesCachedQuotedText)' in hook
|
|
||||||
assert '!hermesQuotedTextContext.referenced && !textBody.startsWith("/")' in hook
|
|
||||||
assert 'ctx.Body = hermesQuotedTextContext.prompt' in hook
|
|
||||||
assert 'await dispatchHermesQuotedTextFailClosed(hermesQuotedTextContext, dispatcher)' in guard
|
|
||||||
assert 'markDispatchIdle();' in guard
|
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_quoted_text_fail_closed_uses_dispatcher_without_model(tmp_path: Path) -> None:
|
|
||||||
result = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'(async () => {',
|
|
||||||
' const calls = [];',
|
|
||||||
' const dispatcher = {',
|
|
||||||
' sendFinalReply(payload) { calls.push(["final", payload]); },',
|
|
||||||
' markComplete() { calls.push(["complete"]); },',
|
|
||||||
' async waitForIdle() { calls.push(["idle"]); },',
|
|
||||||
' };',
|
|
||||||
' const context = resolveHermesQuotedTextContext([{ ref_msg: { message_item: { type: 0, msg_id: "quoted-1" } } }], "这是什么意思?");',
|
|
||||||
' let modelCalls = 0;',
|
|
||||||
' const handled = await dispatchHermesQuotedTextFailClosed(context, dispatcher);',
|
|
||||||
' if (!handled) modelCalls += 1;',
|
|
||||||
' console.log(JSON.stringify({ handled, modelCalls, calls }));',
|
|
||||||
'})();',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['handled'] is True
|
|
||||||
assert result['modelCalls'] == 0
|
|
||||||
assert result['calls'] == [
|
|
||||||
['final', {'text': '无法读取被引用文字,请复制原文后再提问。'}],
|
|
||||||
['complete'],
|
|
||||||
['idle'],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
('is_ts', 'guard_name', 'needle'),
|
|
||||||
[
|
|
||||||
(True, 'TS_QUOTED_TEXT_DISPATCH_GUARD', ' logger.debug(`dispatchReplyFromConfig: starting agentId=${route.agentId ?? "(none)"}`);\n'),
|
|
||||||
(False, 'JS_QUOTED_TEXT_DISPATCH_GUARD', ' logger.debug(`dispatchReplyFromConfig: starting agentId=${route.agentId ?? "(none)"}`);\n'),
|
|
||||||
],
|
|
||||||
ids=['ts', 'js'],
|
|
||||||
)
|
|
||||||
def test_dispatch_guard_remove_and_reinsert_is_byte_idempotent(is_ts: bool, guard_name: str, needle: str) -> None:
|
|
||||||
patcher = load_patcher_module()
|
|
||||||
guard = getattr(patcher, guard_name)
|
|
||||||
|
|
||||||
def cycle(text: str) -> str:
|
|
||||||
without = patcher.remove_existing_process_hermes_hook(text)
|
|
||||||
return without.replace(needle, guard + '\n' + needle, 1)
|
|
||||||
|
|
||||||
once = cycle(needle)
|
|
||||||
twice = cycle(once)
|
|
||||||
|
|
||||||
assert guard + '\n' + needle in once
|
|
||||||
assert twice == once
|
|
||||||
|
|
||||||
|
|
||||||
def test_reference_metadata_content_is_not_treated_as_quoted_text(tmp_path: Path) -> None:
|
|
||||||
result = run_js_process_helpers(
|
|
||||||
tmp_path,
|
|
||||||
'\n'.join(
|
|
||||||
[
|
|
||||||
'const items = [{ ref_msg: { message_item: { type: 0, msg_id: "quoted-1", content: "metadata only" } } }];',
|
|
||||||
'console.log(JSON.stringify(resolveHermesQuotedTextContext(items, "这是什么意思?")));',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result['handled'] is True
|
|
||||||
assert result['reason'] == 'quoted_text_unavailable'
|
|
||||||
assert result['replyText'] == '无法读取被引用文字,请复制原文后再提问。'
|
|
||||||
|
|
||||||
|
|
||||||
def test_patch_plugin_process_only_skips_slash_and_channel(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
||||||
patcher = load_patcher_module()
|
|
||||||
calls: list[tuple[str, Path]] = []
|
|
||||||
|
|
||||||
def record(name: str, result):
|
|
||||||
def _inner(root: Path):
|
|
||||||
calls.append((name, root))
|
|
||||||
return result
|
|
||||||
|
|
||||||
return _inner
|
|
||||||
|
|
||||||
monkeypatch.setattr(patcher, 'patch_ts', record('patch_ts', False))
|
|
||||||
monkeypatch.setattr(patcher, 'patch_js', record('patch_js', False))
|
|
||||||
monkeypatch.setattr(patcher, 'patch_process_messages', record('patch_process_messages', {'src/messaging/process-message.ts': True, 'dist/src/messaging/process-message.js': False}))
|
|
||||||
monkeypatch.setattr(patcher, 'patch_channel_files', record('patch_channel_files', {'src/channel.ts': False, 'dist/src/channel.js': False}))
|
|
||||||
monkeypatch.setattr(patcher, 'verify_plugin_hook', record('verify_plugin_hook', {
|
|
||||||
'hook_status': {},
|
|
||||||
'hook_installed': False,
|
|
||||||
'process_status': {'src/messaging/process-message.ts': True, 'dist/src/messaging/process-message.js': True},
|
|
||||||
'process_hook_installed': True,
|
|
||||||
'channel_status': {},
|
|
||||||
'channel_hook_installed': False,
|
|
||||||
'dispatch_reference_found': False,
|
|
||||||
'dispatch_reference_files': [],
|
|
||||||
}))
|
|
||||||
|
|
||||||
result = patcher.patch_plugin(tmp_path, process_only=True)
|
|
||||||
|
|
||||||
assert calls == [
|
|
||||||
('patch_process_messages', tmp_path),
|
|
||||||
('verify_plugin_hook', tmp_path),
|
|
||||||
]
|
|
||||||
assert result['mode'] == 'process-only'
|
|
||||||
assert result['plugin_root'] == str(tmp_path)
|
|
||||||
assert result['slash_changes'] == {}
|
|
||||||
assert result['channel_changes'] == {}
|
|
||||||
assert result['process_changes'] == {'src/messaging/process-message.ts': True, 'dist/src/messaging/process-message.js': False}
|
|
||||||
assert result['behavior_changes'] == {'src/messaging/process-message.ts': True, 'dist/src/messaging/process-message.js': False}
|
|
||||||
assert result['verification']['process_hook_installed'] is True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_supports_default_root_and_process_only_flag() -> None:
|
|
||||||
patcher = load_patcher_module()
|
|
||||||
|
|
||||||
defaults = patcher.parse_args([])
|
|
||||||
assert defaults.plugin_root == patcher.DEFAULT_PLUGIN_ROOT
|
|
||||||
assert defaults.process_only is False
|
|
||||||
|
|
||||||
process_only = patcher.parse_args(['--process-only'])
|
|
||||||
assert process_only.plugin_root == patcher.DEFAULT_PLUGIN_ROOT
|
|
||||||
assert process_only.process_only is True
|
|
||||||
|
|
||||||
custom_root = Path('/tmp/custom-plugin-root')
|
|
||||||
custom = patcher.parse_args([str(custom_root), '--process-only'])
|
|
||||||
assert custom.plugin_root == custom_root
|
|
||||||
assert custom.process_only is True
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
PATCHER_PATH = Path(__file__).resolve().parents[1] / 'fixes' / 'patch_openclaw_weixin_outbound_quotes.py'
|
|
||||||
|
|
||||||
|
|
||||||
def load_patcher():
|
|
||||||
spec = importlib.util.spec_from_file_location('patch_openclaw_weixin_outbound_quotes', PATCHER_PATH)
|
|
||||||
assert spec is not None and spec.loader is not None
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_patch_adds_exact_create_time_and_is_idempotent() -> None:
|
|
||||||
patcher = load_patcher()
|
|
||||||
ts = '''function buildTextMessageReq(params: {
|
|
||||||
to: string;
|
|
||||||
text: string;
|
|
||||||
contextToken?: string;
|
|
||||||
runId?: string;
|
|
||||||
clientId: string;
|
|
||||||
}): SendMessageReq {
|
|
||||||
const { to, text, contextToken, runId, clientId } = params;
|
|
||||||
return { msg: { client_id: clientId, message_type: MessageType.BOT } };
|
|
||||||
}
|
|
||||||
export async function sendMessageWeixin(params: {}): Promise<{ messageId: string }> {
|
|
||||||
const { to, text, opts } = params;
|
|
||||||
const clientId = generateClientId();
|
|
||||||
const req = buildSendMessageReq({
|
|
||||||
payload: {},
|
|
||||||
clientId,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await sendMessageApi({ body: req });
|
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return { messageId: clientId };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Send a single structured MessageItem downstream. */'''
|
|
||||||
js = '''function buildTextMessageReq(params) {
|
|
||||||
const { to, text, contextToken, runId, clientId } = params;
|
|
||||||
return { msg: { client_id: clientId, message_type: MessageType.BOT } };
|
|
||||||
}
|
|
||||||
export async function sendMessageWeixin(params) {
|
|
||||||
const clientId = generateClientId();
|
|
||||||
const req = buildSendMessageReq({
|
|
||||||
payload: {},
|
|
||||||
clientId,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await sendMessageApi({ body: req });
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return { messageId: clientId };
|
|
||||||
}
|
|
||||||
/** Send a single structured MessageItem downstream. */'''
|
|
||||||
|
|
||||||
ts_once = patcher.patch_send_ts(ts)
|
|
||||||
js_once = patcher.patch_send_js(js)
|
|
||||||
assert patcher.patch_send_ts(ts_once) == ts_once
|
|
||||||
assert patcher.patch_send_js(js_once) == js_once
|
|
||||||
for result in (ts_once, js_once):
|
|
||||||
assert 'const createTimeMs = Date.now();' in result
|
|
||||||
assert 'text item missing' in result
|
|
||||||
assert 'req.msg.item_list[0].create_time_ms = createTimeMs' in result
|
|
||||||
assert 'req.msg.create_time_ms = createTimeMs' not in result
|
|
||||||
assert 'return { messageId: clientId, createTimeMs }' in result
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "V1R4C5",
|
|
||||||
"codename": "C5 reliability-security-admin",
|
|
||||||
"status": "active-baseline",
|
|
||||||
"tag": "V1R4C5",
|
|
||||||
"tag_ref": "refs/tags/V1R4C5",
|
|
||||||
"tag_object": "ac9bdacb19ff66cba8046d75358866f0f0fffb82",
|
|
||||||
"previous_version": "V1R4C3",
|
|
||||||
"rollback_version": "V1R4C3",
|
|
||||||
"baseline": "V1R4C3",
|
|
||||||
"release_reason": "reliability, watcher cleanup, user data isolation gate, and admin workbench read-only preparation; memory search disabled",
|
|
||||||
"purpose": "stabilize autonomous operations, enforce user isolation gate, prepare admin workbench read-only for C6",
|
|
||||||
"changes": [
|
|
||||||
"reliability: idle container GC with nixos-activate fallback retention policy and CI reset",
|
|
||||||
"watcher: remove redundant local async watcher, consolidate to server-pushed events",
|
|
||||||
"security: user data isolation gate (read-only admin panel, fb isolation check)",
|
|
||||||
"admin workbench: read-only preparation layout for V1R4C6 admin-search integration",
|
|
||||||
"governance: final owner approval via Owner Gate Web UI"
|
|
||||||
],
|
|
||||||
"artifacts": [
|
|
||||||
"VERSION",
|
|
||||||
"docs/releases/V1R4C5-acceptance-evidence.md",
|
|
||||||
"docs/releases/V1R4C5-final-owner-approval.md"
|
|
||||||
],
|
|
||||||
"validated_actions": [],
|
|
||||||
"secret_plaintext_written": false,
|
|
||||||
"owner_approval_required": true,
|
|
||||||
"owner_notification_channels": ["wechat", "web_ui_audit"],
|
|
||||||
"constraints_original": {
|
|
||||||
"memory_search": "remain disabled",
|
|
||||||
"sr04": "disable read-only visibility slice if RBAC or isolation check fails",
|
|
||||||
"gitea": "enter autonomous safe mode if branch consistency drift detected"
|
|
||||||
},
|
|
||||||
"promoted_at": "2026-07-11T02:17:31+08:00",
|
|
||||||
"promoted_by": "openclaw/admin",
|
|
||||||
"created_at": "2026-07-11T01:40:00+08:00",
|
|
||||||
"author": "openclaw"
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "V1R4C6",
|
|
||||||
"codename": "admin-search-agent-integration",
|
|
||||||
"status": "release-candidate-started",
|
|
||||||
"base_baseline": "V1R4C5",
|
|
||||||
"created_at": "2026-07-11",
|
|
||||||
"created_by": "chatgpt-gitops-bot",
|
|
||||||
"release_type": "controlled-bundled-integration",
|
|
||||||
"promotion_policy": "owner-gated",
|
|
||||||
"owner_decision": {
|
|
||||||
"accepted_scope": true,
|
|
||||||
"requested_delivery": "admin workbench, SR-03, and Agent Platform Integration in one release train",
|
|
||||||
"memory_search_enabled": false,
|
|
||||||
"force_push_allowed": false
|
|
||||||
},
|
|
||||||
"scope": {
|
|
||||||
"included_groups": [
|
|
||||||
{
|
|
||||||
"id": "V1R4C6-A",
|
|
||||||
"name": "Admin Workbench and Portal AIOS Read-Only Rollout",
|
|
||||||
"branches": [
|
|
||||||
"feature/v1r4c4-sr04-admin-workbench-visibility-20260711",
|
|
||||||
"feature/v1r4c4-sr04-portal-aios-visibility-20260710",
|
|
||||||
"patch/user-portal-me-route-diagnostics-20260709",
|
|
||||||
"patch/user-portal-rbac-ops-dashboard",
|
|
||||||
"patch/agent-observability-webui",
|
|
||||||
"patch/web-ui-complete-gitops-dashboard-20260706"
|
|
||||||
],
|
|
||||||
"risk": "medium",
|
|
||||||
"delivery": "administrator-visible task progress, process diagrams, health, agent observability, and portal AIOS entry points",
|
|
||||||
"guardrails": [
|
|
||||||
"read-only first",
|
|
||||||
"RBAC enforced",
|
|
||||||
"empty states explicit",
|
|
||||||
"no cross-user or cross-project data exposure"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "V1R4C6-B",
|
|
||||||
"name": "SR-03 Search Gateway Real Provider Binding",
|
|
||||||
"branches": [
|
|
||||||
"feature/v1r4c4-sr02-search-gateway-resilience-20260710",
|
|
||||||
"patch/search-gateway-governed-general-capability",
|
|
||||||
"patch/gitops-change-archive-audit"
|
|
||||||
],
|
|
||||||
"risk": "high",
|
|
||||||
"delivery": "real provider binding, governed search gateway, resilience, and audit trail",
|
|
||||||
"guardrails": [
|
|
||||||
"user data isolation gate required",
|
|
||||||
"scope filter before ranking",
|
|
||||||
"provenance required",
|
|
||||||
"no global memory/vector index"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "V1R4C6-C",
|
|
||||||
"name": "Agent Platform Integration",
|
|
||||||
"branches": [
|
|
||||||
"patch/agent-event-bus-unified",
|
|
||||||
"patch/agent-gpt-message-plane",
|
|
||||||
"patch/autonomous-controller-dispatch-20260709",
|
|
||||||
"patch/self-evolution-agent-orchestration-policy",
|
|
||||||
"patch/aios-autonomous-orchestrator-architecture-review-v1"
|
|
||||||
],
|
|
||||||
"risk": "high",
|
|
||||||
"delivery": "agent event bus, GPT message plane, dispatch, orchestration policy, and architecture record",
|
|
||||||
"guardrails": [
|
|
||||||
"controller safe-mode respected",
|
|
||||||
"event/message scopes enforced",
|
|
||||||
"owner gate for consequential operations",
|
|
||||||
"observability before autonomous expansion"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"excluded": [
|
|
||||||
"memory search activation",
|
|
||||||
"global vector index",
|
|
||||||
"full cross-session recall",
|
|
||||||
"unscoped search over memory",
|
|
||||||
"bulk merge of test/smoke/auth branches",
|
|
||||||
"Gitea DB mutation",
|
|
||||||
"force-push reconciliation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"acceptance_required": [
|
|
||||||
"getRuntimeStatus active",
|
|
||||||
"getBaseline reports V1R4C5 before integration and V1R4C6 after promotion",
|
|
||||||
"Admin workbench RBAC read-only check passes",
|
|
||||||
"Portal AIOS manifest/dashboard/health routes verified",
|
|
||||||
"Flow/process diagram renders or returns explicit empty state",
|
|
||||||
"Search gateway provider binding returns scoped, provenanced results only",
|
|
||||||
"User data isolation acceptance remains green",
|
|
||||||
"Agent event bus and GPT message plane tests pass",
|
|
||||||
"Autonomous controller dispatch respects safe mode",
|
|
||||||
"Memory search remains disabled"
|
|
||||||
],
|
|
||||||
"deployment_constraints": {
|
|
||||||
"AIOS_MEMORY_ENABLED": false,
|
|
||||||
"memory_search_activation_requires_future_owner_approval": true,
|
|
||||||
"force_push_allowed": false,
|
|
||||||
"gitea_db_mutation_requires_owner_admin_approval": true,
|
|
||||||
"admin_workbench_mode": "read_only_first",
|
|
||||||
"search_provider_scope": "scoped_only"
|
|
||||||
},
|
|
||||||
"rollback_policy": {
|
|
||||||
"admin_workbench": "disable new menu/routes if RBAC or route acceptance fails",
|
|
||||||
"search_gateway": "fallback to disabled provider binding, not memory search",
|
|
||||||
"agent_platform": "disable dispatch expansion and keep event/message plane read-only if safety tests fail",
|
|
||||||
"memory_search": "remain disabled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user