Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d54112aa47 | ||
|
|
3cf9958cb9 | ||
|
|
6b70a33080 | ||
|
|
8d23692320 | ||
|
|
33d3947da6 | ||
|
|
806163b8ba | ||
|
|
13c2a18286 | ||
|
|
b8f10951ea | ||
|
|
2fcf0879c6 | ||
|
|
1136b4186d | ||
|
|
cca73ad52f | ||
|
|
424bf36c03 | ||
|
|
9eb00da0c4 | ||
|
|
9269e81997 | ||
|
|
6642820aa0 | ||
|
|
8374c6b1c7 | ||
|
|
c8a3251834 | ||
|
|
2418f56273 | ||
|
|
297582941f | ||
|
|
958a61cbc4 | ||
|
|
3096212919 | ||
|
|
aa2b091966 | ||
|
|
1a9cb9095e | ||
|
|
0312159168 | ||
|
|
e204a90398 | ||
|
|
d8cf6058e9 | ||
|
|
2aefc19e3d | ||
|
|
28a430066f | ||
|
|
1013aef461 | ||
|
|
bb3210d74f | ||
|
|
745395e93b | ||
|
|
ae49bf0fa3 | ||
|
|
8410cd895e | ||
|
|
dc1642c2e8 | ||
|
|
c75b865710 | ||
|
|
e992b624e0 | ||
|
|
c60e4cd02c | ||
|
|
4b2996ca9f | ||
|
|
086076b74f | ||
|
|
7b2d694272 | ||
|
|
0583060c7c | ||
|
|
20e707fb52 | ||
|
|
6c75ec6bb2 | ||
|
|
e9fefd4c18 | ||
|
|
3ece481c74 | ||
|
|
33177548c3 | ||
|
|
ed1413a05e | ||
|
|
05d1e64228 | ||
|
|
64bbe1b51c | ||
|
|
3c6f2c8c65 | ||
|
|
f6d9bb1510 | ||
|
|
8ca4db3e8c | ||
|
|
d194d3a4d7 |
@@ -0,0 +1,172 @@
|
||||
---
|
||||
authorization_id: TASK-V2R1-RUNTIME-INTEGRATION-P1-20260714
|
||||
task_id: TASK-V2R1-RUNTIME-INTEGRATION-P1-20260714
|
||||
executor: GPT_CODEX
|
||||
status: PASS
|
||||
started_at: 2026-07-14T02:24:00+08:00
|
||||
finished_at: 2026-07-14T02:27:46+08:00
|
||||
repository: /data/aios-gitops/workspace
|
||||
review_branch: review/codex/v2r1-runtime-integration-discovery-20260714
|
||||
review_worktree: /data/aios-gitops/worktrees/v2r1-runtime-integration-discovery-20260714
|
||||
result_commit: SELF
|
||||
result_commit_resolution: The containing commit is resolved by Git from the review branch because embedding its object ID would change the commit identity.
|
||||
parent_commit: 3cf9958cb94614449e093639cbf902216d73447f
|
||||
source_commit: 502ef30642597fb0b70b90ae2e8a0b6af217d80e
|
||||
gate1_commit: 7f5d5d90b3111c35fb00044f385358f528e3344e
|
||||
merge_base: 1a9cb9095e6e76c548c967e03cb7a3e47b85134b
|
||||
main_graph_commits: 21
|
||||
source_graph_commits: 44
|
||||
main_changed_paths: 16
|
||||
source_changed_paths: 42
|
||||
approved_replay_paths: 38
|
||||
excluded_source_paths: 4
|
||||
changed_path_overlap: 0
|
||||
workspace_clean_after_commit: true
|
||||
rollback_target: 3cf9958cb94614449e093639cbf902216d73447f
|
||||
runtime_modified: false
|
||||
tests_modified: false
|
||||
main_modified: false
|
||||
tag_created: false
|
||||
baseline_modified: false
|
||||
service_modified: false
|
||||
deployment_performed: false
|
||||
force_push_used: false
|
||||
risk: HIGH
|
||||
recommendation: Proceed to a separately authorized P2 allowlist replay from exact main; keep tag, baseline, staging and deployment blocked until P4/P5 close every listed blocker.
|
||||
---
|
||||
|
||||
# V2R1 Runtime Integration P1 Discovery Result
|
||||
|
||||
## Decision
|
||||
|
||||
**PASS for discovery only.** The repository graph, path classifications, preservation boundary, exclusion boundary, and safe integration strategy are now explicit. This result does not authorize runtime integration or release activity.
|
||||
|
||||
## Authoritative identities
|
||||
|
||||
| Item | Verified value |
|
||||
|---|---|
|
||||
| Current main | `3cf9958cb94614449e093639cbf902216d73447f` |
|
||||
| V2R1 source | `502ef30642597fb0b70b90ae2e8a0b6af217d80e` |
|
||||
| Gate1 PASS commit | `7f5d5d90b3111c35fb00044f385358f528e3344e` |
|
||||
| Merge base | `1a9cb9095e6e76c548c967e03cb7a3e47b85134b` |
|
||||
| Main graph | `21` commits / `16` changed paths |
|
||||
| Source graph | `44` commits / `42` changed paths |
|
||||
| Changed-path overlap | `0` |
|
||||
|
||||
Gate1 commit is a direct child of source and adds one OpenClaw report. Main already contains the identical accepted report blob `ff31f7ab603d7a7cd07319e6f8422ae464916179`. The Gate1 commit must not be merged again.
|
||||
|
||||
## Exact classification
|
||||
|
||||
### Preserve from main — 16 paths
|
||||
|
||||
```text
|
||||
.agent/results/openclaw/RESULT-V2R1-G1-OPENCLAW-FINAL-EVIDENCE-20260714.md
|
||||
deployments/V1R4C6/backup-result.md
|
||||
deployments/V1R4C6/deployment-result.md
|
||||
deployments/V1R4C6/preflight-result.md
|
||||
deployments/V1R4C6/rollback-result.md
|
||||
deployments/V1R4C6/verification-result.md
|
||||
docs/incidents/WEIXIN_TEXT_IMAGE_CACHE_ISOLATION_20260711.md
|
||||
docs/superpowers/plans/2026-07-12-weixin-quoted-text-failclosed.md
|
||||
docs/superpowers/specs/2026-07-12-weixin-quoted-text-failclosed-design.md
|
||||
fixes/patch_openclaw_weixin_bigint.py
|
||||
fixes/patch_openclaw_weixin_hermes.py
|
||||
fixes/patch_openclaw_weixin_outbound_quotes.py
|
||||
patches/deploy_v1r4c6-production-20260711.branch.json
|
||||
tests/test_weixin_bigint_parser.py
|
||||
tests/test_weixin_image_followup_intent.py
|
||||
tests/test_weixin_outbound_quote_timestamp.py
|
||||
```
|
||||
|
||||
### Replay from source — 38 paths
|
||||
|
||||
```text
|
||||
.gitignore
|
||||
active-runtime/app/aios_v2r1/audit.py
|
||||
active-runtime/app/aios_v2r1/cell_manager.py
|
||||
active-runtime/app/aios_v2r1/channel_adapter.py
|
||||
active-runtime/app/aios_v2r1/cli.py
|
||||
active-runtime/app/aios_v2r1/fleet.py
|
||||
active-runtime/app/aios_v2r1/hermes_fabric.py
|
||||
active-runtime/app/aios_v2r1/hermes_profile.py
|
||||
active-runtime/app/aios_v2r1/identity.py
|
||||
active-runtime/app/aios_v2r1/memory_scope.py
|
||||
active-runtime/app/aios_v2r1/message_ledger.py
|
||||
active-runtime/app/aios_v2r1/router.py
|
||||
active-runtime/app/aios_v2r1/runtime.py
|
||||
active-runtime/app/aios_v2r1/secret_scope.py
|
||||
active-runtime/app/aios_v2r1/session_pool.py
|
||||
active-runtime/app/aios_v2r1/tenant_context.py
|
||||
agents/registry.yaml
|
||||
data/webops.sqlite3 [delete]
|
||||
deploy/v2r1/healthcheck.sh
|
||||
deploy/v2r1/install.sh
|
||||
deploy/v2r1/validate.sh
|
||||
docs/architecture/AIOS_AGENT_AUDIT_PLANE.md
|
||||
docs/architecture/AIOS_V1R5_to_V2R1_Migration_Plan.md
|
||||
docs/architecture/AIOS_V2R1_OpenClaw_Tenant_Cell_Hermes_Shared_Agent_Fabric_Architecture.md
|
||||
openclaw/plugins/aios-tenant-boundary/dist/index.js
|
||||
openclaw/plugins/aios-tenant-boundary/dist/policy.js
|
||||
openclaw/plugins/aios-tenant-boundary/openclaw.plugin.json
|
||||
openclaw/plugins/aios-tenant-boundary/package.json
|
||||
release/v2r1c0-rc1/RELEASE.md
|
||||
tests/integration/test_runtime_ingress_flow.py
|
||||
tests/v2r1/test_acceptance_manifest.md
|
||||
tests/v2r1/test_hermes_contract.md
|
||||
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
|
||||
```
|
||||
|
||||
### Exclude from source — 4 paths
|
||||
|
||||
```text
|
||||
.agent/results/codex/RESULT-V2R1-202607132000-CODEX.md
|
||||
.agent/results/codex/RESULT-V2R1-G1-CODEX-CLEANUP-20260713.md
|
||||
patches/release_v2r1c0-tenant-cell-fabric-20260712.branch.json
|
||||
release/v2r1c0-rc1/STATUS.md
|
||||
```
|
||||
|
||||
The task record contains the category-level counts, exact commit exclusions, and reason for every excluded class.
|
||||
|
||||
## Strategy and invariants
|
||||
|
||||
The approved future strategy is allowlist final-blob replay from source onto exact main. It intentionally avoids merging or cherry-picking historical evidence ancestry. Thirty-seven present paths are restored from source and the tracked `data/webops.sqlite3` path is explicitly deleted.
|
||||
|
||||
The future integration must preserve:
|
||||
|
||||
- exact first parent and rollback target `3cf9958cb94614449e093639cbf902216d73447f`;
|
||||
- all 16 main-only paths above;
|
||||
- Gate1 evidence blob `ff31f7ab603d7a7cd07319e6f8422ae464916179`;
|
||||
- `VERSION` blob `dd4988511ee5957d599098408c89de0c8b758df9` (`V1R4C6`);
|
||||
- baseline registry blob `81b5bbe59397e01d7fff048a5a91c9636ee85696`;
|
||||
- no tag, baseline, service, deployment, or force-push mutation.
|
||||
|
||||
## Baseline and tag evidence
|
||||
|
||||
`V1R4C6` is annotated tag object `356a8a8c1860b94b7d61a456c29228c88e57d963`, peeled release commit `d8cf6058e99f5c7f2dfd2b45a406c0a7edf955b2`. The baseline registry pointer is `e204a90398bf646d46245b3088226749fa056edd`, while the current registry-containing commit is `031215916826f1f0700147242a77878b8514b719`. They are distinct identities. No `V2*` tag exists and P1 creates none.
|
||||
|
||||
## Conflict and workspace evidence
|
||||
|
||||
Current main contains six tracked conflict-marker lines in two files. Source contains zero, and the two corrected source blobs are in the replay allowlist. Final verification must still prove zero markers.
|
||||
|
||||
The canonical workspace had two unrelated untracked controller audit records during discovery. They were not cleaned or included. The P1 review branch uses a separate worktree created from exact main.
|
||||
|
||||
## Blockers
|
||||
|
||||
- Attachment-composition regression is not closed by candidate-bound evidence.
|
||||
- Node plugin tests are missing even though `npm test` references `test/*.test.js`.
|
||||
- Plugin versions differ: `2.1.0-rc.1` and `2.1.0-rc1`.
|
||||
- V2R1 install tooling hardcodes an old release branch.
|
||||
- RC1 `STATUS.md` is stale and excluded.
|
||||
- Static scripts do not prove a real OpenClaw runtime probe or rollback recovery.
|
||||
- Existing release automation masks failures and does not safely update the current baseline schema.
|
||||
|
||||
These blockers prevent release, tagging, baseline promotion, staging, and deployment. They do not invalidate the P1 discovery result.
|
||||
|
||||
## Disposition
|
||||
|
||||
P1 produces evidence only. A separately authorized P2 may implement the exact allowlist replay. P4/P5 must bind tests, security scans, attachment regression, Node plugin verification, real runtime inspection, and rollback evidence to the resulting immutable integration commit before any release action.
|
||||
@@ -0,0 +1,156 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
authorization_id: TASK-V2R1-RUNTIME-INTEGRATION-P1-20260714
|
||||
task_id: TASK-V2R1-RUNTIME-INTEGRATION-P1-20260714
|
||||
executor: GPT_CODEX
|
||||
status: DISCOVERY_COMPLETE
|
||||
started_at: 2026-07-14T02:24:00+08:00
|
||||
finished_at: 2026-07-14T02:27:46+08:00
|
||||
repository: /data/aios-gitops/workspace
|
||||
review_branch: review/codex/v2r1-runtime-integration-discovery-20260714
|
||||
review_worktree: /data/aios-gitops/worktrees/v2r1-runtime-integration-discovery-20260714
|
||||
main_commit: 3cf9958cb94614449e093639cbf902216d73447f
|
||||
source_commit: 502ef30642597fb0b70b90ae2e8a0b6af217d80e
|
||||
gate1_commit: 7f5d5d90b3111c35fb00044f385358f528e3344e
|
||||
merge_base: 1a9cb9095e6e76c548c967e03cb7a3e47b85134b
|
||||
result_path: .agent/results/codex/RESULT-V2R1-RUNTIME-INTEGRATION-P1-20260714.md
|
||||
write_scope:
|
||||
- .agent/tasks/codex/TASK-V2R1-RUNTIME-INTEGRATION-P1-20260714.md
|
||||
- .agent/results/codex/RESULT-V2R1-RUNTIME-INTEGRATION-P1-20260714.md
|
||||
forbidden_actions:
|
||||
- modify_runtime_or_tests
|
||||
- merge_or_push_main
|
||||
- create_or_move_tag
|
||||
- update_baseline
|
||||
- change_service_or_deploy
|
||||
---
|
||||
|
||||
# V2R1 Runtime Integration P1 Discovery Task
|
||||
|
||||
## Objective
|
||||
|
||||
Produce a reproducible, evidence-only discovery record for integrating the approved V2R1 source tree onto current main without importing divergent historical evidence ancestry. P1 does not implement the integration.
|
||||
|
||||
## Locked graph
|
||||
|
||||
- Authoritative main: `3cf9958cb94614449e093639cbf902216d73447f`.
|
||||
- Approved V2R1 source: `502ef30642597fb0b70b90ae2e8a0b6af217d80e`.
|
||||
- Accepted Gate1 evidence commit: `7f5d5d90b3111c35fb00044f385358f528e3344e`.
|
||||
- Merge base: `1a9cb9095e6e76c548c967e03cb7a3e47b85134b`.
|
||||
- Main-only graph: `21` commits and `16` changed paths.
|
||||
- Source-only graph: `44` commits and `42` changed paths.
|
||||
- Main/source changed-path overlap: `0` paths.
|
||||
|
||||
## Main paths that must be preserved exactly — 16
|
||||
|
||||
1. `.agent/results/openclaw/RESULT-V2R1-G1-OPENCLAW-FINAL-EVIDENCE-20260714.md`
|
||||
2. `deployments/V1R4C6/backup-result.md`
|
||||
3. `deployments/V1R4C6/deployment-result.md`
|
||||
4. `deployments/V1R4C6/preflight-result.md`
|
||||
5. `deployments/V1R4C6/rollback-result.md`
|
||||
6. `deployments/V1R4C6/verification-result.md`
|
||||
7. `docs/incidents/WEIXIN_TEXT_IMAGE_CACHE_ISOLATION_20260711.md`
|
||||
8. `docs/superpowers/plans/2026-07-12-weixin-quoted-text-failclosed.md`
|
||||
9. `docs/superpowers/specs/2026-07-12-weixin-quoted-text-failclosed-design.md`
|
||||
10. `fixes/patch_openclaw_weixin_bigint.py`
|
||||
11. `fixes/patch_openclaw_weixin_hermes.py`
|
||||
12. `fixes/patch_openclaw_weixin_outbound_quotes.py`
|
||||
13. `patches/deploy_v1r4c6-production-20260711.branch.json`
|
||||
14. `tests/test_weixin_bigint_parser.py`
|
||||
15. `tests/test_weixin_image_followup_intent.py`
|
||||
16. `tests/test_weixin_outbound_quote_timestamp.py`
|
||||
|
||||
Additional immutable main invariants:
|
||||
|
||||
- `VERSION` remains `V1R4C6`, blob `dd4988511ee5957d599098408c89de0c8b758df9`.
|
||||
- `baseline/current.json` remains blob `81b5bbe59397e01d7fff048a5a91c9636ee85696`.
|
||||
- Accepted Gate1 evidence remains blob `ff31f7ab603d7a7cd07319e6f8422ae464916179`.
|
||||
- The future integration commit must have `3cf9958cb94614449e093639cbf902216d73447f` as its first-parent/rollback target.
|
||||
|
||||
## Approved source-tree allowlist — 38 paths
|
||||
|
||||
### Python V2R1 runtime — 15
|
||||
|
||||
1. `active-runtime/app/aios_v2r1/audit.py`
|
||||
2. `active-runtime/app/aios_v2r1/cell_manager.py`
|
||||
3. `active-runtime/app/aios_v2r1/channel_adapter.py`
|
||||
4. `active-runtime/app/aios_v2r1/cli.py`
|
||||
5. `active-runtime/app/aios_v2r1/fleet.py`
|
||||
6. `active-runtime/app/aios_v2r1/hermes_fabric.py`
|
||||
7. `active-runtime/app/aios_v2r1/hermes_profile.py`
|
||||
8. `active-runtime/app/aios_v2r1/identity.py`
|
||||
9. `active-runtime/app/aios_v2r1/memory_scope.py`
|
||||
10. `active-runtime/app/aios_v2r1/message_ledger.py`
|
||||
11. `active-runtime/app/aios_v2r1/router.py`
|
||||
12. `active-runtime/app/aios_v2r1/runtime.py`
|
||||
13. `active-runtime/app/aios_v2r1/secret_scope.py`
|
||||
14. `active-runtime/app/aios_v2r1/session_pool.py`
|
||||
15. `active-runtime/app/aios_v2r1/tenant_context.py`
|
||||
|
||||
### OpenClaw plugin — 4
|
||||
|
||||
1. `openclaw/plugins/aios-tenant-boundary/dist/index.js`
|
||||
2. `openclaw/plugins/aios-tenant-boundary/dist/policy.js`
|
||||
3. `openclaw/plugins/aios-tenant-boundary/openclaw.plugin.json`
|
||||
4. `openclaw/plugins/aios-tenant-boundary/package.json`
|
||||
|
||||
### Tests and contracts — 9
|
||||
|
||||
1. `tests/integration/test_runtime_ingress_flow.py`
|
||||
2. `tests/v2r1/test_acceptance_manifest.md`
|
||||
3. `tests/v2r1/test_hermes_contract.md`
|
||||
4. `tests/v2r1/test_hermes_fabric.py`
|
||||
5. `tests/v2r1/test_message_ledger_append.py`
|
||||
6. `tests/v2r1/test_router_isolation.py`
|
||||
7. `tests/v2r1/test_runtime_identity_binding.py`
|
||||
8. `tests/v2r1/test_runtime_wiring.py`
|
||||
9. `tests/v2r1/test_tenant_context.py`
|
||||
|
||||
### Static deployment tooling — 3
|
||||
|
||||
1. `deploy/v2r1/healthcheck.sh`
|
||||
2. `deploy/v2r1/install.sh`
|
||||
3. `deploy/v2r1/validate.sh`
|
||||
|
||||
### Architecture and release documents — 4
|
||||
|
||||
1. `docs/architecture/AIOS_AGENT_AUDIT_PLANE.md`
|
||||
2. `docs/architecture/AIOS_V1R5_to_V2R1_Migration_Plan.md`
|
||||
3. `docs/architecture/AIOS_V2R1_OpenClaw_Tenant_Cell_Hermes_Shared_Agent_Fabric_Architecture.md`
|
||||
4. `release/v2r1c0-rc1/RELEASE.md`
|
||||
|
||||
### Cleanup and governance — 3
|
||||
|
||||
1. `.gitignore`
|
||||
2. `agents/registry.yaml`
|
||||
3. `data/webops.sqlite3` — explicit tracked-file deletion, not a restore operation.
|
||||
|
||||
The replay operation restores the `37` present paths from source and explicitly removes `data/webops.sqlite3`. No directory-wide restore is allowed outside this list.
|
||||
|
||||
## Excluded source paths — 4
|
||||
|
||||
1. `.agent/results/codex/RESULT-V2R1-202607132000-CODEX.md` — historical branch evidence.
|
||||
2. `.agent/results/codex/RESULT-V2R1-G1-CODEX-CLEANUP-20260713.md` — historical branch evidence.
|
||||
3. `patches/release_v2r1c0-tenant-cell-fabric-20260712.branch.json` — stale branch-specific patch metadata.
|
||||
4. `release/v2r1c0-rc1/STATUS.md` — stale status: references tree `6569065` and contradicts later test/rollback evidence.
|
||||
|
||||
## Excluded commit ancestry
|
||||
|
||||
- Do not merge source commit ancestry and do not cherry-pick any of its `44` source-only commits. Replay only the approved final blobs.
|
||||
- Do not merge Gate1 commit `7f5d5d90b3111c35fb00044f385358f528e3344e`; main already contains its accepted evidence blob.
|
||||
- Do not replay result-only source commits `d6ba7e8fa859407ec55acf49b5468bba5eac96da`, `b9e1f9db66eb08282b7b690199e56461d28dd76d`, or `502ef30642597fb0b70b90ae2e8a0b6af217d80e` as commits.
|
||||
- Explicitly exclude rejected OpenClaw candidates `7a2f16ee72e944737a9f6995e511ef176413ae6b`, `b1d3439ee9e66b5eed1c94595a22bc630d893b6e`, `38320044b54c172c28877a81f291478267dd7b9e`, `7fd63b55f27b23658ea26e2a39d949c7b1e91dda`, and `71239b4d187e683e05fd22fc9d5df8ae324e3390`.
|
||||
- Prior Codex final-verification commit `0eebcf0b72f06c02205fdb6e300ceb5b3a5647a7` is not an integration input.
|
||||
|
||||
## Strategy decision
|
||||
|
||||
Use allowlist file replay from the immutable source tree, not a historical merge or commit-range cherry-pick. The two branches diverged for `21` and `44` commits after the merge base. Historical ancestry includes obsolete result reports and branch metadata, while main contains V1R4C6 incident fixes and the already accepted Gate1 evidence. Final-blob replay makes the intended runtime delta explicit and keeps those histories out of the future integration commit.
|
||||
|
||||
P2 must start from exact main, revalidate all refs, restore only the 37 present allowlist paths from source, remove the one tracked SQLite path, and stop if any additional path appears.
|
||||
|
||||
## Conflict preview
|
||||
|
||||
- Current main has `6` tracked conflict-marker lines across `agents/registry.yaml` and `docs/architecture/AIOS_AGENT_AUDIT_PLANE.md`.
|
||||
- Source has `0` tracked conflict-marker lines.
|
||||
- Both corrected source blobs are in the allowlist; the final tree must scan to zero markers.
|
||||
- The changed-path overlap between main and source deltas is zero, but this does not waive blob-preservation checks.
|
||||
|
||||
## Baseline and tag distinction
|
||||
|
||||
- Annotated tag object `356a8a8c1860b94b7d61a456c29228c88e57d963` is `V1R4C6` and peels to release commit `d8cf6058e99f5c7f2dfd2b45a406c0a7edf955b2`.
|
||||
- `baseline/current.json` records baseline registry pointer `e204a90398bf646d46245b3088226749fa056edd`; its latest modifying commit is `031215916826f1f0700147242a77878b8514b719`.
|
||||
- The tag object, peeled release commit, registry pointer, and registry-containing commit are separate audit identities.
|
||||
- No V2 tag exists. Future approved namespace is `refs/tags/V2R1C0`; P1 does not create it.
|
||||
|
||||
## Current workspace pollution
|
||||
|
||||
At discovery time, canonical workspace `/data/aios-gitops/workspace` contained two unrelated untracked controller records:
|
||||
|
||||
- `audit/controller-runs/c-1783966122.json`
|
||||
- `audit/controller-runs/c-1783966722.json`
|
||||
|
||||
They are excluded. All P1 writes occur in the isolated worktree, and no canonical workspace cleanup is authorized.
|
||||
|
||||
## Known blockers
|
||||
|
||||
1. Attachment-composition regression lacks a closed, candidate-bound regression result.
|
||||
2. Plugin `npm test` targets `test/*.test.js`, but source tracks no Node test files.
|
||||
3. Plugin package version `2.1.0-rc.1` differs from manifest version `2.1.0-rc1`.
|
||||
4. `deploy/v2r1/install.sh` hardcodes release branch `release/v2r1c0-tenant-cell-fabric-20260712`.
|
||||
5. `release/v2r1c0-rc1/STATUS.md` is stale and is excluded rather than silently rewritten.
|
||||
6. `validate.sh --install/--rollback` and `healthcheck.sh` do not prove a real OpenClaw runtime or rollback path.
|
||||
7. `scripts/release_version.sh` masks failures and does not match the active baseline registry schema; it must not be used.
|
||||
|
||||
## Exit criteria
|
||||
|
||||
P1 passes when the two evidence files are the only commit delta, the commit parent is exact main, the isolated worktree is clean after commit, and the review branch is pushed without force. Runtime integration, main update, tag, baseline promotion, and deployment remain separate blocked phases.
|
||||
@@ -0,0 +1,79 @@
|
||||
"""HTTP handler and display data for architecture owner gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .architecture_owner_gate import ArchitectureOwnerGate, ArchitectureOwnerApprovalRequest
|
||||
|
||||
|
||||
gate = ArchitectureOwnerGate()
|
||||
|
||||
# In-memory store for pending approval requests
|
||||
_pending_requests: dict[str, dict[str, Any]] = {}
|
||||
_decisions: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def create_owner_approval_request(change: dict[str, Any], architect_decision: dict[str, Any]) -> dict[str, Any]:
|
||||
req = gate.create_request(change, architect_decision)
|
||||
d = asdict(req)
|
||||
_pending_requests[req.change_id] = d
|
||||
return d
|
||||
|
||||
|
||||
def approve_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
|
||||
req = _pending_requests.get(change_id)
|
||||
if not req:
|
||||
return {"error": f"no pending request for change_id={change_id}"}
|
||||
decision = asdict(
|
||||
ArchitectureOwnerApprovalRequest.__bases__[1].__new__(type("_ArchitectureOwnerDecision", (), {})) # noqa
|
||||
)
|
||||
try:
|
||||
from .architecture_owner_gate import ArchitectureOwnerDecision
|
||||
dec = ArchitectureOwnerDecision(
|
||||
request_id=req["request_id"],
|
||||
change_id=change_id,
|
||||
decision="approved",
|
||||
decided_by=decided_by,
|
||||
reason=reason,
|
||||
)
|
||||
d = asdict(dec)
|
||||
_decisions[change_id] = d
|
||||
# remove from pending
|
||||
_pending_requests.pop(change_id, None)
|
||||
return {"allowed": True, "decision": d}
|
||||
except Exception as exc:
|
||||
return {"error": str(exc)}
|
||||
|
||||
|
||||
def reject_request(change_id: str, decided_by: str = "owner", reason: str = "") -> dict[str, Any]:
|
||||
from .architecture_owner_gate import ArchitectureOwnerDecision
|
||||
req = _pending_requests.get(change_id)
|
||||
if not req:
|
||||
return {"error": f"no pending request for change_id={change_id}"}
|
||||
try:
|
||||
dec = ArchitectureOwnerDecision(
|
||||
request_id=req["request_id"],
|
||||
change_id=change_id,
|
||||
decision="rejected",
|
||||
decided_by=decided_by,
|
||||
reason=reason,
|
||||
)
|
||||
d = asdict(dec)
|
||||
_decisions[change_id] = d
|
||||
_pending_requests.pop(change_id, None)
|
||||
return {"allowed": False, "decision": d}
|
||||
except Exception as exc:
|
||||
return {"error": str(exc)}
|
||||
|
||||
|
||||
def get_owner_gate_overview() -> dict[str, Any]:
|
||||
return {
|
||||
"pending_requests": list(_pending_requests.values()),
|
||||
"decisions": list(_decisions.values()),
|
||||
"pending_count": len(_pending_requests),
|
||||
"approved_count": sum(1 for d in _decisions.values() if d.get("decision") == "approved"),
|
||||
"rejected_count": sum(1 for d in _decisions.values() if d.get("decision") == "rejected"),
|
||||
}
|
||||
@@ -58,6 +58,8 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
||||
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
|
||||
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
|
||||
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
|
||||
<li onclick="showPanel('arch-owner')"><i>🔐</i> Owner Gate</li>
|
||||
<li onclick="showPanel('arch-approve')"><i>✓</i> Approve Changes</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main">
|
||||
@@ -90,6 +92,14 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
||||
</div>
|
||||
<div id="panel-agents" class="panel" style="display:none;">
|
||||
<div class="main-header"><h2>Recent Agent Executions</h2></div>
|
||||
<div id="panel-arch-owner" class="panel" style="display:none;">
|
||||
<div class="main-header"><h2>🔐 Architecture Owner Gate</h2></div>
|
||||
<div id="arch-owner-content">Loading...</div>
|
||||
</div>
|
||||
<div id="panel-arch-approve" class="panel" style="display:none;">
|
||||
<div class="main-header"><h2>✓ Approve Architecture Changes</h2></div>
|
||||
<div id="arch-approve-content">Loading...</div>
|
||||
</div>
|
||||
<div id="agents-list">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,3 +107,159 @@ pre { white-space: pre-wrap; word-break: break-word; }
|
||||
<script>
|
||||
async function fetchData() {
|
||||
const r = await fetch('/admin/ai-evolution/api/data');
|
||||
const data = await r.json();
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('error').style.display = 'none';
|
||||
document.getElementById('content').style.display = '';
|
||||
|
||||
const hs = document.getElementById('header-status');
|
||||
hs.className = 'status-badge ' + (data.healthy ? 'ok' : 'fail');
|
||||
hs.textContent = data.healthy ? '▪ operational' : '⚠ degraded';
|
||||
|
||||
document.getElementById('base').textContent = data.baseline || '-';
|
||||
document.getElementById('branch').textContent = data.main_branch || 'main';
|
||||
document.getElementById('uptime').textContent = data.uptime || '-';
|
||||
document.getElementById('bus-status').textContent = data.bus ? JSON.stringify(data.bus, null, 2) : 'no event bus data';
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
const el = document.getElementById('versions-list');
|
||||
el.innerHTML = '<div class="loading">Loading versions...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/versions');
|
||||
const data = await r.json();
|
||||
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
|
||||
let html = '<div class="grid">';
|
||||
for (const v of data.versions || []) {
|
||||
html += '<div class="card"><div class="card-title">' + v.tag + '</div><div class="card-value">' + (v.version || v.tag) + '</div></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load versions: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCommands() {
|
||||
const el = document.getElementById('commands-list');
|
||||
el.innerHTML = '<div class="loading">Loading commands...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/data');
|
||||
const data = await r.json();
|
||||
let cmds = data.commands || [];
|
||||
let html = '<div style="display:flex;gap:24px;margin-bottom:16px;">';
|
||||
html += '<div><div class="card"><div class="card-title">Total</div><div class="card-value">' + cmds.length + '</div></div></div>';
|
||||
html += '<div><div class="card"><div class="card-title">By Category</div>';
|
||||
const cats = {};
|
||||
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
|
||||
for (const [k, v] of Object.entries(cats)) html += '<div style="font-size:14px;color:#8b949e;margin:4px 0;">' + k + ': ' + v + '</div>';
|
||||
html += '</div></div></div>';
|
||||
for (const cmd of cmds.slice().reverse()) {
|
||||
html += '<div class="command-item"><div class="meta">' + cmd.category + ' | ' + cmd.filename + '</div><div class="name">' + cmd.title + '</div><div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + cmd.summary + '</div></div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load commands: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAgents() {
|
||||
const el = document.getElementById('agents-list');
|
||||
el.innerHTML = '<div class="loading">Loading agent runs...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/agents');
|
||||
const data = await r.json();
|
||||
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
|
||||
let html = '';
|
||||
for (const agent of data.agents || []) {
|
||||
html += '<div class="agent-item"><div class="agent-title">' + agent.name + ' <span class="state state-' + (agent.status === 'running' ? 'running' : (agent.status === 'ok' ? 'completed' : 'failed')) + '">' + agent.status + '</span></div><div class="agent-meta">' + (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-') + '</div></div>';
|
||||
}
|
||||
el.innerHTML = html || '<div style="padding:24px;color:#8b949e;">No agent runs recorded yet.</div>';
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load agents: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function showPanel(name) {
|
||||
|
||||
async function loadOwnerGate() {
|
||||
const el = document.getElementById('arch-owner-content');
|
||||
el.innerHTML = '<div class="loading">Loading owner gate data...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/owner-gate');
|
||||
const data = await r.json();
|
||||
let html = '<div class="row">';
|
||||
html += '<div class="col"><div class="card"><div class="card-title">Pending Requests</div><div class="card-value">' + data.pending_count + '</div></div></div>';
|
||||
html += '<div class="col"><div class="card"><div class="card-title">Approved</div><div class="card-value">' + data.approved_count + '</div></div></div>';
|
||||
html += '<div class="col"><div class="card"><div class="card-title">Rejected</div><div class="card-value">' + data.rejected_count + '</div></div></div>';
|
||||
html += '</div>';
|
||||
html += '<div class="card"><div class="card-title">Pending Approval Requests</div>';
|
||||
if (data.pending_requests && data.pending_requests.length) {
|
||||
for (const req of data.pending_requests) {
|
||||
html += '<div class="command-item">';
|
||||
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
|
||||
html += '<div class="meta">ID: ' + req.change_id + ' | Risk: ' + req.risk + '</div>';
|
||||
html += '<div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + (req.reason || '') + '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
} else {
|
||||
html += '<div style="color:#8b949e;padding:12px;">No pending requests.</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApprove() {
|
||||
const el = document.getElementById('arch-approve-content');
|
||||
el.innerHTML = '<div class="loading">Loading pending approvals...</div>';
|
||||
try {
|
||||
const r = await fetch('/admin/ai-evolution/api/owner-gate');
|
||||
const data = await r.json();
|
||||
let html = '';
|
||||
if (data.pending_requests && data.pending_requests.length) {
|
||||
html += '<div class="card"><div class="card-title">Pending changes requiring your approval</div>';
|
||||
for (const req of data.pending_requests) {
|
||||
html += '<div class="command-item">';
|
||||
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
|
||||
html += '<div class="meta">Risk: <strong>' + req.risk + '</strong></div>';
|
||||
html += '<div style="font-size:13px;color:#c9d1d9;margin:8px 0;">' + (req.reason || '') + '</div>';
|
||||
html += '<div style="display:flex;gap:8px;">';
|
||||
html += '<button onclick="ownerAction.classList.add('\'approved\');this.disabled=true" style="padding:6px 16px;background:#238636;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">✓ Approve</button>';
|
||||
html += '</div></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div class="card"><div style="padding:24px;text-align:center;color:#8b949e;">✅ All changes processed. No pending approval requests.</div></div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
|
||||
document.getElementById('panel-' + name).style.display = '';
|
||||
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
|
||||
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4}[name] || 1) + ')').classList.add('active');
|
||||
if (name === 'overview') {
|
||||
document.getElementById('panel-overview').innerHTML = '<div class="main-header"><h2>System Overview</h2><div id="header-status" class="status-badge">Loading...</div></div><div id="loading" class="loading">Loading...</div><div id="error" class="error" style="display:none;"></div><div id="content"><div class="row"><div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div></div><div class="card" style="margin-top:16px"><div class="card-title">Event Bus Status</div><pre id="bus-status">-</pre></div></div>';
|
||||
fetchData();
|
||||
}
|
||||
const loaders = {
|
||||
'arch-owner': loadOwnerGate,
|
||||
'arch-approve': loadApprove,
|
||||
versions: loadVersions,
|
||||
commands: loadCommands,
|
||||
agents: loadAgents,
|
||||
};
|
||||
const fn = loaders[name];
|
||||
if (fn) fn();
|
||||
}
|
||||
|
||||
fetchData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-1
@@ -1 +1,13 @@
|
||||
{"current_baseline":"V1R3C1","promoted_at":"2026-07-08T14:31:00+08:00","previous_baseline":"V1R2C3"}
|
||||
{
|
||||
"current_baseline": "V1R4C6",
|
||||
"previous_baseline": "V1R4C5",
|
||||
"promoted_at": "2026-07-11T21:00:00+08:00",
|
||||
"git": "e204a90398bf646d46245b3088226749fa056edd",
|
||||
"promoted_by": "openclaw/admin",
|
||||
"promotion_reason": "V1R4C6 admin search agent integration, search gateway, agent platform, VERSION bump, merge tests deploy pipeline — not yet deployed to production",
|
||||
"constraints": {
|
||||
"memory_search_enabled": false,
|
||||
"force_push_allowed": false,
|
||||
"gitea_db_mutation_requires_owner_approval": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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
|
||||
@@ -0,0 +1,113 @@
|
||||
# 微信文本-图片缓存隔离事故报告
|
||||
|
||||
## 结论
|
||||
|
||||
本次事故已经按最新备份链路回滚并完成第二轮评审修复。`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,212 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,107 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,137 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,83 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,92 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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`.
|
||||
@@ -0,0 +1,152 @@
|
||||
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
|
||||
@@ -0,0 +1,112 @@
|
||||
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())
|
||||
Executable
+2726
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
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())
|
||||
@@ -0,0 +1,429 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,217 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "deploy/v1r4c6-production-20260711",
|
||||
"base": "main",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-11T13:10:51Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -25,8 +25,11 @@ class SuccessProvider:
|
||||
|
||||
|
||||
def test_single_provider_failure_is_not_terminal():
|
||||
gateway = SearchGateway(providers=[FailingProvider(), SuccessProvider()])
|
||||
result = gateway.search(SearchRequest(query="policy update", requester="test", intent="policy_regulation"))
|
||||
gateway = SearchGateway(providers=[SuccessProvider(), FailingProvider()])
|
||||
result = gateway.search(SearchRequest(
|
||||
query="policy update", requester="test", intent="policy_regulation",
|
||||
group_profile={"provider_order": ["general_web", "official_site"]},
|
||||
))
|
||||
assert result.status == "found"
|
||||
assert any(a.status == "failed" for a in result.attempts)
|
||||
assert result.confidence == "high"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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',
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
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
|
||||
@@ -0,0 +1,75 @@
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"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