Compare commits

..
8 changed files with 412 additions and 372 deletions
@@ -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
+77
View File
@@ -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.
@@ -1,64 +0,0 @@
# V1R4C4 Integration Plan
## Goal
Create the next reliability-focused baseline after V1R4C3.
The integration goal is not bulk patch merging. It is controlled consolidation.
## Phase 1: P0 Reliability Integration
Merge or integrate:
- Gitea branch consistency sentinel.
- Autonomous safe-mode gate.
- Controller mutation guard.
- Gitea incident response documentation.
- Branch metadata drift prevention.
Acceptance:
- Gitea refs, branch metadata and API state remain consistent.
- Runtime acceptance sequence succeeds.
## Phase 2: P1 Capability Review
Review for consolidation:
- autonomous controller dispatch;
- agent event bus;
- GPT message plane;
- observability capabilities;
- governance/isolation changes;
- search gateway improvements.
Each item requires:
- diff review;
- duplicate detection;
- regression check;
- baseline compatibility decision.
## Phase 3: P2 Cleanup
Archive or supersede:
- smoke tests;
- acceptance-only branches;
- auth verification branches;
- temporary diagnostics;
- historical promotion requests.
## Completion Criteria
The integration is complete when:
1. No patch branch has undefined status.
2. Reliability controls are part of the active baseline.
3. Historical branches are archived with reason.
4. Acceptance tests pass.
5. Future autonomous changes are protected by lifecycle controls.
## Safety Rule
Do not trade reliability for patch count reduction. A clean backlog with incorrect merges is worse than a larger but controlled backlog.
@@ -1,138 +0,0 @@
# Patch Lifecycle Reconciliation Inventory - 2026-07-10
Baseline at reconciliation: `V1R4C3`.
Current branch scan found:
- `61` branches under `patch/*`
- `75` non-main operational branches
- `5` P0 merge candidates
- `16` P1 keep-open review candidates
- `14` P2 superseded branches
- `40` P2 archive branches
Note: the user-reported backlog was 59. The current runtime scan shows 61 `patch/*` branches and 75 non-main operational branches. This inventory records the current observed state.
## Execution Decision
Do not bulk merge.
Execute lifecycle reconciliation:
```text
P0 -> controlled merge into V1R4C4 reliability integration
P1 -> diff review, consolidate or supersede
P2 superseded -> do not merge
P2 archive -> preserve evidence, close/mark archived after owner-safe cleanup
```
## P0 Merge Candidates
| Branch | Status | Action |
|---|---|---|
| `feature/gitea-reliability-guardrails-20260710` | `merge` | P0 reliability guardrails required for V1R4C4 |
| `incident/gitea-autonomous-ops-stop-20260710` | `merge` | P0 incident record required for V1R4C4 |
| `ops/patch-lifecycle-reconciliation-20260710` | `merge` | P0 lifecycle governance required before backlog cleanup |
| `patch/autonomous-controller-systemd-health-20260709` | `merge` | P0 autonomous controller health guardrails |
| `patch/gitea-branch-index-sync` | `merge` | P0 branch metadata drift recovery/prevention |
## P1 Keep-Open Review Candidates
| Branch | Status | Action |
|---|---|---|
| `patch/agent-event-bus-unified` | `keep-open` | P1 capability consolidation candidate |
| `patch/agent-gpt-message-plane` | `keep-open` | P1 capability consolidation candidate |
| `patch/agent-observability-webui` | `keep-open` | P1 capability consolidation candidate |
| `patch/aios-autonomous-orchestrator-architecture-review-v1` | `keep-open` | P1 architecture review candidate |
| `patch/autonomous-controller-dispatch-20260709` | `keep-open` | P1 controller dispatch candidate; verify against P0 guardrails |
| `patch/chatgpt-actions-consequential-governance` | `keep-open` | P1 governance candidate |
| `patch/fix-tag-confirmation-state-machine` | `keep-open` | P1 governance fix candidate |
| `patch/gitops-change-archive-audit` | `keep-open` | P1 audit/governance candidate |
| `patch/gpt-actions-consequential-governance` | `keep-open` | P1 governance candidate |
| `patch/group-owner-agent-context-isolation` | `keep-open` | P1 isolation candidate |
| `patch/search-gateway-governed-general-capability` | `keep-open` | P1 capability candidate |
| `patch/self-evolution-agent-orchestration-policy` | `keep-open` | P1 governance policy candidate |
| `patch/user-portal-rbac-ops-dashboard` | `keep-open` | P1 portal capability candidate |
| `patch/user-profile-isolation` | `keep-open` | P1 isolation candidate |
| `patch/v1r4c1-review-risk-fixes` | `keep-open` | P1 review risk-fix candidate; verify if superseded by V1R4C3 |
| `patch/web-ui-complete-gitops-dashboard-20260706` | `keep-open` | P1 UI capability candidate; verify overlap with portal/dashboard patches |
## P2 Superseded
| Branch | Status | Action |
|---|---|---|
| `patch/V1R2C1-autonomous-orchestrator-foundation` | `superseded` | Superseded by V1R4C3 autonomous operations baseline unless diff proves otherwise |
| `patch/V1R2C2-autonomous-recovery-chain-integration` | `superseded` | Superseded by V1R4C3 autonomous operations baseline unless diff proves otherwise |
| `patch/V1R2C2-autonomous-recovery-loop` | `superseded` | Superseded by V1R4C3 autonomous operations baseline unless diff proves otherwise |
| `patch/V1R2C2-system-supervisor-recovery-v1` | `superseded` | Superseded by V1R4C3 autonomous operations baseline unless diff proves otherwise |
| `patch/V1R2C3-custom-gpt-schema-rollback` | `superseded` | Superseded by current GPT Actions runtime acceptance |
| `patch/V1R2C3-gpt-actions-clientresponse-compat` | `superseded` | Superseded by current GPT Actions runtime acceptance |
| `patch/V1R2C3-revert-confirm-governance-test` | `superseded` | Superseded by current owner-gate governance |
| `patch/V1R2C3-tag-approval-action-governance` | `superseded` | Superseded by current owner-gate governance unless diff proves otherwise |
| `patch/V1R2C3-tag-execute-idempotent-permission` | `superseded` | Superseded by current owner-gate governance unless diff proves otherwise |
| `patch/collab-custom-gpt-20260706T104130Z` | `superseded` | Superseded by self-evolution orchestration policy and later baselines |
| `patch/collab-local-codex-20260706T104130Z` | `superseded` | Superseded by self-evolution orchestration policy and later baselines |
| `patch/gitops-write-auth-fix` | `superseded` | Superseded by current successful runtime write acceptance |
| `patch/user-portal-rbac-ops-dashboard-v1r2c0` | `superseded` | Superseded by later user portal RBAC ops branches |
| `patch/v1r3c0-user-portal-rbac-ops-20260708` | `superseded` | Superseded by V1R4C3 or later portal/dashboard candidates unless diff proves otherwise |
## P2 Archive
| Branch | Status | Action |
|---|---|---|
| `check/gitops-actions-loaded-20260708` | `archive` | Non-production verification/request branch; retain as evidence only |
| `patch/actions-double-bearer-20260706T134646Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/actions-final-smoke-20260706T135238Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/actions-invalid-diag-smoke-20260706T140311Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/actions-raw-token-smoke-20260706T134706Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/bridge-write-auth-smoke-20260706T132506Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/chatgpt-actions-acceptance-20260706T093843Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/chatgpt-actions-acceptance-20260706T094207Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/chatgpt-br-1783496324783-496c38aa` | `archive` | Temporary diagnostic/autogenerated branch; do not merge unless separately revalidated |
| `patch/chatgpt-model-acceptance-20260706T093843Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/chatgpt-model-acceptance-20260706T094207Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/codex-gitops-acceptance-20260706T102314Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/codex-online-sync-acceptance-20260706T102819Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/codex-write-auth-smoke-20260706T115256Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gitea-runtime-acceptance-20260710` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gitops-write-auth-fix-smoke-20260706T114818Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gitops-write-auth-smoke-20260706T115230Z` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-auth-live-20260708` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-auth-patch-test` | `archive` | Archive pending diff; not in P0/P1 integration path |
| `patch/gpt-actions-auth-test` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-auth-test-1783429590` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-commitfile-test` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-live-verify-v1r2c3-20260708` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-model-priority-test` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-nl-plan-test` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/gpt-actions-rollback-request-test` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/user-portal-me-route-diagnostics-20260709` | `archive` | Temporary diagnostic/autogenerated branch; do not merge unless separately revalidated |
| `patch/v1r2c3-baseline-promotion-20260708` | `archive` | Archive pending diff; not in P0/P1 integration path |
| `patch/v1r2c3-final-smoke-20260708` | `archive` | Smoke/acceptance/auth/diagnostic branch; do not merge into main |
| `patch/v1r2c3-promotion-request-20260708` | `archive` | Archive pending diff; not in P0/P1 integration path |
| `promote/V1R3C1-baseline` | `archive` | Non-production verification/request branch; retain as evidence only |
| `request/tag-V1R2C3` | `archive` | Non-production verification/request branch; retain as evidence only |
| `request/tag-V1R3C0-self-loop-autonomous-engine` | `archive` | Non-production verification/request branch; retain as evidence only |
| `request/tag-V1R3C1-self-loop-corrective` | `archive` | Non-production verification/request branch; retain as evidence only |
| `test/alias-test-branch` | `archive` | Non-production verification/request branch; retain as evidence only |
| `test/alias-verify` | `archive` | Non-production verification/request branch; retain as evidence only |
| `test/alias-write-verify` | `archive` | Non-production verification/request branch; retain as evidence only |
| `test/cmd-write-permission-verify-v2` | `archive` | Non-production verification/request branch; retain as evidence only |
| `test/diag-branch` | `archive` | Non-production verification/request branch; retain as evidence only |
| `test/gitea-runtime-acceptance-20260710` | `archive` | Non-production verification/request branch; retain as evidence only |
## Required Closeout Actions
1. Merge/integrate only the P0 reliability items after diff review.
2. Review P1 candidates and convert each to either `merge` or `superseded`.
3. Mark P2 branches as archived/superseded in GitOps records.
4. Do not delete remote branches until branch metadata sentinel is deployed and healthy.
5. Run acceptance after each closeout batch:
```text
getRuntimeStatus -> getBaseline -> createBranch -> createPatch
```
## Safety Constraint
Branch deletion or force update is not part of this reconciliation step. Cleanup must not create new Gitea metadata drift.
-170
View File
@@ -1,170 +0,0 @@
# Patch Lifecycle Policy
## Purpose
Prevent long-lived GitOps patch branches from accumulating into reliability risk.
A patch is not allowed to remain in an undefined state. Every patch branch must have one explicit lifecycle status:
```text
merge
superseded
archive
reject
keep-open
```
## Principle
The goal is not to merge every patch. The goal is to eliminate unmanaged patch state.
```text
many unreviewed patches
↓
zero undefined patches
```
## Lifecycle States
### merge
Use when the patch is still valid, current, non-duplicative, and required in the active baseline.
Required before merge:
1. Diff reviewed against current baseline.
2. No regression against V1R4C3 or later.
3. Conflict risk known.
4. Runtime acceptance test defined.
5. Owner/admin approval recorded for medium or high risk changes.
### superseded
Use when the patch intent has already been absorbed by a later baseline or a newer patch.
Required evidence:
- newer branch or baseline provides the same function;
- old branch should not be merged;
- branch may be retained only as historical evidence.
### archive
Use when the patch is useful as historical evidence, smoke-test proof, acceptance proof, or temporary validation branch, but should not enter main.
Examples:
- smoke tests;
- authentication probes;
- acceptance branches;
- temporary diagnostics;
- one-off verification branches.
### reject
Use when the patch is unsafe, incorrect, obsolete, or conflicts with the current architecture.
Required evidence:
- reason for rejection;
- known replacement path, if any.
### keep-open
Use only for active work that is intentionally not ready to merge.
Required fields:
- owner;
- next action;
- target baseline;
- expiration/review date.
## Priority Classes
### P0: Reliability and stop-the-line fixes
Must be reviewed first.
Includes:
- incident documentation;
- Gitea consistency sentinel;
- autonomous safe mode;
- controller health guardrails;
- branch metadata drift prevention;
- fixes required to prevent autonomous operation stop.
### P1: Current product or platform capabilities
Review for consolidation into the next integration baseline.
Includes:
- agent event bus;
- GPT message plane;
- observability UI;
- autonomous controller dispatch;
- governance and isolation patches;
- search gateway capability.
### P2: Historical, test, and superseded branches
Do not merge by default. Archive or mark superseded.
Includes:
- smoke branches;
- auth-test branches;
- acceptance branches;
- rollback-request tests;
- model acceptance probes;
- tag request branches;
- old promotion branches;
- temporary test branches.
## Required Cleanup Cadence
Patch lifecycle review must run when any of the following is true:
1. More than 20 patch branches are open.
2. Any patch branch is older than 7 days without status.
3. A baseline is promoted.
4. An incident occurs.
5. Gitea branch metadata drift is detected.
## Merge Restrictions
Never bulk merge old patches.
Old patches must not be merged if:
- they predate the current baseline and have no diff review;
- they are smoke/test/auth-only branches;
- they modify governance or credentials without owner approval;
- they would overwrite newer baseline behavior;
- their original purpose is unknown.
## Post-Cleanup Acceptance
After lifecycle cleanup or integration baseline creation, run:
```text
getRuntimeStatus
->
getBaseline
->
createBranch
->
createPatch
```
Expected:
- read operations return active runtime information;
- write operations return commit/revision/sync status;
- no new branch metadata drift appears.
## Operational Rule
Any patch backlog large enough to affect autonomous operation reliability is a reliability concern and must be handled through controlled lifecycle reconciliation, not ad hoc merging.
@@ -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
}
+110
View File
@@ -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())