Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
174799eed2 | ||
|
|
940a31515d | ||
|
|
f3a6ed42af | ||
|
|
1824699c59 | ||
|
|
b99d881d26 |
@@ -0,0 +1,63 @@
|
|||||||
|
cmd_id: CMD-20260710-memory-index-rebuild
|
||||||
|
operation: memory.index.rebuild
|
||||||
|
risk: medium
|
||||||
|
permission: OWNER_NON_DANGEROUS_ALL
|
||||||
|
status: pending_owner_gate
|
||||||
|
|
||||||
|
reason: >
|
||||||
|
Memory search is disabled because the existing embedding index metadata
|
||||||
|
does not match the current embedding provider configuration. Rebuild the
|
||||||
|
memory index using the current provider configuration.
|
||||||
|
|
||||||
|
requested_by: codex-chartgpt-bridge
|
||||||
|
requested_at: "2026-07-10T08:00:00+08:00"
|
||||||
|
|
||||||
|
command_key: openclaw.memory.index.force
|
||||||
|
args:
|
||||||
|
force: true
|
||||||
|
|
||||||
|
safety:
|
||||||
|
destructive: false
|
||||||
|
arbitrary_shell: false
|
||||||
|
credential_mutation: false
|
||||||
|
provider_mutation: false
|
||||||
|
external_network_write: false
|
||||||
|
production_data_delete: false
|
||||||
|
baseline_or_tag_mutation: false
|
||||||
|
main_branch_direct_write: false
|
||||||
|
|
||||||
|
owner_gate:
|
||||||
|
required: true
|
||||||
|
approval_model: protected_branch_merge_or_signed_owner_commit
|
||||||
|
approved_by: null
|
||||||
|
approved_at: null
|
||||||
|
approval_commit: null
|
||||||
|
note: >
|
||||||
|
Owner has granted broad permission for non-dangerous operations, but this
|
||||||
|
request still requires the configured owner gate before runner execution.
|
||||||
|
|
||||||
|
execution:
|
||||||
|
runner: owner-maintenance-runner
|
||||||
|
lock_key: memory-index-rebuild
|
||||||
|
timeout_seconds: 1800
|
||||||
|
executed_by: null
|
||||||
|
executed_at: null
|
||||||
|
exit_code: null
|
||||||
|
|
||||||
|
acceptance:
|
||||||
|
commands:
|
||||||
|
- openclaw memory search "V1R4C3"
|
||||||
|
- openclaw memory search "Gitea Branch Sync"
|
||||||
|
- openclaw memory search "Autonomous Controller"
|
||||||
|
expected:
|
||||||
|
- no_embedding_metadata_mismatch
|
||||||
|
- memory_search_returns_results
|
||||||
|
- memory_index_uses_current_embedding_provider
|
||||||
|
|
||||||
|
success_signals:
|
||||||
|
- AIOS_OWNER_MAINTENANCE_MEMORY_INDEX_REBUILT
|
||||||
|
- AIOS_MEMORY_SEARCH_RESTORED
|
||||||
|
|
||||||
|
rollback:
|
||||||
|
destructive: false
|
||||||
|
strategy: rebuild_previous_index_if_snapshot_exists
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Owner Maintenance Gate
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Allow the owner to approve and execute non-dangerous maintenance operations without granting agents arbitrary host CLI access.
|
||||||
|
|
||||||
|
## Permission model
|
||||||
|
|
||||||
|
- `READ_ONLY`: status, health, baseline, versions, and search inspection.
|
||||||
|
- `AUTO_PATCH`: branch creation, repository file changes, patch commits, and test/report artifacts.
|
||||||
|
- `OWNER_NON_DANGEROUS_ALL`: owner-approved non-destructive maintenance operations, only through allowlisted command keys.
|
||||||
|
- `OWNER_HIGH_RISK`: merge to main, baseline promotion, tag creation, credential/provider mutation, destructive cleanup, production restart, SSH/firewall changes.
|
||||||
|
|
||||||
|
## Key rule
|
||||||
|
|
||||||
|
Agents must not execute arbitrary shell commands. They may create owner requests. The runner may execute only fixed allowlist entries.
|
||||||
|
|
||||||
|
Example safe mapping:
|
||||||
|
|
||||||
|
```text
|
||||||
|
command_key: openclaw.memory.index.force
|
||||||
|
argv: /usr/local/bin/openclaw memory index --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Unsafe patterns are rejected:
|
||||||
|
|
||||||
|
```text
|
||||||
|
bash -c ...
|
||||||
|
sh -c ...
|
||||||
|
rm -rf ...
|
||||||
|
curl ... | bash
|
||||||
|
arbitrary command strings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Owner approval
|
||||||
|
|
||||||
|
The default owner gate is protected-branch merge or a signed owner commit that fills:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
owner_gate:
|
||||||
|
approved_by: <owner>
|
||||||
|
approved_at: <timestamp>
|
||||||
|
approval_commit: <signed-owner-commit-sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
Patch branch approval metadata is not enough unless the runner explicitly trusts that branch.
|
||||||
|
|
||||||
|
## Memory index rebuild flow
|
||||||
|
|
||||||
|
1. Agent detects embedding metadata mismatch.
|
||||||
|
2. Agent creates `commands/owner-requests/CMD-20260710-memory-index-rebuild.yaml`.
|
||||||
|
3. Owner approves through protected GitOps gate.
|
||||||
|
4. `owner-maintenance-runner` validates safety metadata and allowlist mapping.
|
||||||
|
5. Runner executes `openclaw.memory.index.force`.
|
||||||
|
6. Runner records exit code and stdout/stderr hashes.
|
||||||
|
7. Acceptance checks verify memory search works again.
|
||||||
|
|
||||||
|
Expected success signals:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AIOS_OWNER_MAINTENANCE_MEMORY_INDEX_REBUILT
|
||||||
|
AIOS_MEMORY_SEARCH_RESTORED
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scope boundaries
|
||||||
|
|
||||||
|
Owner has broad permission for non-dangerous operations, but non-dangerous still means:
|
||||||
|
|
||||||
|
- no destructive data deletion
|
||||||
|
- no arbitrary shell
|
||||||
|
- no credential/provider mutation
|
||||||
|
- no baseline/tag mutation
|
||||||
|
- no direct main write
|
||||||
|
- no SSH/firewall mutation
|
||||||
|
- no production service restart unless separately allowlisted and classified
|
||||||
|
|
||||||
|
High-risk operations remain owner-gated with extra review.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "branch",
|
||||||
|
"branch": "feature/v1r4c4-owner-maintenance-gate-20260710",
|
||||||
|
"base": "main",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"created_at": "2026-07-10T10:31:12Z",
|
||||||
|
"direct_main_push": false,
|
||||||
|
"owner_approval_required": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
version: 1
|
||||||
|
name: owner-maintenance-gate
|
||||||
|
baseline: V1R4C3
|
||||||
|
created_at: "2026-07-10"
|
||||||
|
purpose: >
|
||||||
|
Define owner-controlled permissions for non-dangerous maintenance actions
|
||||||
|
without granting arbitrary shell access to agents or automation.
|
||||||
|
|
||||||
|
principles:
|
||||||
|
- agents_may_detect_and_request
|
||||||
|
- owner_may_approve_non_dangerous_actions
|
||||||
|
- runner_executes_only_allowlisted_command_keys
|
||||||
|
- no_arbitrary_shell
|
||||||
|
- protected_branch_merge_is_the_default_owner_gate
|
||||||
|
- every_execution_must_be_audited
|
||||||
|
|
||||||
|
permission_tiers:
|
||||||
|
READ_ONLY:
|
||||||
|
description: Status, health, baseline, version, and search inspection.
|
||||||
|
agent_allowed: true
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: false
|
||||||
|
|
||||||
|
AUTO_PATCH:
|
||||||
|
description: Create branches, write repo files, run tests where available, and commit patch artifacts.
|
||||||
|
agent_allowed: true
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: false
|
||||||
|
|
||||||
|
OWNER_NON_DANGEROUS_ALL:
|
||||||
|
description: >
|
||||||
|
Owner-approved, non-destructive maintenance actions. This tier grants owner
|
||||||
|
permission to execute all commands classified as non-dangerous, provided
|
||||||
|
they are represented by explicit allowlist command_key entries and pass
|
||||||
|
safety classification checks.
|
||||||
|
agent_allowed: false
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: true
|
||||||
|
approval_model: protected_branch_merge_or_signed_owner_commit
|
||||||
|
|
||||||
|
OWNER_HIGH_RISK:
|
||||||
|
description: Main merge, baseline promotion, tag creation, credential/provider mutation, destructive cleanup, or production restart.
|
||||||
|
agent_allowed: false
|
||||||
|
owner_allowed: true
|
||||||
|
requires_owner_gate: true
|
||||||
|
extra_review_required: true
|
||||||
|
|
||||||
|
safety_classification:
|
||||||
|
non_dangerous_requirements:
|
||||||
|
- destructive: false
|
||||||
|
- arbitrary_shell: false
|
||||||
|
- credential_mutation: false
|
||||||
|
- provider_mutation: false
|
||||||
|
- external_network_write: false
|
||||||
|
- production_data_delete: false
|
||||||
|
- baseline_or_tag_mutation: false
|
||||||
|
- main_branch_direct_write: false
|
||||||
|
- command_key_must_be_allowlisted: true
|
||||||
|
|
||||||
|
always_high_risk:
|
||||||
|
- merge_to_main
|
||||||
|
- baseline_promotion
|
||||||
|
- tag_creation
|
||||||
|
- credential_rotation
|
||||||
|
- embedding_provider_change
|
||||||
|
- database_delete_or_drop
|
||||||
|
- rm_recursive_or_force
|
||||||
|
- service_restart_production
|
||||||
|
- firewall_or_ssh_config_change
|
||||||
|
- arbitrary_bash_or_shell
|
||||||
|
|
||||||
|
allowlist:
|
||||||
|
openclaw.memory.index.force:
|
||||||
|
permission: OWNER_NON_DANGEROUS_ALL
|
||||||
|
risk: medium
|
||||||
|
destructive: false
|
||||||
|
command_vector:
|
||||||
|
- /usr/local/bin/openclaw
|
||||||
|
- memory
|
||||||
|
- index
|
||||||
|
- --force
|
||||||
|
timeout_seconds: 1800
|
||||||
|
lock_key: memory-index-rebuild
|
||||||
|
reason_required: true
|
||||||
|
acceptance:
|
||||||
|
- openclaw memory search "V1R4C3"
|
||||||
|
- openclaw memory search "Gitea Branch Sync"
|
||||||
|
- openclaw memory search "Autonomous Controller"
|
||||||
|
success_signals:
|
||||||
|
- AIOS_OWNER_MAINTENANCE_MEMORY_INDEX_REBUILT
|
||||||
|
- AIOS_MEMORY_SEARCH_RESTORED
|
||||||
|
|
||||||
|
audit_requirements:
|
||||||
|
required_fields:
|
||||||
|
- cmd_id
|
||||||
|
- operation
|
||||||
|
- command_key
|
||||||
|
- requested_by
|
||||||
|
- requested_at
|
||||||
|
- approved_by
|
||||||
|
- approved_at
|
||||||
|
- approval_commit
|
||||||
|
- executed_by
|
||||||
|
- executed_at
|
||||||
|
- exit_code
|
||||||
|
- stdout_sha256
|
||||||
|
- stderr_sha256
|
||||||
|
- validation_status
|
||||||
|
|
||||||
|
status: proposed
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Owner maintenance allowlist runner.
|
||||||
|
|
||||||
|
This runner intentionally does not execute arbitrary shell strings. It resolves
|
||||||
|
approved command_key values to fixed argv vectors and enforces safety metadata
|
||||||
|
before execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml # type: ignore
|
||||||
|
except Exception: # pragma: no cover - runtime dependency guard
|
||||||
|
yaml = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AllowedCommand:
|
||||||
|
command_key: str
|
||||||
|
argv: tuple[str, ...]
|
||||||
|
permission: str
|
||||||
|
risk: str
|
||||||
|
destructive: bool
|
||||||
|
timeout_seconds: int
|
||||||
|
lock_key: str
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_COMMANDS: dict[str, AllowedCommand] = {
|
||||||
|
"openclaw.memory.index.force": AllowedCommand(
|
||||||
|
command_key="openclaw.memory.index.force",
|
||||||
|
argv=("/usr/local/bin/openclaw", "memory", "index", "--force"),
|
||||||
|
permission="OWNER_NON_DANGEROUS_ALL",
|
||||||
|
risk="medium",
|
||||||
|
destructive=False,
|
||||||
|
timeout_seconds=1800,
|
||||||
|
lock_key="memory-index-rebuild",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
DENY_FLAGS = {
|
||||||
|
"arbitrary_shell",
|
||||||
|
"credential_mutation",
|
||||||
|
"provider_mutation",
|
||||||
|
"external_network_write",
|
||||||
|
"production_data_delete",
|
||||||
|
"baseline_or_tag_mutation",
|
||||||
|
"main_branch_direct_write",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_text(value: str) -> str:
|
||||||
|
return hashlib.sha256(value.encode("utf-8", errors="replace")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def load_request(path: Path) -> dict[str, Any]:
|
||||||
|
if yaml is None:
|
||||||
|
raise RuntimeError("PyYAML is required to load owner request YAML")
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("owner request must be a YAML object")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def validate_request(req: dict[str, Any]) -> AllowedCommand:
|
||||||
|
command_key = req.get("command_key")
|
||||||
|
if command_key not in ALLOWED_COMMANDS:
|
||||||
|
raise PermissionError(f"command_key is not allowlisted: {command_key}")
|
||||||
|
|
||||||
|
allowed = ALLOWED_COMMANDS[command_key]
|
||||||
|
if req.get("permission") != allowed.permission:
|
||||||
|
raise PermissionError("request permission does not match allowlist")
|
||||||
|
if bool(req.get("safety", {}).get("destructive", True)) != allowed.destructive:
|
||||||
|
raise PermissionError("request destructive flag does not match allowlist")
|
||||||
|
|
||||||
|
safety = req.get("safety", {})
|
||||||
|
if not isinstance(safety, dict):
|
||||||
|
raise PermissionError("safety metadata is required")
|
||||||
|
denied = [flag for flag in DENY_FLAGS if safety.get(flag) is True]
|
||||||
|
if denied:
|
||||||
|
raise PermissionError(f"request contains denied safety flags: {denied}")
|
||||||
|
|
||||||
|
gate = req.get("owner_gate", {})
|
||||||
|
if not isinstance(gate, dict) or gate.get("required") is not True:
|
||||||
|
raise PermissionError("owner_gate.required must be true")
|
||||||
|
|
||||||
|
if not gate.get("approved_by") or not gate.get("approved_at") or not gate.get("approval_commit"):
|
||||||
|
raise PermissionError("owner approval fields are required before execution")
|
||||||
|
|
||||||
|
return allowed
|
||||||
|
|
||||||
|
|
||||||
|
def run_allowed_command(allowed: AllowedCommand, dry_run: bool) -> dict[str, Any]:
|
||||||
|
if dry_run:
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"argv": list(allowed.argv),
|
||||||
|
"exit_code": None,
|
||||||
|
"stdout_sha256": None,
|
||||||
|
"stderr_sha256": None,
|
||||||
|
"executed_at": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
proc = subprocess.run(
|
||||||
|
list(allowed.argv),
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=allowed.timeout_seconds,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"dry_run": False,
|
||||||
|
"argv": list(allowed.argv),
|
||||||
|
"exit_code": proc.returncode,
|
||||||
|
"stdout_sha256": sha256_text(proc.stdout),
|
||||||
|
"stderr_sha256": sha256_text(proc.stderr),
|
||||||
|
"executed_at": utc_now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Run an owner-approved allowlisted maintenance request")
|
||||||
|
parser.add_argument("request", type=Path, help="Path to owner request YAML")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Validate only; do not execute")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = load_request(args.request)
|
||||||
|
allowed = validate_request(req)
|
||||||
|
result = run_allowed_command(allowed, dry_run=args.dry_run)
|
||||||
|
print(json.dumps({"status": "ok", "command_key": allowed.command_key, "result": result}, indent=2))
|
||||||
|
return 0 if result.get("exit_code") in (None, 0) else int(result["exit_code"])
|
||||||
|
except Exception as exc:
|
||||||
|
print(json.dumps({"status": "error", "error": str(exc)}, indent=2), file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user