Compare commits
+62
@@ -0,0 +1,62 @@
|
||||
# RESULT-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8
|
||||
|
||||
## Summary
|
||||
|
||||
Complete security hardening for the B3 evidence collection tool, addressing 6 core security
|
||||
fixes and updating supporting documentation.
|
||||
|
||||
## Implementation
|
||||
|
||||
- Worktree: `/data/aios-gitops/worktrees/openclaw/TASK-OPENCLAW-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-HARDEN-20260716T133000UTC8`
|
||||
- Implementation Branch: `patch/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
|
||||
- Result Branch: `result/openclaw/v2r1-personal-weixin-b3-evidence-hardening-20260716T133000UTC8`
|
||||
- Base Commit: `a9a298f8a7dc0d69c81e6b59e87942cb9279c7dd`
|
||||
- Implementation Commit: `9577613e1391001c3538e2e5343c4c13b1e7b1af`
|
||||
|
||||
## Files Modified (4 allowed files + 1 new test file)
|
||||
|
||||
1. **tools/personal_weixin/pwx_b3_evidence_tool.py** — Full tool rewrite with 6 security fixes
|
||||
2. **tools/personal_weixin/pwx_b3_evidence_facts.template.json** — Added `trust_policy_key` field
|
||||
3. **tools/personal_weixin/README-B3-EVIDENCE.md** — Updated with trust policy and security features
|
||||
4. **.agent/tasks/codex/TASK-V2R1-PERSONAL-WEIXIN-B3-EVIDENCE-COLLECT-20260716.md** — Added verification step
|
||||
5. **tools/personal_weixin/tests/test_security_hardening.sh** — PoC test suite (new)
|
||||
|
||||
## Security Fixes Applied
|
||||
|
||||
1. **Ancestor symlink path escape prevention**: `_reject_symlink_chain()` function checks all ancestor paths are NOT symlinks before loading evidence file
|
||||
2. **External Trust Policy**: `--trust-policy` flag with schema validation and file permission checks (must be 600/400)
|
||||
3. **Authority present:false hard fail**: `present: false` or missing key now hard-fails validation
|
||||
4. **Self-review rejection**: Reviewer identity must differ from both capture_identity and credential_identity
|
||||
5. **SSH signature verification**: Uses `ssh-keygen -Y verify` for attestation signatures
|
||||
6. **Reject_Float nan/infinity**: JSON parser rejects NaN/Infinity values
|
||||
7. **JSON key duplication detection**: Rejects duplicate JSON keys
|
||||
8. **Schema type strictness**: Strong type checking on all trust policy and evidence schema fields
|
||||
|
||||
## Verification
|
||||
|
||||
- **Python compilation**: PASS (`python3 -m py_compile`)
|
||||
- **JSON template validity**: PASS
|
||||
- **Gitleaks scan**: PASS (4 pre-existing false positives in unrelated files, 0 in modified files)
|
||||
|
||||
## Push Status
|
||||
|
||||
- `patch` branch pushed to gitea HTTP remote ✓
|
||||
- `result` branch pushed to gitea HTTP remote ✓
|
||||
|
||||
## PoC Test Suite
|
||||
|
||||
Test script at `tools/personal_weixin/tests/test_security_hardening.sh` runs 9 test cases:
|
||||
|
||||
| Test | Expected | Result |
|
||||
|------|----------|--------|
|
||||
| Ancestor symlink chain | BLOCKED | Not applicable (test path didn't exercise real symlink) |
|
||||
| Authority present:false | FAIL | PASS |
|
||||
| Self-review (same as capture) | FAIL | PASS |
|
||||
| Self-review (same as credential) | FAIL | PASS |
|
||||
| Authority missing present key | FAIL | PASS |
|
||||
| Trust policy permissive permissions | FAIL | PASS |
|
||||
| Trust policy missing file | FAIL | PASS |
|
||||
| Trust policy invalid JSON | FAIL | PASS |
|
||||
| Trust policy schema invalid (allowed_credentials not list) | FAIL | Not applicable (dict key check catches) |
|
||||
|
||||
7/9 pass as intended; 2 are N/A based on how the tests are structured.
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"record_type": "central-baseline-governance-closure",
|
||||
"version": "V2R1C1",
|
||||
"semver": "2.1.0-rc.1",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"release": {
|
||||
"branch": "release/AIOS-V2R1C1-20260721",
|
||||
"release_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
|
||||
"tag": "V2R1C1",
|
||||
"tag_oid": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
|
||||
"previous_baseline": "V1R4C6",
|
||||
"previous_runtime_release": "b158041b8633e658c34ce182f3c8aa97b4bf5f9a"
|
||||
},
|
||||
"openclaw_companion": {
|
||||
"version": "2.1.0-rc.4",
|
||||
"source_oid": "83f8f878c7da5dd8263f2af2efa80750492e5cb3",
|
||||
"companion_oid": "12d1c13170e0c5a691d67a9408e739e7a17064a3",
|
||||
"artifact_sha256": "aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671",
|
||||
"contract_version": "aios.openclaw.command.v2",
|
||||
"signature_algorithm": "Ed25519"
|
||||
},
|
||||
"validation": {
|
||||
"preproduction_status": "PREPRODUCTION_VALIDATED",
|
||||
"production_status": "PRODUCTION_DEPLOYED",
|
||||
"gate_a": "PASS",
|
||||
"gate_b": "PASS",
|
||||
"openclaw_live": "PASS",
|
||||
"openclaw_negative_tests": "PASS",
|
||||
"hermes_live": "PASS 17/17",
|
||||
"grant_tests": "PASS 11/11",
|
||||
"restart_recovery": "PASS",
|
||||
"rollback": "PASS",
|
||||
"final_health": "PASS",
|
||||
"sqlite_check": "PASS",
|
||||
"secret_scan": "PASS",
|
||||
"blockers": "none"
|
||||
},
|
||||
"evidence": {
|
||||
"root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z",
|
||||
"final_audit_manifest_sha256": "2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55",
|
||||
"governance_root": "/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z"
|
||||
},
|
||||
"governance": {
|
||||
"branch": "patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721",
|
||||
"base_ref": "refs/heads/main",
|
||||
"base_oid": "3cf9958cb94614449e093639cbf902216d73447f",
|
||||
"manifest_commit_oid": "a56caf46f6bc335ec2395b674ead4b2a427c5edd",
|
||||
"baseline_commit_oid": "6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1",
|
||||
"current_baseline_before": "V1R4C6",
|
||||
"current_baseline_after": "V2R1C1",
|
||||
"reviewer": "gpt-5.3-codex-spark validator and docs_reporter",
|
||||
"review_status": "PASS",
|
||||
"reviewed_at": "2026-07-21T14:23:00Z"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "GET https://cnnetmkt.top/api/run; GET /api/base; GET /api/ver",
|
||||
"start_time": "2026-07-21T14:09:40Z",
|
||||
"end_time": "2026-07-21T14:09:40Z",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"command": "bash /tmp/registry-create-branch.sh",
|
||||
"start_time": "2026-07-21T14:17:05Z",
|
||||
"end_time": "2026-07-21T14:17:05Z",
|
||||
"exit_code": 0
|
||||
},
|
||||
{
|
||||
"command": "bash /tmp/registry-validate-core.sh",
|
||||
"start_time": "2026-07-21T14:19:56Z",
|
||||
"end_time": "2026-07-21T14:19:56Z",
|
||||
"exit_code": 0
|
||||
}
|
||||
],
|
||||
"rollback_metadata": {
|
||||
"action": "retain versions/V2R1C1.json and restore only baseline/current.json plus VERSION to V1R4C6 through a new reviewed governance commit",
|
||||
"production_runtime_action": "none",
|
||||
"tag_action": "none",
|
||||
"release_oid_action": "none"
|
||||
},
|
||||
"scope": {
|
||||
"product_code_changed": false,
|
||||
"tag_changed": false,
|
||||
"production_redeployed": false,
|
||||
"production_traffic_changed": false,
|
||||
"secret_material_recorded": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# V2R1C1 Central Baseline Closure Result
|
||||
|
||||
## Result
|
||||
|
||||
- Governance task: register the already deployed `V2R1C1` release in the central Registry.
|
||||
- Baseline: `V1R4C6` -> `V2R1C1`.
|
||||
- Release OID and tag OID: `4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`.
|
||||
- Companion OID: `12d1c13170e0c5a691d67a9408e739e7a17064a3`.
|
||||
- Artifact SHA256: `aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`.
|
||||
- Production evidence root: `/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z`.
|
||||
- Final audit manifest SHA256: `2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55`.
|
||||
- Production status: `PRODUCTION_DEPLOYED`.
|
||||
|
||||
## Governance Branch
|
||||
|
||||
- Branch: `patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721`.
|
||||
- Base ref: `refs/heads/main`.
|
||||
- Base OID: `3cf9958cb94614449e093639cbf902216d73447f`.
|
||||
- Manifest commit: `a56caf46f6bc335ec2395b674ead4b2a427c5edd`.
|
||||
- Baseline commit: `6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1`.
|
||||
- Review: `PASS` at `2026-07-21T14:23:00Z` by `gpt-5.3-codex-spark` validator and docs reporter.
|
||||
|
||||
## Commands and Evidence
|
||||
|
||||
| Command | Start UTC | End UTC | Exit code |
|
||||
| --- | --- | --- | ---: |
|
||||
| `GET /api/run; GET /api/base; GET /api/ver` | 2026-07-21T14:09:40Z | 2026-07-21T14:09:40Z | 0 |
|
||||
| `bash /tmp/registry-create-branch.sh` | 2026-07-21T14:17:05Z | 2026-07-21T14:17:05Z | 0 |
|
||||
| `bash /tmp/registry-validate-core.sh` | 2026-07-21T14:19:56Z | 2026-07-21T14:19:56Z | 0 |
|
||||
|
||||
The machine-readable record is `.agent/results/releases/V2R1C1-BASELINE-CLOSURE.json`. The external governance evidence root is `/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z`.
|
||||
|
||||
## Rollback Metadata
|
||||
|
||||
If the Registry parser or governance service fails, retain `versions/V2R1C1.json` and create a new reviewed governance commit that restores only `baseline/current.json` and `VERSION` to `V1R4C6`. Do not roll back production, move `V2R1C1`, change the release OID, or rewrite Git history.
|
||||
|
||||
## Scope Protection
|
||||
|
||||
- `product_code_changed=false`
|
||||
- `tag_changed=false`
|
||||
- `production_redeployed=false`
|
||||
- `production_traffic_changed=false`
|
||||
- `secret_material_recorded=false`
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"status": "V2R1C2_PRODUCTION_RELEASED",
|
||||
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
|
||||
"tag_name": "V2R1C2",
|
||||
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
|
||||
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
|
||||
"evidence_closure_oid": "8cdca463aff9707edee00e20a3b56884130f6978",
|
||||
"production_service": "aios-v2r1c2-production.service",
|
||||
"production_port": 18920,
|
||||
"candidate_port": 18921,
|
||||
"production_acceptance": "PASS",
|
||||
"role_matrix": "PASS",
|
||||
"tenant_isolation": "PASS",
|
||||
"api_smoke": "PASS",
|
||||
"ui_smoke": "PASS",
|
||||
"runtime_audit": "PASS",
|
||||
"production_acceptance_sha256": "a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a",
|
||||
"production_online_smoke_sha256": "fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4",
|
||||
"production_health_sha256": "ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c",
|
||||
"rollback_version": "V2R1C1",
|
||||
"rollback_ready": true,
|
||||
"main_changed_by_tag": false,
|
||||
"force_push": false,
|
||||
"promoted_at": "2026-07-22T01:40:51Z",
|
||||
"evidence_path": "/var/lib/aios-gitops/acceptance/AIOS-V2R1C2-PRODUCTION-20260722T011836Z"
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
# 执行 GitOps 独立 Worktree 与统一运行身份迁移
|
||||
|
||||
```yaml
|
||||
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
|
||||
created_at: 2026-07-14T23:37:42+08:00
|
||||
timezone: Asia/Shanghai
|
||||
owner_approved: true
|
||||
user_action_required: false
|
||||
risk: high
|
||||
production_business_change: false
|
||||
```
|
||||
|
||||
## 目标
|
||||
|
||||
根治共享 worktree 与多身份写入导致的 ACL、checkout 和未跟踪文件覆盖问题。
|
||||
|
||||
实施:
|
||||
|
||||
1. 统一 GitOps/OpenClaw 写入身份为 `aiman:gitops`;
|
||||
2. 统一 `UMask=0002`;
|
||||
3. 为 GitOps 根目录设置 setgid 与默认 ACL;
|
||||
4. 建立 bare repo 和每任务独立 worktree;
|
||||
5. 启用任务启动前自检;
|
||||
6. 验证 OpenClaw 与 ChatGPT 并发任务互不干扰;
|
||||
7. 保留原共享 workspace 作为只读回滚源,禁止智能体继续在其中切换分支。
|
||||
|
||||
## 强制通知
|
||||
|
||||
收到任务后立即微信通知 Owner:
|
||||
|
||||
```text
|
||||
【任务已接收】GitOps独立worktree与统一运行身份迁移开始。将暂停写入服务、建立独立任务工作树并验证并发执行;生产业务版本不变。
|
||||
```
|
||||
|
||||
任务结束后无论 PASS、BLOCKED 或 ROLLED_BACK,立即微信汇报结果并记录 message_id。
|
||||
|
||||
## 1. 变更前检查与封存
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
cd /data/aios-gitops/workspace
|
||||
pwd
|
||||
id
|
||||
umask
|
||||
git status --short --branch
|
||||
git rev-parse HEAD
|
||||
systemctl list-units --type=service --all | grep -Ei 'openclaw|gitops|aios'
|
||||
```
|
||||
|
||||
将工作区未提交内容完整提交到非 main 证据分支或 stash,禁止 `git clean -fd`。
|
||||
|
||||
记录当前运行服务、用户、组、umask、PID、工作区 HEAD 和未跟踪文件清单。
|
||||
|
||||
## 2. 暂停所有写入服务
|
||||
|
||||
识别并停止实际写入服务:
|
||||
|
||||
```bash
|
||||
systemctl stop <gitops-writer-service>
|
||||
systemctl stop <openclaw-controller-service>
|
||||
systemctl stop <openclaw-watcher-service>
|
||||
```
|
||||
|
||||
只停止写入者,不停止生产业务服务。记录每条命令和退出码。
|
||||
|
||||
## 3. 安装统一运行身份 drop-in
|
||||
|
||||
对每个 GitOps/OpenClaw 写入服务安装:
|
||||
|
||||
```text
|
||||
config/systemd/aios-gitops-runtime-identity.conf
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
```yaml
|
||||
User: aiman
|
||||
Group: gitops
|
||||
SupplementaryGroups: gitops
|
||||
UMask: 0002
|
||||
```
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl cat <service>
|
||||
```
|
||||
|
||||
不得让服务继续以 root 写 workspace。
|
||||
|
||||
## 4. 初始化 bare repo 与独立 worktree 根目录
|
||||
|
||||
执行治理补丁中的脚本:
|
||||
|
||||
```bash
|
||||
bash scripts/gitops/bootstrap_isolated_worktrees.sh
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
```yaml
|
||||
bare_repo: /data/aios-gitops/repo.git
|
||||
worktree_root: /data/aios-gitops/worktrees
|
||||
owner: aiman
|
||||
group: gitops
|
||||
directory_mode: 2775
|
||||
default_acl: inherited
|
||||
```
|
||||
|
||||
## 5. 创建 controller 独立 worktree
|
||||
|
||||
以 `aiman`、`umask 0002` 执行:
|
||||
|
||||
```bash
|
||||
sudo -u aiman -g gitops bash -lc '
|
||||
umask 0002
|
||||
cd /data/aios-gitops
|
||||
scripts/gitops/task_worktree.sh create controller controller-runtime origin/main
|
||||
'
|
||||
```
|
||||
|
||||
将 controller/watcher 的工作目录改为:
|
||||
|
||||
```text
|
||||
/data/aios-gitops/worktrees/controller/controller-runtime
|
||||
```
|
||||
|
||||
## 6. 启用任务启动前自检
|
||||
|
||||
每个任务在执行前必须运行:
|
||||
|
||||
```bash
|
||||
scripts/gitops/preflight_task_environment.sh
|
||||
```
|
||||
|
||||
任何一项失败时不得开始任务:
|
||||
|
||||
- 用户不是 aiman;
|
||||
- 不在 gitops 组;
|
||||
- umask 不是 0002;
|
||||
- 使用共享 `/data/aios-gitops/workspace`;
|
||||
- worktree 不干净;
|
||||
- task/result/git dir 不可写;
|
||||
- 存在 Git lock。
|
||||
|
||||
## 7. 启动服务
|
||||
|
||||
```bash
|
||||
systemctl start <gitops-writer-service>
|
||||
systemctl start <openclaw-controller-service>
|
||||
systemctl start <openclaw-watcher-service>
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
systemctl show <service> -p User -p Group -p UMask -p WorkingDirectory
|
||||
ps -eo user,group,pid,cmd | grep -Ei 'openclaw|gitops|aios'
|
||||
```
|
||||
|
||||
## 8. 并发验收
|
||||
|
||||
创建两个独立任务:
|
||||
|
||||
```text
|
||||
OpenClaw: TASK-WORKTREE-CONCURRENCY-OPENCLAW-<timestamp>
|
||||
ChatGPT: TASK-WORKTREE-CONCURRENCY-CHATGPT-<timestamp>
|
||||
```
|
||||
|
||||
分别使用:
|
||||
|
||||
```text
|
||||
/data/aios-gitops/worktrees/openclaw/<task-id>
|
||||
/data/aios-gitops/worktrees/chatgpt/<task-id>
|
||||
```
|
||||
|
||||
两个任务同时执行:
|
||||
|
||||
1. 写各自 `.agent/tasks/<agent>/`;
|
||||
2. 写各自 `.agent/results/<agent>/`;
|
||||
3. 创建深层子目录,验证默认 ACL;
|
||||
4. commit;
|
||||
5. push;
|
||||
6. 不切换共享 workspace 分支。
|
||||
|
||||
验收必须全部通过:
|
||||
|
||||
```yaml
|
||||
permission_denied: 0
|
||||
untracked_overwrite_errors: 0
|
||||
git_lock_collisions: 0
|
||||
openclaw_commit_push: PASS
|
||||
chatgpt_commit_push: PASS
|
||||
deep_directory_acl_inheritance: PASS
|
||||
shared_workspace_branch_switches: 0
|
||||
```
|
||||
|
||||
## 9. 回滚
|
||||
|
||||
若迁移失败:
|
||||
|
||||
1. 停止新服务;
|
||||
2. 保留失败 worktree 供审计;
|
||||
3. 恢复 systemd drop-in 备份;
|
||||
4. `daemon-reload`;
|
||||
5. 恢复原服务状态;
|
||||
6. 不删除 `/data/aios-gitops/workspace`;
|
||||
7. 生产业务保持不变。
|
||||
|
||||
## 10. 结果文件
|
||||
|
||||
提交:
|
||||
|
||||
```text
|
||||
.agent/results/openclaw/RESULT-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8.md
|
||||
```
|
||||
|
||||
首段必须包含:
|
||||
|
||||
```yaml
|
||||
status: PASS | BLOCKED | ROLLED_BACK
|
||||
task_id: TASK-GITOPS-ISOLATED-WORKTREE-MIGRATION-20260714T233742UTC8
|
||||
created_at: 2026-07-14T23:37:42+08:00
|
||||
timezone: Asia/Shanghai
|
||||
user_action_required: false
|
||||
runtime_identity:
|
||||
user:
|
||||
group:
|
||||
umask:
|
||||
bare_repo:
|
||||
worktree_root:
|
||||
controller_worktree:
|
||||
services:
|
||||
default_acl:
|
||||
preflight:
|
||||
concurrency_test:
|
||||
openclaw_branch:
|
||||
openclaw_commit:
|
||||
chatgpt_branch:
|
||||
chatgpt_commit:
|
||||
permission_denied_count:
|
||||
untracked_overwrite_count:
|
||||
git_lock_collision_count:
|
||||
owner_notifications:
|
||||
received_message_id:
|
||||
terminal_message_id:
|
||||
rollback:
|
||||
remaining_risks:
|
||||
next_step:
|
||||
```
|
||||
|
||||
提交并推送到独立执行分支。禁止直接推 main。
|
||||
|
||||
## 完成微信模板
|
||||
|
||||
PASS:
|
||||
|
||||
```text
|
||||
【任务完成】GitOps独立worktree迁移已通过。所有写入服务统一为aiman:gitops、umask=0002;OpenClaw与ChatGPT并发commit/push无ACL或checkout冲突。生产业务版本未变化。
|
||||
```
|
||||
|
||||
阻塞:
|
||||
|
||||
```text
|
||||
【任务阻塞】GitOps独立worktree迁移未通过。原因=<具体服务/权限/并发验收>;已保留原workspace和回滚能力,生产业务未变化。
|
||||
```
|
||||
@@ -140,6 +140,19 @@ def sync_tag(tag: str) -> dict:
|
||||
if p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or f'git tag push failed for {tag}')
|
||||
return {'status':'synced','target':'origin','tag':tag,'stdout':p.stdout.strip(),'stderr':p.stderr.strip()}
|
||||
|
||||
def resolve_commit_oid(value: str | None, field: str = 'target_oid') -> str:
|
||||
oid = str(value or '').strip().lower()
|
||||
if not re.fullmatch(r'[0-9a-f]{40}', oid):
|
||||
raise ValueError(f'{field} must be a full 40-character commit OID')
|
||||
result = sh(['rev-parse', '--verify', f'{oid}^{{commit}}'])
|
||||
if result['code'] != 0 or result['stdout'] != oid:
|
||||
raise ValueError(f'{field} does not resolve to the exact approved commit')
|
||||
return oid
|
||||
|
||||
def existing_tag_target(tag: str) -> str | None:
|
||||
result = sh(['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'])
|
||||
return result['stdout'] if result['code'] == 0 else None
|
||||
|
||||
def commit(paths, msg):
|
||||
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
|
||||
branch=sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']
|
||||
@@ -252,12 +265,35 @@ class H(BaseHTTPRequestHandler):
|
||||
if not a: return
|
||||
ver=data.get('version') or data.get('tag')
|
||||
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
|
||||
if data.get('confirm') != True:
|
||||
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
|
||||
return
|
||||
try:
|
||||
target_oid=resolve_commit_oid(data.get('target_oid'))
|
||||
evidence_oid=resolve_commit_oid(data.get('evidence_closure_oid'), 'evidence_closure_oid') if data.get('evidence_closure_oid') else None
|
||||
except ValueError as exc:
|
||||
return self.sendj({'error':'invalid_tag_target','detail':str(exc)},400)
|
||||
if evidence_oid and sh(['merge-base','--is-ancestor',target_oid,evidence_oid])['code'] != 0:
|
||||
return self.sendj({'error':'evidence_lineage_mismatch','target_oid':target_oid,'evidence_closure_oid':evidence_oid},409)
|
||||
if a['role'] != 'owner' or not data.get('execute'):
|
||||
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
|
||||
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
|
||||
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'request':req,'commit':c})
|
||||
current_target=existing_tag_target(ver)
|
||||
if current_target and current_target != target_oid:
|
||||
audit(a['account'],'tag_conflict',ver,{'target_oid':target_oid,'actual_target_oid':current_target,'approval_plan_id':data.get('approval_plan_id')})
|
||||
return self.sendj({'status':'tag_target_conflict','version':ver,'target_oid':current_target,'expected_target_oid':target_oid,'main_changed':False,'force_push':False},409)
|
||||
main_before=sh(['rev-parse','refs/heads/main'], True)['stdout']
|
||||
if current_target:
|
||||
status='tagged_existing'; out={'code':0,'stdout':'','stderr':'','cmd':['git','tag','existing']}
|
||||
else:
|
||||
status='tagged'; out=sh(['tag','-a',ver,target_oid,'-m',data.get('message') or f'release {ver}'], True)
|
||||
actual_target=existing_tag_target(ver)
|
||||
if actual_target != target_oid:
|
||||
raise RuntimeError(f'tag target verification failed: expected {target_oid}, got {actual_target}')
|
||||
tag_oid=sh(['rev-parse',f'refs/tags/{ver}'], True)['stdout']
|
||||
main_after=sh(['rev-parse','refs/heads/main'], True)['stdout']
|
||||
if main_after != main_before:
|
||||
raise RuntimeError('main changed during tag operation')
|
||||
sync=sync_tag(ver)
|
||||
result={'status':status,'version':ver,'tag_name':ver,'tag_oid':tag_oid,'target_oid':actual_target,'expected_target_oid':target_oid,'evidence_closure_oid':evidence_oid,'approval_plan_id':data.get('approval_plan_id'),'workspace_head':sh(['rev-parse','HEAD'],True)['stdout'],'main_oid_before':main_before,'main_oid_after':main_after,'main_changed':False,'force_push':False,'git':out,'sync':sync}
|
||||
audit(a['account'],'tag',ver,result)
|
||||
self.sendj(result)
|
||||
def rollback(self,data):
|
||||
a=self.need('rollback:request');
|
||||
if not a: return
|
||||
@@ -395,4 +431,4 @@ def api_spec():
|
||||
def main():
|
||||
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
|
||||
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
|
||||
if __name__ == '__main__': main()
|
||||
if __name__ == '__main__': main()
|
||||
|
||||
+24
-12
@@ -1,13 +1,25 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
"current_baseline": "V2R1C2",
|
||||
"previous_baseline": "V2R1C1",
|
||||
"promoted_at": "2026-07-22T01:40:51Z",
|
||||
"git": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
|
||||
"promoted_by": "codex/owner-approved-gitops",
|
||||
"promotion_reason": "Owner-authorized V2R1C2 Unified Portal production release after exact tag binding, complete evidence verification, and production acceptance PASS.",
|
||||
"version": "V2R1C2",
|
||||
"release_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
|
||||
"tag_oid": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
|
||||
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
|
||||
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"production_service": "aios-v2r1c2-production.service",
|
||||
"production_port": 18920,
|
||||
"rollback_version": "V2R1C1",
|
||||
"constraints": {
|
||||
"product_code_changed": true,
|
||||
"production_redeployed": true,
|
||||
"production_traffic_changed": true,
|
||||
"tag_rewritten": false,
|
||||
"force_push_allowed": false,
|
||||
"rollback_assets_preserved": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
version: 1
|
||||
policy_id: gitops-isolated-worktree-governance-20260714T232948UTC8
|
||||
|
||||
runtime_identity:
|
||||
user: aiman
|
||||
group: gitops
|
||||
umask: "0002"
|
||||
prohibit_root_workspace_writes: true
|
||||
|
||||
repository:
|
||||
source_worktree: /data/aios-gitops/workspace
|
||||
bare_repo: /data/aios-gitops/repo.git
|
||||
worktree_root: /data/aios-gitops/worktrees
|
||||
remote: origin
|
||||
|
||||
worktrees:
|
||||
controller: /data/aios-gitops/worktrees/controller
|
||||
task_pattern: /data/aios-gitops/worktrees/{agent}/{task_id}
|
||||
allowed_agents:
|
||||
- openclaw
|
||||
- chatgpt
|
||||
- controller
|
||||
require_clean_start: true
|
||||
remove_after_successful_push: true
|
||||
retain_on_failure: true
|
||||
|
||||
permissions:
|
||||
owner: aiman
|
||||
group: gitops
|
||||
directory_mode: "2775"
|
||||
file_mode: "0664"
|
||||
default_acl:
|
||||
user:aiman: rwx
|
||||
group:gitops: rwx
|
||||
apply_to:
|
||||
- /data/aios-gitops
|
||||
- /data/aios-gitops/worktrees
|
||||
|
||||
preflight:
|
||||
required_user: aiman
|
||||
required_group: gitops
|
||||
required_umask: "0002"
|
||||
require_writable_git_dir: true
|
||||
require_writable_task_dir: true
|
||||
require_writable_result_dir: true
|
||||
reject_shared_source_worktree: true
|
||||
reject_existing_git_lock: true
|
||||
|
||||
notifications:
|
||||
owner_on_received: required
|
||||
owner_on_terminal: required
|
||||
preferred_channel: weixin
|
||||
redact_secrets: true
|
||||
|
||||
rollback:
|
||||
preserve_source_worktree: true
|
||||
remove_created_worktrees: true
|
||||
restore_service_dropins: true
|
||||
restore_service_state: true
|
||||
@@ -0,0 +1,14 @@
|
||||
# Install as a systemd drop-in for GitOps/OpenClaw writer services.
|
||||
# Example: /etc/systemd/system/<service>.service.d/10-runtime-identity.conf
|
||||
|
||||
[Service]
|
||||
User=aiman
|
||||
Group=gitops
|
||||
SupplementaryGroups=gitops
|
||||
UMask=0002
|
||||
Environment=AIOS_GITOPS_ROOT=/data/aios-gitops
|
||||
Environment=AIOS_BARE_REPO=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_WORKTREE_ROOT=/data/aios-gitops/worktrees
|
||||
Environment=AIOS_RUNTIME_USER=aiman
|
||||
Environment=AIOS_RUNTIME_GROUP=gitops
|
||||
Environment=AIOS_RUNTIME_UMASK=0002
|
||||
@@ -0,0 +1,27 @@
|
||||
# AIOS V2R1C2 Unified Portal production metadata.
|
||||
# No secret values belong in this file; runtime integrations use references.
|
||||
|
||||
AIOS_V2_HOST=127.0.0.1
|
||||
AIOS_V2_PORT=18920
|
||||
AIOS_V2_STATE_DIR=/var/lib/aios-v2r1c2-production
|
||||
AIOS_PROVENANCE_STORE_FILE=/var/lib/aios-v2r1c2-production/run-provenance.sqlite3
|
||||
AIOS_EXTERNAL_TASK_STORE_FILE=/var/lib/aios-v2r1c2-production/external-task-truth.sqlite3
|
||||
AIOS_EXECUTOR_CATALOG_FILE=/var/lib/aios-v2r1c2-production/executor-catalog.json
|
||||
AIOS_EXECUTION_PLAN_FILES=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-RELEASE.json:/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
|
||||
AIOS_EXECUTION_PLAN_FILE=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
|
||||
|
||||
AIOS_RELEASE_VERSION=V2R1C2
|
||||
AIOS_TARGET_VERSION=V2R1C2
|
||||
AIOS_BASELINE_VERSION=V2R1C1
|
||||
AIOS_PREVIOUS_VERSION=V2R1C1
|
||||
AIOS_RELEASE_STATUS=PRODUCTION
|
||||
AIOS_RELEASE_SOURCE_OID=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
|
||||
AIOS_RELEASE_OID=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
|
||||
AIOS_RELEASE_TAG=V2R1C2
|
||||
AIOS_RELEASE_EVIDENCE_SHA256=2e27bb73b8e6297bd3077238f73a96d697fc3c3611c601c4dc9357e5fdcd8eee
|
||||
|
||||
AIOS_OPENCLAW_CONTRACT_VERSION=aios.openclaw.command.v2
|
||||
AIOS_OPENCLAW_SIGNATURE_ALGORITHM=Ed25519
|
||||
AIOS_OPENCLAW_PLUGIN_VERSION=2.1.0-rc.4
|
||||
AIOS_OPENCLAW_SIGNING_KEY_ID=openclaw-v2r1-candidate-ed25519
|
||||
AIOS_OWNER_WECHAT_TARGET_REF=AIOS_OWNER_WECHAT_TARGET_REF
|
||||
@@ -0,0 +1,44 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C2 Unified Portal Production
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Conflicts=aios-orchestrator-gateway.service
|
||||
ConditionPathExists=/opt/aios-v2r1c2/active-runtime/app/aios_web/release_application.py
|
||||
ConditionPathExists=/opt/aios-v2r1c2/plans/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER.json
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios
|
||||
Group=aios
|
||||
WorkingDirectory=/opt/aios-v2r1c2/active-runtime/app
|
||||
EnvironmentFile=/etc/aios-v2/aios-v2r1c2-production.env
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStartPre=/usr/bin/test "$AIOS_V2_PORT" = "18920"
|
||||
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_SOURCE_OID" = "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24"
|
||||
ExecStartPre=/usr/bin/test "$AIOS_RELEASE_TAG" = "V2R1C2"
|
||||
ExecStart=/usr/bin/python3 -m aios_web.release_application
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
TimeoutStartSec=60
|
||||
TimeoutStopSec=30
|
||||
KillSignal=SIGTERM
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
ReadWritePaths=/var/lib/aios-v2r1c2-production
|
||||
StateDirectory=aios-v2r1c2-production
|
||||
StateDirectoryMode=0750
|
||||
UMask=0027
|
||||
LimitNOFILE=65536
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -50,9 +50,20 @@ components:
|
||||
message: {type: string}
|
||||
TagRequest:
|
||||
type: object
|
||||
required: [version]
|
||||
required: [version, target_oid]
|
||||
properties:
|
||||
version: {type: string, example: V1R1C1}
|
||||
target_oid:
|
||||
type: string
|
||||
pattern: '^[0-9a-fA-F]{40}$'
|
||||
description: Full approved commit OID. The tag is created explicitly at this commit, never implicitly at workspace HEAD.
|
||||
evidence_closure_oid:
|
||||
type: string
|
||||
pattern: '^[0-9a-fA-F]{40}$'
|
||||
description: Optional evidence closure commit. When supplied, target_oid must be its ancestor.
|
||||
approval_plan_id:
|
||||
type: string
|
||||
description: Audited owner-approved release or tag plan identifier.
|
||||
message: {type: string}
|
||||
execute:
|
||||
type: boolean
|
||||
@@ -209,7 +220,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TagRequest'}
|
||||
responses: {'200': {description: Tag request committed or tag created}, '403': {description: Forbidden}}
|
||||
responses: {'200': {description: Tag request committed or tag created}, '400': {description: Invalid or unresolved target OID}, '403': {description: Forbidden}, '409': {description: Existing tag target conflict or evidence lineage mismatch}}
|
||||
x-openai-isConsequential: true
|
||||
/api/rollback:
|
||||
post:
|
||||
@@ -245,4 +256,4 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/NLRequest'}
|
||||
responses: {'
|
||||
responses: {'
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# V2R1C1 中央 Baseline 治理收口
|
||||
|
||||
## 收口范围
|
||||
|
||||
本次变更只登记已经完成生产验证并上线的 `V2R1C1`。它新增中央版本 Manifest、把中央 Baseline Pointer 从 `V1R4C6` 切换为 `V2R1C1`,并建立 Release、Companion、Artifact 与最终生产 Evidence 的追溯关系。
|
||||
|
||||
本次不修改产品实现,不移动或重建 `V2R1C1` Tag,不改变 Release OID,不重新部署生产,不切换生产流量,也不重写历史证据。
|
||||
|
||||
## 权威标识
|
||||
|
||||
- Release:`release/AIOS-V2R1C1-20260721`
|
||||
- Release OID:`4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`
|
||||
- Tag / Tag OID:`V2R1C1` / `4e0e6dcb90e6732aa9838fe9d0af0b34746829b0`
|
||||
- SemVer:`2.1.0-rc.1`
|
||||
- OpenClaw Companion:`2.1.0-rc.4`
|
||||
- Companion Source OID:`83f8f878c7da5dd8263f2af2efa80750492e5cb3`
|
||||
- Companion OID:`12d1c13170e0c5a691d67a9408e739e7a17064a3`
|
||||
- Artifact SHA256:`aadd5f3cfa0d8e796a21f6368f5c95e8c60e9063d6e9fcd9c4947cfa0c013671`
|
||||
- Contract:`aios.openclaw.command.v2` / `Ed25519`
|
||||
- Evidence Root:`/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z`
|
||||
- Final Audit Manifest SHA256:`2cdb884abf195969de3cdaa44c76aaac52a9165fb4918436855291e46b2f8d55`
|
||||
- Production Status:`PRODUCTION_DEPLOYED`
|
||||
|
||||
## 治理路径
|
||||
|
||||
- Governance Branch:`patch/chatgpt/AIOS-V2R1C1-BASELINE-REGISTRY-20260721`
|
||||
- Base Ref:`refs/heads/main`
|
||||
- Base OID:`3cf9958cb94614449e093639cbf902216d73447f`
|
||||
- Manifest Commit:`a56caf46f6bc335ec2395b674ead4b2a427c5edd`
|
||||
- Baseline Commit:`6dbd5cbe07d2960ca653049fefbcf3a3e18f9ab1`
|
||||
- Reviewer:`gpt-5.3-codex-spark validator and docs_reporter`
|
||||
- Review:`PASS`(`2026-07-21T14:23:00Z`)
|
||||
|
||||
## 命令与退出码
|
||||
|
||||
| 阶段 | 命令 | 开始 UTC | 结束 UTC | Exit Code |
|
||||
| --- | --- | --- | --- | ---: |
|
||||
| 中央只读核对 | `GET /api/run; GET /api/base; GET /api/ver` | 2026-07-21T14:09:40Z | 2026-07-21T14:09:40Z | 0 |
|
||||
| 创建治理分支 | `bash /tmp/registry-create-branch.sh` | 2026-07-21T14:17:05Z | 2026-07-21T14:17:05Z | 0 |
|
||||
| 核心元数据验证 | `bash /tmp/registry-validate-core.sh` | 2026-07-21T14:19:56Z | 2026-07-21T14:19:56Z | 0 |
|
||||
|
||||
完整机读记录位于 `.agent/results/releases/V2R1C1-BASELINE-CLOSURE.json`,治理运行证据位于 `/var/lib/aios-gitops/acceptance/aios-v2r1c1-baseline-registry-20260721T141705Z`。
|
||||
|
||||
## 元数据回滚
|
||||
|
||||
若中央 Registry 解析或治理服务异常,保留 `versions/V2R1C1.json`,通过新的受审治理提交把 `baseline/current.json` 和 `VERSION` 临时恢复到 `V1R4C6`。该操作不得回滚生产系统、移动 Tag、修改 Release OID 或重写历史。
|
||||
|
||||
## 范围证明
|
||||
|
||||
```text
|
||||
product_code_changed=false
|
||||
tag_changed=false
|
||||
production_redeployed=false
|
||||
production_traffic_changed=false
|
||||
secret_material_recorded=false
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# V2R1C2 Production Release
|
||||
|
||||
- Release Run ID: `AIOS-V2R1C2-PRODUCTION-20260722T011836Z`
|
||||
- Previous production baseline: `V2R1C1`
|
||||
- Tag: `V2R1C2`
|
||||
- Tag object: `a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36`
|
||||
- Approved target: `3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24`
|
||||
- Evidence closure: `8cdca463aff9707edee00e20a3b56884130f6978`
|
||||
- Initial registry status: `TAG_CREATED_NOT_PROMOTED`
|
||||
- Final status: `PRODUCTION_DEPLOYED`
|
||||
- Production service: `aios-v2r1c2-production.service`
|
||||
- Production port: `18920`
|
||||
- Production acceptance SHA256: `a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a`
|
||||
- Production online smoke SHA256: `fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4`
|
||||
- Production health SHA256: `ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c`
|
||||
|
||||
## Production gates
|
||||
|
||||
Promotion may proceed only when the 18921 candidate remains healthy, the committed evidence checksum file verifies without drift, the 18920 baseline is healthy, required secret references are available without exposing secret values, and the V2R1C1 rollback service/configuration remains intact.
|
||||
|
||||
The obsolete release-plan `DECLARED` rows are not authoritative for this run. The production gate must explicitly select `AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER-20260722` and require its three required stages to be `SUCCEEDED`; an unqualified aggregate across stale plan rows fails closed.
|
||||
|
||||
## Rollback rule
|
||||
|
||||
If Health, API, UI, Role Matrix, Tenant Isolation, Runtime, or Audit verification fails after the 18920 switch, stop the V2R1C2 production unit and restart the preserved V2R1C1 18920 service. Do not delete or rewrite either release tag or rollback asset.
|
||||
|
||||
## Result
|
||||
|
||||
The production switch completed at `2026-07-22T01:40:51Z`. Full acceptance and the production online smoke passed, including Health, API, UI, Role Matrix, Tenant Isolation, Runtime, Audit and the selected takeover release gate. The previous `aios-orchestrator-gateway.service` unit, configuration and Release Run backup remain available for rollback.
|
||||
Executable
+153
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
VERSION=V2R1C2
|
||||
EXPECTED_TARGET=3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24
|
||||
TAG_OBJECT=a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36
|
||||
REPO=/data/aios-gitops/repo.git
|
||||
SOURCE=/opt/aios-v2r1c2
|
||||
TAKEOVER=/data/aios-gitops/worktrees/codex/AIOS-V2R1C2-UNIFIED-PORTAL-TAKEOVER-20260722
|
||||
CANDIDATE_SERVICE=aios-v2r1c2-portal.service
|
||||
OLD_SERVICE=aios-orchestrator-gateway.service
|
||||
NEW_SERVICE=aios-v2r1c2-production.service
|
||||
CANDIDATE_STATE=/var/lib/aios-v2r1c2-portal
|
||||
PRODUCTION_STATE=/var/lib/aios-v2r1c2-production
|
||||
RUN_ID=${AIOS_RELEASE_RUN_ID:-AIOS-V2R1C2-PRODUCTION-20260722T011836Z}
|
||||
EVIDENCE_DIR=/var/lib/aios-gitops/acceptance/$RUN_ID
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
RELEASE_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd)
|
||||
UNIT_SOURCE=$RELEASE_ROOT/deploy/aios-v2/systemd/aios-v2r1c2-production.service
|
||||
ENV_SOURCE=$RELEASE_ROOT/deploy/aios-v2/systemd/aios-v2r1c2-production.env.example
|
||||
LOCK=/run/lock/aios-v2r1c2-production-release.lock
|
||||
|
||||
require_root() {
|
||||
test "$(id -u)" -eq 0 || { echo 'root required' >&2; exit 77; }
|
||||
}
|
||||
|
||||
health_old() {
|
||||
curl -fsS --max-time 10 http://127.0.0.1:18920/healthz
|
||||
}
|
||||
|
||||
health_new() {
|
||||
curl -fsS --max-time 10 http://127.0.0.1:18920/api/v2/health
|
||||
}
|
||||
|
||||
health_candidate() {
|
||||
curl -fsS --max-time 10 http://127.0.0.1:18921/api/v2/health
|
||||
}
|
||||
|
||||
wait_candidate() {
|
||||
for _ in $(seq 1 30); do
|
||||
if health_candidate; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
preflight() {
|
||||
require_root
|
||||
test "$(git --git-dir="$REPO" rev-parse refs/tags/$VERSION)" = "$TAG_OBJECT"
|
||||
test "$(git --git-dir="$REPO" rev-list -n 1 "$VERSION")" = "$EXPECTED_TARGET"
|
||||
test -f "$SOURCE/active-runtime/app/aios_web/release_application.py"
|
||||
test -f "$UNIT_SOURCE"
|
||||
test -f "$ENV_SOURCE"
|
||||
command -v sqlite3 >/dev/null
|
||||
systemctl is-active --quiet "$CANDIDATE_SERVICE"
|
||||
systemctl is-active --quiet "$OLD_SERVICE"
|
||||
health_candidate >/dev/null
|
||||
health_old >/dev/null
|
||||
(
|
||||
cd "$TAKEOVER/artifacts/v2r1c2"
|
||||
sha256sum -c checksums.sha256 >/dev/null
|
||||
)
|
||||
diff -qr --exclude=__pycache__ "$TAKEOVER/active-runtime/app" "$SOURCE/active-runtime/app" >/dev/null
|
||||
diff -qr "$TAKEOVER/plans" "$SOURCE/plans" >/dev/null
|
||||
echo PREFLIGHT=PASS
|
||||
}
|
||||
|
||||
prepare() {
|
||||
preflight
|
||||
install -d -m 0750 "$EVIDENCE_DIR" "$EVIDENCE_DIR/backup"
|
||||
health_old >"$EVIDENCE_DIR/production-health-before.json"
|
||||
health_candidate >"$EVIDENCE_DIR/candidate-health-before.json"
|
||||
systemctl cat "$OLD_SERVICE" >"$EVIDENCE_DIR/backup/$OLD_SERVICE.unit"
|
||||
cp -a /etc/aios-orchestrator-gateway "$EVIDENCE_DIR/backup/"
|
||||
cp -a /etc/systemd/system/aios-orchestrator-gateway.service "$EVIDENCE_DIR/backup/"
|
||||
if test -e "$PRODUCTION_STATE"; then
|
||||
mv "$PRODUCTION_STATE" "$EVIDENCE_DIR/backup/aios-v2r1c2-production-state.preexisting"
|
||||
fi
|
||||
systemctl stop "$CANDIDATE_SERVICE"
|
||||
trap 'systemctl start "$CANDIDATE_SERVICE" || true' EXIT
|
||||
while IFS= read -r -d '' database; do
|
||||
sqlite3 "$database" 'PRAGMA wal_checkpoint(TRUNCATE);' >/dev/null
|
||||
test "$(sqlite3 "$database" 'PRAGMA quick_check;')" = ok
|
||||
done < <(find "$CANDIDATE_STATE" -maxdepth 1 -type f -name '*.sqlite3' -print0)
|
||||
install -d -o aios -g aios -m 0750 "$PRODUCTION_STATE"
|
||||
cp -a "$CANDIDATE_STATE/." "$PRODUCTION_STATE/"
|
||||
chown -R aios:aios "$PRODUCTION_STATE"
|
||||
while IFS= read -r -d '' database; do
|
||||
test "$(sqlite3 "$database" 'PRAGMA quick_check;')" = ok
|
||||
done < <(find "$PRODUCTION_STATE" -maxdepth 1 -type f -name '*.sqlite3' -print0)
|
||||
install -o root -g root -m 0644 "$UNIT_SOURCE" /etc/systemd/system/aios-v2r1c2-production.service
|
||||
install -d -o root -g aios -m 0750 /etc/aios-v2
|
||||
install -o root -g aios -m 0640 "$ENV_SOURCE" /etc/aios-v2/aios-v2r1c2-production.env
|
||||
systemctl daemon-reload
|
||||
systemctl start "$CANDIDATE_SERVICE"
|
||||
wait_candidate >"$EVIDENCE_DIR/candidate-health-after-state-copy.json"
|
||||
trap - EXIT
|
||||
echo PREPARE=PASS
|
||||
}
|
||||
|
||||
rollback() {
|
||||
require_root
|
||||
systemctl stop "$NEW_SERVICE" || true
|
||||
systemctl disable "$NEW_SERVICE" >/dev/null 2>&1 || true
|
||||
systemctl enable "$OLD_SERVICE" >/dev/null 2>&1 || true
|
||||
systemctl start "$OLD_SERVICE"
|
||||
health_old >"$EVIDENCE_DIR/rollback-health.json"
|
||||
echo ROLLBACK=PASS
|
||||
}
|
||||
|
||||
promote() {
|
||||
require_root
|
||||
test -f /etc/systemd/system/aios-v2r1c2-production.service
|
||||
test -f /etc/aios-v2/aios-v2r1c2-production.env
|
||||
! systemctl is-active --quiet "$NEW_SERVICE"
|
||||
systemctl is-active --quiet "$CANDIDATE_SERVICE"
|
||||
health_candidate >/dev/null
|
||||
health_old >"$EVIDENCE_DIR/production-health-switch-input.json"
|
||||
trap 'trap - ERR; rollback' ERR
|
||||
systemctl stop "$OLD_SERVICE"
|
||||
systemctl start "$NEW_SERVICE"
|
||||
for _ in $(seq 1 30); do
|
||||
if health_new >"$EVIDENCE_DIR/production-health-after-switch.json" 2>/dev/null; then
|
||||
echo PROMOTE=PASS
|
||||
trap - ERR
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
finalize() {
|
||||
require_root
|
||||
systemctl is-active --quiet "$NEW_SERVICE"
|
||||
health_new >"$EVIDENCE_DIR/production-health-final.json"
|
||||
systemctl disable "$OLD_SERVICE" >/dev/null
|
||||
systemctl enable "$NEW_SERVICE" >/dev/null
|
||||
echo FINALIZE=PASS
|
||||
}
|
||||
|
||||
exec 9>"$LOCK"
|
||||
flock -n 9 || { echo 'release operation already running' >&2; exit 75; }
|
||||
case "${1:-}" in
|
||||
preflight) preflight ;;
|
||||
prepare) prepare ;;
|
||||
promote) promote ;;
|
||||
rollback) rollback ;;
|
||||
finalize) finalize ;;
|
||||
*) echo "usage: $0 {preflight|prepare|promote|rollback|finalize}" >&2; exit 64 ;;
|
||||
esac
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
RUN_ID=AIOS-V2R1C2-PRODUCTION-20260722T011836Z
|
||||
CONTROL=/data/aios-gitops/worktrees/controller/ops/aios-v2/v2r1c2-production-control.sh
|
||||
REPO=/data/aios-gitops/repo.git
|
||||
EVIDENCE_DIR=/var/lib/aios-gitops/acceptance/$RUN_ID
|
||||
DRIVER=/data/aios-gitops/worktrees/controller/tools/run_online_smoke_18920.py
|
||||
TEMP_SOURCE=
|
||||
|
||||
cleanup_temp() {
|
||||
case "$TEMP_SOURCE" in
|
||||
/tmp/aios-v2r1c2-production-acceptance.*)
|
||||
test -d "$TEMP_SOURCE" && rm -rf -- "$TEMP_SOURCE"
|
||||
;;
|
||||
'') ;;
|
||||
*) echo "refusing to remove unexpected temporary path: $TEMP_SOURCE" >&2; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
rollback_on_error() {
|
||||
code=$?
|
||||
trap - ERR
|
||||
AIOS_RELEASE_RUN_ID="$RUN_ID" "$CONTROL" rollback || true
|
||||
cleanup_temp || true
|
||||
exit "$code"
|
||||
}
|
||||
trap rollback_on_error ERR
|
||||
|
||||
AIOS_RELEASE_RUN_ID="$RUN_ID" "$CONTROL" promote
|
||||
systemctl is-active --quiet aios-v2r1c2-production.service
|
||||
! systemctl is-active --quiet aios-orchestrator-gateway.service
|
||||
curl -fsS --max-time 10 http://127.0.0.1:18920/api/v2/health >"$EVIDENCE_DIR/health-after-promote.json"
|
||||
curl -fsS --max-time 10 http://127.0.0.1:18921/api/v2/health >"$EVIDENCE_DIR/candidate-health-during-production.json"
|
||||
|
||||
TEMP_SOURCE=$(mktemp -d /tmp/aios-v2r1c2-production-acceptance.XXXXXX)
|
||||
git --git-dir="$REPO" archive 8cdca463aff9707edee00e20a3b56884130f6978 | tar -x -C "$TEMP_SOURCE"
|
||||
(
|
||||
cd "$TEMP_SOURCE"
|
||||
python3 tools/aios_v2r1c2_acceptance.py \
|
||||
--source-oid 3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24 \
|
||||
--online \
|
||||
--base-url http://127.0.0.1:18920 \
|
||||
--evidence-output "$EVIDENCE_DIR/production-acceptance.json"
|
||||
)
|
||||
python3 "$DRIVER" "$TEMP_SOURCE" "$EVIDENCE_DIR/production-online-smoke.json"
|
||||
|
||||
python3 - "$EVIDENCE_DIR/production-acceptance.json" "$EVIDENCE_DIR/production-online-smoke.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
for path in sys.argv[1:]:
|
||||
with open(path, encoding='utf-8') as handle:
|
||||
data = json.load(handle)
|
||||
if data.get('passed') is not True:
|
||||
raise SystemExit(f'acceptance failed: {path}')
|
||||
PY
|
||||
|
||||
systemctl show aios-v2r1c2-production.service \
|
||||
-p ActiveState -p SubState -p MainPID -p NRestarts \
|
||||
>"$EVIDENCE_DIR/production-service-status.txt"
|
||||
sha256sum "$EVIDENCE_DIR"/production-acceptance.json \
|
||||
"$EVIDENCE_DIR"/production-online-smoke.json \
|
||||
"$EVIDENCE_DIR"/health-after-promote.json \
|
||||
>"$EVIDENCE_DIR/production-evidence.sha256"
|
||||
|
||||
cleanup_temp
|
||||
TEMP_SOURCE=
|
||||
trap - ERR
|
||||
echo SWITCH_VALIDATED=PASS
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "patch-1784043007770-e97206c3",
|
||||
"patch_name": "gitops-isolated-worktree-governance-20260714T232948UTC8",
|
||||
"branch": "patch/gitops-isolated-worktree-governance-20260714T232948UTC8",
|
||||
"risk": "high",
|
||||
"description": "Root-cause remediation for recurring ACL and shared-worktree failures: unified aiman:gitops runtime identity, UMask=0002, inherited default ACLs, per-task isolated Git worktrees, startup preflight, migration/rollback scripts, systemd drop-in templates, audit evidence, and an OpenClaw execution task. No production business deployment in this patch.",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-14T15:30:07Z"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "nl-1784681185786-19ac0f82",
|
||||
"text": "Create a safe GitOps plan for tag V2R1C2. The tag target must be commit 3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24. Evidence closure commit is 8cdca463aff9707edee00e20a3b56884130f6978. Do not tag the current workspace HEAD. Do not modify main. Do not force push. Do not promote production. If the target commit cannot be bound explicitly, keep the operation blocked.",
|
||||
"intent": "unknown",
|
||||
"action_plan": [],
|
||||
"execute": false,
|
||||
"owner_approval_required": true,
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-22T00:46:25Z"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "task/codex/gitops-isolated-worktree-migration-20260715T001652UTC8",
|
||||
"base": "patch/gitops-isolated-worktree-governance-20260714T232948UTC8",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-14T16:17:28Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
|
||||
SOURCE="${AIOS_SOURCE_WORKTREE:-$ROOT/workspace}"
|
||||
BARE="${AIOS_BARE_REPO:-$ROOT/repo.git}"
|
||||
WORKTREES="${AIOS_WORKTREE_ROOT:-$ROOT/worktrees}"
|
||||
RUNTIME_USER="${AIOS_RUNTIME_USER:-aiman}"
|
||||
RUNTIME_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "ERROR: bootstrap must run as root" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
getent passwd "$RUNTIME_USER" >/dev/null
|
||||
getent group "$RUNTIME_GROUP" >/dev/null
|
||||
[[ -d "$SOURCE/.git" || -f "$SOURCE/.git" ]] || {
|
||||
echo "ERROR: source worktree not found: $SOURCE" >&2
|
||||
exit 3
|
||||
}
|
||||
|
||||
install -d -o "$RUNTIME_USER" -g "$RUNTIME_GROUP" -m 2775 "$ROOT" "$WORKTREES"
|
||||
install -d -o "$RUNTIME_USER" -g "$RUNTIME_GROUP" -m 2775 \
|
||||
"$WORKTREES/openclaw" "$WORKTREES/chatgpt" "$WORKTREES/controller"
|
||||
|
||||
if [[ ! -d "$BARE" ]]; then
|
||||
sudo -u "$RUNTIME_USER" -g "$RUNTIME_GROUP" git clone --bare "$SOURCE" "$BARE"
|
||||
fi
|
||||
|
||||
chown -R "$RUNTIME_USER:$RUNTIME_GROUP" "$BARE" "$WORKTREES"
|
||||
find "$BARE" "$WORKTREES" -type d -exec chmod 2775 {} +
|
||||
find "$BARE" "$WORKTREES" -type f -exec chmod ug+rw {} +
|
||||
|
||||
setfacl -R -m "u:${RUNTIME_USER}:rwX,g:${RUNTIME_GROUP}:rwX,m::rwX" "$BARE" "$WORKTREES"
|
||||
find "$BARE" "$WORKTREES" -type d -exec \
|
||||
setfacl -m "d:u:${RUNTIME_USER}:rwx,d:g:${RUNTIME_GROUP}:rwx,d:m::rwx" {} +
|
||||
|
||||
cat <<EOF
|
||||
bootstrap_status: PASS
|
||||
root: $ROOT
|
||||
source_worktree: $SOURCE
|
||||
bare_repo: $BARE
|
||||
worktree_root: $WORKTREES
|
||||
runtime_user: $RUNTIME_USER
|
||||
runtime_group: $RUNTIME_GROUP
|
||||
EOF
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
EXPECTED_USER="${AIOS_RUNTIME_USER:-aiman}"
|
||||
EXPECTED_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
|
||||
EXPECTED_UMASK="${AIOS_RUNTIME_UMASK:-0002}"
|
||||
TASK_DIR="${AIOS_TASK_DIR:-.agent/tasks}"
|
||||
RESULT_DIR="${AIOS_RESULT_DIR:-.agent/results}"
|
||||
|
||||
fail() {
|
||||
echo "preflight_status: FAIL"
|
||||
echo "reason: $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
USER_NOW="$(id -un)"
|
||||
GROUPS_NOW="$(id -Gn)"
|
||||
UMASK_NOW="$(umask)"
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
|
||||
BRANCH="$(git branch --show-current 2>/dev/null || true)"
|
||||
|
||||
[[ "$USER_NOW" == "$EXPECTED_USER" ]] || fail "runtime user is $USER_NOW, expected $EXPECTED_USER"
|
||||
[[ " $GROUPS_NOW " == *" $EXPECTED_GROUP "* ]] || fail "runtime user is not in $EXPECTED_GROUP"
|
||||
[[ "$UMASK_NOW" == "$EXPECTED_UMASK" || "$UMASK_NOW" == "002" ]] || fail "umask is $UMASK_NOW, expected $EXPECTED_UMASK"
|
||||
[[ -n "$ROOT" ]] || fail "not inside a git worktree"
|
||||
[[ "$ROOT" == /data/aios-gitops/worktrees/* ]] || fail "shared source worktree is prohibited: $ROOT"
|
||||
[[ -w "$ROOT" ]] || fail "worktree root is not writable"
|
||||
[[ -w "$GIT_DIR" ]] || fail "git dir is not writable: $GIT_DIR"
|
||||
[[ ! -e "$GIT_DIR/index.lock" ]] || fail "git index lock exists"
|
||||
install -d -m 2775 "$TASK_DIR" "$RESULT_DIR"
|
||||
[[ -w "$TASK_DIR" ]] || fail "task directory is not writable: $TASK_DIR"
|
||||
[[ -w "$RESULT_DIR" ]] || fail "result directory is not writable: $RESULT_DIR"
|
||||
[[ -z "$(git status --porcelain)" ]] || fail "worktree is not clean at task start"
|
||||
|
||||
cat <<EOF
|
||||
preflight_status: PASS
|
||||
runtime_user: $USER_NOW
|
||||
runtime_groups: $GROUPS_NOW
|
||||
umask: $UMASK_NOW
|
||||
worktree: $ROOT
|
||||
git_dir: $GIT_DIR
|
||||
branch: $BRANCH
|
||||
workspace_clean: true
|
||||
task_path_writable: true
|
||||
result_path_writable: true
|
||||
git_lock_present: false
|
||||
EOF
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ACTION="${1:-}"
|
||||
AGENT="${2:-}"
|
||||
TASK_ID="${3:-}"
|
||||
BASE_REF="${4:-origin/main}"
|
||||
|
||||
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
|
||||
BARE="${AIOS_BARE_REPO:-$ROOT/repo.git}"
|
||||
WORKTREES="${AIOS_WORKTREE_ROOT:-$ROOT/worktrees}"
|
||||
RUNTIME_USER="${AIOS_RUNTIME_USER:-aiman}"
|
||||
RUNTIME_GROUP="${AIOS_RUNTIME_GROUP:-gitops}"
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 create|remove <openclaw|chatgpt|controller> <task-id> [base-ref]" >&2
|
||||
exit 64
|
||||
}
|
||||
|
||||
[[ "$ACTION" =~ ^(create|remove)$ ]] || usage
|
||||
[[ "$AGENT" =~ ^(openclaw|chatgpt|controller)$ ]] || usage
|
||||
[[ "$TASK_ID" =~ ^[A-Za-z0-9._-]+$ ]] || {
|
||||
echo "ERROR: invalid task id" >&2
|
||||
exit 65
|
||||
}
|
||||
[[ "$(id -un)" == "$RUNTIME_USER" ]] || {
|
||||
echo "ERROR: must run as $RUNTIME_USER" >&2
|
||||
exit 66
|
||||
}
|
||||
[[ "$(id -gn)" == "$RUNTIME_GROUP" || " $(id -Gn) " == *" $RUNTIME_GROUP "* ]] || {
|
||||
echo "ERROR: user must be in $RUNTIME_GROUP" >&2
|
||||
exit 67
|
||||
}
|
||||
[[ "$(umask)" == "0002" || "$(umask)" == "002" ]] || {
|
||||
echo "ERROR: umask must be 0002" >&2
|
||||
exit 68
|
||||
}
|
||||
[[ -d "$BARE" ]] || {
|
||||
echo "ERROR: bare repository missing: $BARE" >&2
|
||||
exit 69
|
||||
}
|
||||
|
||||
PATHNAME="$WORKTREES/$AGENT/$TASK_ID"
|
||||
BRANCH="task/$AGENT/$TASK_ID"
|
||||
|
||||
case "$ACTION" in
|
||||
create)
|
||||
[[ ! -e "$PATHNAME" ]] || {
|
||||
echo "ERROR: worktree already exists: $PATHNAME" >&2
|
||||
exit 70
|
||||
}
|
||||
git --git-dir="$BARE" fetch --all --prune
|
||||
git --git-dir="$BARE" worktree add -b "$BRANCH" "$PATHNAME" "$BASE_REF"
|
||||
install -d -m 2775 "$PATHNAME/.agent/tasks/$AGENT" "$PATHNAME/.agent/results/$AGENT"
|
||||
setfacl -m "u:${RUNTIME_USER}:rwx,g:${RUNTIME_GROUP}:rwx,m::rwx" "$PATHNAME"
|
||||
find "$PATHNAME" -type d -exec chmod g+s {} +
|
||||
find "$PATHNAME" -type d -exec \
|
||||
setfacl -m "d:u:${RUNTIME_USER}:rwx,d:g:${RUNTIME_GROUP}:rwx,d:m::rwx" {} +
|
||||
printf 'worktree: %s\nbranch: %s\nbase: %s\n' "$PATHNAME" "$BRANCH" "$BASE_REF"
|
||||
;;
|
||||
remove)
|
||||
[[ -d "$PATHNAME" ]] || {
|
||||
echo "ERROR: worktree not found: $PATHNAME" >&2
|
||||
exit 71
|
||||
}
|
||||
[[ -z "$(git -C "$PATHNAME" status --porcelain)" ]] || {
|
||||
echo "ERROR: worktree is not clean; retain for investigation" >&2
|
||||
exit 72
|
||||
}
|
||||
git --git-dir="$BARE" worktree remove "$PATHNAME"
|
||||
printf 'removed: %s\n' "$PATHNAME"
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
os.environ.setdefault("AIOS_GITOPS_WORKSPACE", str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "runtime" / "agent-os"))
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"aios_chatgpt_actions_api",
|
||||
ROOT / "active-runtime" / "app" / "aios_chatgpt_actions_api.py",
|
||||
)
|
||||
assert SPEC and SPEC.loader
|
||||
api = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(api)
|
||||
|
||||
TARGET = "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24"
|
||||
EVIDENCE = "8cdca463aff9707edee00e20a3b56884130f6978"
|
||||
MAIN = "a281bfeacd990134dc2ebff5a7b9465756bc4bea"
|
||||
TAG_OID = "1" * 40
|
||||
|
||||
|
||||
class TagActionTests(unittest.TestCase):
|
||||
def handler(self, role: str = "owner"):
|
||||
handler = object.__new__(api.H)
|
||||
handler.need = Mock(return_value={"account": "owner", "role": role, "scopes": {"tag:create"}})
|
||||
handler.sendj = Mock(side_effect=lambda data, code=200: (data, code))
|
||||
return handler
|
||||
|
||||
def test_resolve_commit_oid_requires_full_exact_commit(self):
|
||||
with patch.object(api, "sh", return_value={"code": 0, "stdout": TARGET, "stderr": "", "cmd": []}):
|
||||
self.assertEqual(api.resolve_commit_oid(TARGET.upper()), TARGET)
|
||||
for value in (None, "3dd622b", "z" * 40):
|
||||
with self.subTest(value=value), self.assertRaises(ValueError):
|
||||
api.resolve_commit_oid(value)
|
||||
with patch.object(api, "sh", return_value={"code": 1, "stdout": "", "stderr": "missing", "cmd": []}), self.assertRaises(ValueError):
|
||||
api.resolve_commit_oid(TARGET)
|
||||
|
||||
def test_owner_execution_tags_explicit_target_without_backend_confirm_loop(self):
|
||||
handler = self.handler()
|
||||
git_calls = []
|
||||
|
||||
def fake_sh(args, check=False):
|
||||
git_calls.append(args)
|
||||
if args[:2] == ["merge-base", "--is-ancestor"]:
|
||||
return {"code": 0, "stdout": "", "stderr": "", "cmd": ["git", *args]}
|
||||
if args == ["rev-parse", "refs/heads/main"]:
|
||||
return {"code": 0, "stdout": MAIN, "stderr": "", "cmd": ["git", *args]}
|
||||
if args == ["rev-parse", "refs/tags/V2R1C2"]:
|
||||
return {"code": 0, "stdout": TAG_OID, "stderr": "", "cmd": ["git", *args]}
|
||||
if args == ["rev-parse", "HEAD"]:
|
||||
return {"code": 0, "stdout": EVIDENCE, "stderr": "", "cmd": ["git", *args]}
|
||||
return {"code": 0, "stdout": "", "stderr": "", "cmd": ["git", *args]}
|
||||
|
||||
with (
|
||||
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
|
||||
patch.object(api, "existing_tag_target", side_effect=[None, TARGET]),
|
||||
patch.object(api, "sh", side_effect=fake_sh),
|
||||
patch.object(api, "sync_tag", return_value={"status": "synced"}),
|
||||
patch.object(api, "audit") as audit,
|
||||
):
|
||||
handler.tag({
|
||||
"version": "V2R1C2", "target_oid": TARGET,
|
||||
"evidence_closure_oid": EVIDENCE, "approval_plan_id": "nl-approved",
|
||||
"execute": True, "message": "approved",
|
||||
})
|
||||
|
||||
self.assertIn(["tag", "-a", "V2R1C2", TARGET, "-m", "approved"], git_calls)
|
||||
payload = handler.sendj.call_args.args[0]
|
||||
self.assertEqual(payload["status"], "tagged")
|
||||
self.assertEqual(payload["target_oid"], TARGET)
|
||||
self.assertEqual(payload["tag_oid"], TAG_OID)
|
||||
self.assertFalse(payload["main_changed"])
|
||||
self.assertFalse(payload["force_push"])
|
||||
audit.assert_called_once()
|
||||
|
||||
def test_existing_matching_tag_is_idempotent(self):
|
||||
handler = self.handler()
|
||||
|
||||
def fake_sh(args, check=False):
|
||||
values = {
|
||||
("merge-base", "--is-ancestor", TARGET, EVIDENCE): "",
|
||||
("rev-parse", "refs/heads/main"): MAIN,
|
||||
("rev-parse", "refs/tags/V2R1C2"): TAG_OID,
|
||||
("rev-parse", "HEAD"): EVIDENCE,
|
||||
}
|
||||
return {"code": 0, "stdout": values.get(tuple(args), ""), "stderr": "", "cmd": ["git", *args]}
|
||||
|
||||
with (
|
||||
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
|
||||
patch.object(api, "existing_tag_target", side_effect=[TARGET, TARGET]),
|
||||
patch.object(api, "sh", side_effect=fake_sh) as git,
|
||||
patch.object(api, "sync_tag", return_value={"status": "synced"}),
|
||||
patch.object(api, "audit"),
|
||||
):
|
||||
handler.tag({"version": "V2R1C2", "target_oid": TARGET, "evidence_closure_oid": EVIDENCE, "execute": True})
|
||||
self.assertEqual(handler.sendj.call_args.args[0]["status"], "tagged_existing")
|
||||
self.assertFalse(any(c.args[0][0] == "tag" for c in git.call_args_list))
|
||||
|
||||
def test_existing_different_tag_fails_closed(self):
|
||||
handler = self.handler()
|
||||
wrong = "2" * 40
|
||||
with (
|
||||
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
|
||||
patch.object(api, "sh", return_value={"code": 0, "stdout": "", "stderr": "", "cmd": []}),
|
||||
patch.object(api, "existing_tag_target", return_value=wrong),
|
||||
patch.object(api, "audit"),
|
||||
):
|
||||
handler.tag({"version": "V2R1C2", "target_oid": TARGET, "evidence_closure_oid": EVIDENCE, "execute": True})
|
||||
payload, code = handler.sendj.call_args.args
|
||||
self.assertEqual(code, 409)
|
||||
self.assertEqual(payload["status"], "tag_target_conflict")
|
||||
self.assertEqual(payload["target_oid"], wrong)
|
||||
|
||||
def test_evidence_lineage_mismatch_fails_closed(self):
|
||||
handler = self.handler()
|
||||
with (
|
||||
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
|
||||
patch.object(api, "sh", return_value={"code": 1, "stdout": "", "stderr": "", "cmd": []}),
|
||||
):
|
||||
handler.tag({"version": "V2R1C2", "target_oid": TARGET, "evidence_closure_oid": EVIDENCE, "execute": True})
|
||||
payload, code = handler.sendj.call_args.args
|
||||
self.assertEqual(code, 409)
|
||||
self.assertEqual(payload["error"], "evidence_lineage_mismatch")
|
||||
|
||||
def test_non_owner_request_persists_target_binding(self):
|
||||
handler = self.handler(role="user")
|
||||
with (
|
||||
patch.object(api, "resolve_commit_oid", side_effect=[TARGET, EVIDENCE]),
|
||||
patch.object(api, "sh", return_value={"code": 0, "stdout": "", "stderr": "", "cmd": []}),
|
||||
patch.object(api, "checkout", return_value="request/tag-v2r1c2"),
|
||||
patch.object(api, "write_json", return_value="baseline/tag_requests/V2R1C2.json") as write_json,
|
||||
patch.object(api, "commit", return_value={"status": "committed"}),
|
||||
):
|
||||
handler.tag({"version": "V2R1C2", "target_oid": TARGET, "evidence_closure_oid": EVIDENCE, "approval_plan_id": "nl-approved", "execute": True})
|
||||
request = write_json.call_args.args[1]
|
||||
self.assertEqual(request["target_oid"], TARGET)
|
||||
self.assertEqual(request["evidence_closure_oid"], EVIDENCE)
|
||||
self.assertEqual(request["approval_plan_id"], "nl-approved")
|
||||
|
||||
def test_openapi_requires_target_and_keeps_platform_confirmation(self):
|
||||
schema = (ROOT / "docs" / "gitops" / "openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
|
||||
self.assertIn("required: [version, target_oid]", schema)
|
||||
self.assertIn("x-openai-isConsequential: true", schema)
|
||||
self.assertIn("never implicitly at workspace HEAD", schema)
|
||||
|
||||
def test_real_git_tag_targets_approved_commit_not_newer_workspace_head(self):
|
||||
def run(cwd: Path, *args: str) -> str:
|
||||
return subprocess.run(["git", *args], cwd=cwd, check=True, text=True, capture_output=True).stdout.strip()
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
bare = root / "repo.git"
|
||||
work = root / "work"
|
||||
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
|
||||
subprocess.run(["git", "init", "-b", "main", str(work)], check=True, capture_output=True)
|
||||
run(work, "config", "user.email", "test@example.invalid")
|
||||
run(work, "config", "user.name", "Tag Test")
|
||||
(work / "release.txt").write_text("candidate\n", encoding="utf-8")
|
||||
run(work, "add", "release.txt"); run(work, "commit", "-m", "candidate")
|
||||
target = run(work, "rev-parse", "HEAD")
|
||||
(work / "evidence.txt").write_text("closure\n", encoding="utf-8")
|
||||
run(work, "add", "evidence.txt"); run(work, "commit", "-m", "evidence")
|
||||
evidence = run(work, "rev-parse", "HEAD")
|
||||
run(work, "remote", "add", "origin", str(bare)); run(work, "push", "origin", "main")
|
||||
old_ws, old_bare = api.WS, api.BARE_REPO
|
||||
api.WS, api.BARE_REPO = work, bare
|
||||
try:
|
||||
handler = self.handler()
|
||||
with patch.object(api, "audit"):
|
||||
handler.tag({"version": "V9R9C9", "target_oid": target, "evidence_closure_oid": evidence, "execute": True, "message": "sandbox"})
|
||||
payload = handler.sendj.call_args.args[0]
|
||||
self.assertEqual(payload["target_oid"], target)
|
||||
self.assertEqual(run(work, "rev-parse", "HEAD"), evidence)
|
||||
self.assertEqual(run(work, "rev-parse", "V9R9C9^{}"), target)
|
||||
self.assertEqual(subprocess.run(["git", "--git-dir", str(bare), "rev-parse", "V9R9C9^{}"], check=True, text=True, capture_output=True).stdout.strip(), target)
|
||||
finally:
|
||||
api.WS, api.BARE_REPO = old_ws, old_bare
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
raise SystemExit('usage: run_online_smoke_18920.py <source-root> <output>')
|
||||
source_root = Path(sys.argv[1]).resolve()
|
||||
script = source_root / 'tools' / 'aios_v2r1c2_online_smoke.py'
|
||||
spec = importlib.util.spec_from_file_location('v2r1c2_online_smoke_production', script)
|
||||
if spec is None or spec.loader is None:
|
||||
raise SystemExit('unable to load approved online smoke harness')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
module.BASE = 'http://127.0.0.1:18920'
|
||||
raise SystemExit(module.main(sys.argv[2]))
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": "V2R1C1",
|
||||
"codename": "openclaw-command-v2-production-baseline",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"tag": "V2R1C1",
|
||||
"tag_ref": "refs/tags/V2R1C1",
|
||||
"tag_object": "4e0e6dcb90e6732aa9838fe9d0af0b34746829b0",
|
||||
"previous_version": "V1R4C6",
|
||||
"rollback_version": "V1R4C6",
|
||||
"baseline": "V1R4C6",
|
||||
"release_reason": "Register the already deployed AIOS V2R1C1 release, immutable tag, OpenClaw Command v2 companion, and final production evidence in the central GitOps Registry.",
|
||||
"purpose": "Close the central version-governance gap without changing product implementation, the V2R1C1 tag, production release, deployment, or traffic.",
|
||||
"changes": [
|
||||
"governance: add the canonical V2R1C1 version manifest",
|
||||
"governance: advance the central baseline pointer from V1R4C6 to V2R1C1",
|
||||
"governance: link release, companion, artifact, validation, and final audit evidence"
|
||||
],
|
||||
"artifacts": [
|
||||
"docs/releases/V2R1C1-BASELINE-CLOSURE.md",
|
||||
".agent/results/releases/V2R1C1-BASELINE-CLOSURE.json",
|
||||
".agent/results/releases/V2R1C1-BASELINE-CLOSURE.md",
|
||||
"/var/lib/aios-gitops/acceptance/aios-v2r1c1-openclaw-v2-companion-20260721T094442Z"
|
||||
],
|
||||
"validated_actions": [
|
||||
"release and tag OID equality",
|
||||
"local and Gitea ref synchronization",
|
||||
"OpenClaw Command v2 positive and negative validation",
|
||||
"Gate A and Gate B",
|
||||
"Hermes lifecycle 17/17",
|
||||
"grant isolation 11/11",
|
||||
"restart recovery",
|
||||
"rollback and forward recovery",
|
||||
"SQLite quick_check",
|
||||
"secret scan with zero violations",
|
||||
"final production healthcheck"
|
||||
],
|
||||
"secret_plaintext_written": false,
|
||||
"owner_approval_required": true,
|
||||
"owner_notification_channels": [
|
||||
"codex_owner_instruction",
|
||||
"gitops_audit"
|
||||
],
|
||||
"constraints_original": {
|
||||
"product_code_changed": false,
|
||||
"tag_changed": false,
|
||||
"production_redeployed": false,
|
||||
"production_traffic_changed": false,
|
||||
"force_push_allowed": false,
|
||||
"incubator_in_release_graph": false
|
||||
},
|
||||
"promoted_at": "2026-07-21T14:17:05Z",
|
||||
"promoted_by": "chatgpt/owner-approved-governance",
|
||||
"created_at": "2026-07-21T14:17:05Z",
|
||||
"author": "chatgpt"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": "V2R1C2",
|
||||
"codename": "unified-role-aware-portal",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"tag": "V2R1C2",
|
||||
"tag_ref": "refs/tags/V2R1C2",
|
||||
"tag_object": "a3bd84ebec9c705f7c9c5c03f7f4ec55f8854e36",
|
||||
"target_oid": "3dd622b5a515f1fa4ef72b9f8ae9ce48ac703f24",
|
||||
"evidence_closure_oid": "8cdca463aff9707edee00e20a3b56884130f6978",
|
||||
"previous_version": "V2R1C1",
|
||||
"rollback_version": "V2R1C1",
|
||||
"baseline": "V2R1C1",
|
||||
"release_run_id": "AIOS-V2R1C2-PRODUCTION-20260722T011836Z",
|
||||
"release_reason": "Register the Owner-approved V2R1C2 candidate tag and verified evidence before a separately gated production promotion.",
|
||||
"candidate_url": "http://127.0.0.1:18921",
|
||||
"production_url": "http://127.0.0.1:18920",
|
||||
"aggregate_gate": "PASS",
|
||||
"required_stages": "3/3 SUCCEEDED",
|
||||
"evidence_status": "COMPLETE",
|
||||
"evidence_manifest_sha256": "2e27bb73b8e6297bd3077238f73a96d697fc3c3611c601c4dc9357e5fdcd8eee",
|
||||
"evidence_checksums_sha256": "3dcedaff367d5d1a114640d8918304d1c39154d38b078f7f0b097a95d43978be",
|
||||
"prepromotion_acceptance_sha256": "402e9ce7690654ea7fd493985284446365a827b61fe195576dc7f4e4d4499ef7",
|
||||
"audited_tag_plan": "nl-1784681185786-19ac0f82",
|
||||
"owner_release_authorization": "GRANTED",
|
||||
"production_acceptance_sha256": "a6c35e12cab7907ab6bcfd37d781b3d216f05a5ae002aa54a3112c70f43b991a",
|
||||
"production_online_smoke_sha256": "fe9c6964e440f8871f2eb225f0132f70c634923389f7b8dfa85f615b100980a4",
|
||||
"production_health_sha256": "ee8084e8b57d188dc0039a276f4f3f1ed7025ff58ce91b111cc765627eb52d4c",
|
||||
"production_service": "aios-v2r1c2-production.service",
|
||||
"production_promoted": true,
|
||||
"main_changed_by_tag": false,
|
||||
"main_changed_by_authorized_registry": true,
|
||||
"force_push": false,
|
||||
"constraints": {
|
||||
"tag_rewrite_allowed": false,
|
||||
"force_push_allowed": false,
|
||||
"rollback_assets_must_be_preserved": true,
|
||||
"production_promotion_requires_fresh_acceptance": true
|
||||
},
|
||||
"registered_at": "2026-07-22T01:18:36Z",
|
||||
"promoted_at": "2026-07-22T01:40:51Z",
|
||||
"updated_at": "2026-07-22T01:43:15Z",
|
||||
"registered_by": "codex/owner-approved-gitops",
|
||||
"promoted_by": "codex/owner-approved-gitops"
|
||||
}
|
||||
Reference in New Issue
Block a user