feat(governance): 新增操作類別權限模型
All checks were successful
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / tests (push) Successful in 1m24s
CD Pipeline / build-and-deploy (push) Successful in 4m45s
CD Pipeline / post-deploy-checks (push) Successful in 1m46s

This commit is contained in:
Your Name
2026-06-12 15:04:51 +08:00
parent b5112ccf65
commit 7c8bb3645b
14 changed files with 1994 additions and 10 deletions

View File

@@ -76,6 +76,9 @@ from src.services.ai_agent_owner_approved_fixture_dry_run import (
from src.services.ai_agent_owner_approved_learning_dry_run import (
load_latest_ai_agent_owner_approved_learning_dry_run,
)
from src.services.ai_agent_operation_permission_model import (
load_latest_ai_agent_operation_permission_model,
)
from src.services.ai_agent_post_write_verifier_package import (
load_latest_ai_agent_post_write_verifier_package,
)
@@ -1035,6 +1038,34 @@ async def get_agent_runtime_worker_shadow_gate() -> dict[str, Any]:
) from exc
@router.get(
"/agent-operation-permission-model",
response_model=dict[str, Any],
summary="取得 AI Agent 操作類別權限模型",
description=(
"讀取最新已提交的 P2-101 操作類別權限模型;此端點只回傳 permission lane、"
"operation category、Agent responsibility、gate transition 與 operator template"
"不啟動 runtime worker、不寫 Gateway queue、不送 Telegram、不呼叫 Bot API、"
"不寫讀報回執、不執行 verifier live readback、不寫 production target、不讀 secret。"
),
)
async def get_agent_operation_permission_model() -> dict[str, Any]:
"""Return the latest read-only AI Agent operation permission model."""
try:
return await asyncio.to_thread(load_latest_ai_agent_operation_permission_model)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(exc),
) from exc
except (json.JSONDecodeError, ValueError) as exc:
logger.error("ai_agent_operation_permission_model_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AI Agent 操作類別權限模型無效",
) from exc
@router.get(
"/agent-owner-approved-fixture-dry-run",
response_model=dict[str, Any],

View File

@@ -0,0 +1,313 @@
"""
AI Agent operation permission model snapshot.
Loads the latest committed P2-101 operation category permission model.
This module validates repo-committed evidence only; it never enables runtime
workers, writes Gateway queues, sends Telegram messages, reads secrets, or
writes production targets.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from src.services.snapshot_paths import default_evaluations_dir
_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__))
_SNAPSHOT_PATTERN = "ai_agent_operation_permission_model_*.json"
_SCHEMA_VERSION = "ai_agent_operation_permission_model_v1"
_RUNTIME_AUTHORITY = "operation_permission_model_only_no_live_execution_or_send"
def load_latest_ai_agent_operation_permission_model(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed AI Agent operation permission model."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(f"no AI Agent operation permission model snapshots found in {directory}")
latest = candidates[-1]
with latest.open(encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError(f"{latest}: expected JSON object")
_require_schema(payload, str(latest))
_require_no_live_boundaries(payload, str(latest))
_require_permission_lanes(payload, str(latest))
_require_operation_categories(payload, str(latest))
_require_agent_roles(payload, str(latest))
_require_gate_transitions(payload, str(latest))
_require_operator_templates(payload, str(latest))
_require_redaction_contract(payload, str(latest))
_require_rollup_consistency(payload, str(latest))
return payload
def _require_schema(payload: dict[str, Any], label: str) -> None:
if payload.get("schema_version") != _SCHEMA_VERSION:
raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}")
status = payload.get("program_status") or {}
if status.get("read_only_mode") is not True:
raise ValueError(f"{label}: program_status.read_only_mode must be true")
if status.get("runtime_authority") != _RUNTIME_AUTHORITY:
raise ValueError(f"{label}: runtime_authority must remain {_RUNTIME_AUTHORITY}")
if status.get("current_task_id") != "P2-101":
raise ValueError(f"{label}: current_task_id must be P2-101")
if status.get("next_task_id") != "P2-102":
raise ValueError(f"{label}: next_task_id must be P2-102")
def _require_no_live_boundaries(payload: dict[str, Any], label: str) -> None:
truth = payload.get("operation_permission_truth") or {}
required_true = {
"permission_model_ready",
"operation_category_matrix_ready",
"risk_tier_mapping_ready",
"agent_responsibility_mapping_ready",
"approval_gate_mapping_ready",
"manual_sop_lane_ready",
"p2_404_shadow_gate_handoff_ready",
}
missing = sorted(field for field in required_true if truth.get(field) is not True)
if missing:
raise ValueError(f"{label}: permission readiness flags must remain true: {missing}")
required_false = {
"runtime_execution_enabled",
"gateway_queue_write_enabled",
"telegram_send_enabled",
"telegram_bot_api_call_enabled",
"delivery_receipt_write_enabled",
"ai_runtime_worker_enabled",
"medium_low_auto_worker_enabled",
"post_action_verifier_live_readback_enabled",
"production_write_enabled",
"secret_value_read_enabled",
"paid_provider_call_enabled",
"host_or_cluster_command_enabled",
"destructive_operation_enabled",
"work_window_transcript_display_allowed",
}
unsafe = sorted(field for field in required_false if truth.get(field) is not False)
if unsafe:
raise ValueError(f"{label}: live execution/send/write flags must remain false: {unsafe}")
zero_counts = {
"runtime_execution_count_24h",
"gateway_queue_write_count_24h",
"telegram_send_count_24h",
"telegram_bot_api_call_count_24h",
"delivery_receipt_write_count_24h",
"ai_runtime_worker_run_count_24h",
"medium_low_auto_execution_count_24h",
"post_action_verifier_live_readback_count_24h",
"production_write_count_24h",
"secret_value_read_count_24h",
"paid_provider_call_count_24h",
"host_or_cluster_command_count_24h",
"destructive_operation_count_24h",
}
non_zero = sorted(field for field in zero_counts if truth.get(field) != 0)
if non_zero:
raise ValueError(f"{label}: live execution/send/write counts must remain zero: {non_zero}")
def _require_permission_lanes(payload: dict[str, Any], label: str) -> None:
lanes = payload.get("permission_lanes") or []
lane_ids = {lane.get("lane_id") for lane in lanes}
required = {
"observe_only",
"no_write_replay_allowed",
"proposal_only",
"human_approval_required",
"explicitly_blocked",
}
if lane_ids != required:
raise ValueError(f"{label}: permission lanes must match {sorted(required)}")
for lane in lanes:
lane_id = lane.get("lane_id")
if lane.get("live_execution_allowed") is not False:
raise ValueError(f"{label}: lane {lane_id} live_execution_allowed must remain false")
if lane.get("production_write_allowed") is not False:
raise ValueError(f"{label}: lane {lane_id} production_write_allowed must remain false")
def _require_operation_categories(payload: dict[str, Any], label: str) -> None:
categories = payload.get("operation_categories") or []
category_ids = {category.get("category_id") for category in categories}
required = {
"observe_inventory_read",
"diagnose_correlate_evidence",
"report_digest_queue_candidate",
"shadow_no_write_replay",
"manual_sop_draft",
"repair_candidate_proposal",
"low_risk_noop_execution",
"medium_risk_repair_execution",
"post_action_verifier_live_readback",
"telegram_gateway_queue_write",
"production_config_or_data_write",
"secret_or_paid_provider_access",
"destructive_host_or_cluster_action",
}
if category_ids != required:
raise ValueError(f"{label}: operation categories must match {sorted(required)}")
for category in categories:
category_id = category.get("category_id")
if category.get("queue_write_allowed") is not False:
raise ValueError(f"{label}: category {category_id} queue_write_allowed must remain false")
if category.get("telegram_send_allowed") is not False:
raise ValueError(f"{label}: category {category_id} telegram_send_allowed must remain false")
if category.get("production_write_allowed") is not False:
raise ValueError(f"{label}: category {category_id} production_write_allowed must remain false")
if category.get("secret_value_read_allowed") is not False:
raise ValueError(f"{label}: category {category_id} secret_value_read_allowed must remain false")
if category.get("destructive_action_allowed") is not False:
raise ValueError(f"{label}: category {category_id} destructive_action_allowed must remain false")
if category.get("live_execution_allowed") is not False:
raise ValueError(f"{label}: category {category_id} live_execution_allowed must remain false")
if not _is_redacted_sha256(category.get("evidence_hash")):
raise ValueError(f"{label}: category {category_id} must expose a redacted sha256 evidence_hash")
def _require_agent_roles(payload: dict[str, Any], label: str) -> None:
roles = payload.get("agent_permission_roles") or []
agents = {role.get("agent_id") for role in roles}
if agents != {"openclaw", "hermes", "nemotron"}:
raise ValueError(f"{label}: permission roles must include OpenClaw, Hermes, and NemoTron")
for role in roles:
if role.get("live_action_count_24h") != 0:
raise ValueError(f"{label}: agent {role.get('agent_id')} live_action_count_24h must remain zero")
if role.get("self_approval_allowed") is not False:
raise ValueError(f"{label}: agent {role.get('agent_id')} self_approval_allowed must remain false")
def _require_gate_transitions(payload: dict[str, Any], label: str) -> None:
gates = payload.get("gate_transitions") or []
gate_ids = {gate.get("gate_id") for gate in gates}
required = {
"p2_101_permission_review_gate",
"p2_102_dry_run_evidence_gate",
"gateway_queue_write_permission_gate",
"telegram_send_permission_gate",
"medium_low_auto_worker_permission_gate",
"post_action_verifier_live_gate",
"production_write_permission_gate",
"secret_or_paid_provider_gate",
}
if gate_ids != required:
raise ValueError(f"{label}: gate transitions must match {sorted(required)}")
for gate in gates:
gate_id = gate.get("gate_id")
if gate.get("opens_live_execution") is not False:
raise ValueError(f"{label}: gate {gate_id} opens_live_execution must remain false")
if gate.get("current_status") not in {"ready_for_review", "blocked_until_evidence", "blocked_by_policy"}:
raise ValueError(f"{label}: gate {gate_id} current_status is invalid")
def _require_operator_templates(payload: dict[str, Any], label: str) -> None:
templates = payload.get("operator_decision_templates") or []
template_ids = {template.get("template_id") for template in templates}
required = {
"evidence_collect_next_step",
"manual_sop_next_step",
"repair_proposal_next_step",
"queue_candidate_next_step",
"rollback_or_fix_next_step",
}
if template_ids != required:
raise ValueError(f"{label}: operator templates must match {sorted(required)}")
for template in templates:
if template.get("creates_runtime_action") is not False:
raise ValueError(f"{label}: template {template.get('template_id')} creates_runtime_action must remain false")
if template.get("requires_human_review") is not True:
raise ValueError(f"{label}: template {template.get('template_id')} requires_human_review must remain true")
def _require_redaction_contract(payload: dict[str, Any], label: str) -> None:
contract = payload.get("display_redaction_contract") or {}
required_false = {
"raw_prompt_display_allowed",
"private_reasoning_display_allowed",
"secret_value_display_allowed",
"raw_telegram_payload_display_allowed",
"work_window_transcript_display_allowed",
}
if contract.get("redaction_required") is not True:
raise ValueError(f"{label}: display redaction must remain required")
unsafe = sorted(field for field in required_false if contract.get(field) is not False)
if unsafe:
raise ValueError(f"{label}: display redaction fields must remain false: {unsafe}")
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
rollups = payload.get("rollups") or {}
truth = payload.get("operation_permission_truth") or {}
lanes = payload.get("permission_lanes") or []
categories = payload.get("operation_categories") or []
roles = payload.get("agent_permission_roles") or []
gates = payload.get("gate_transitions") or []
templates = payload.get("operator_decision_templates") or []
expected = {
"permission_lane_count": len(lanes),
"operation_category_count": len(categories),
"observe_only_category_count": sum(1 for item in categories if item.get("permission_lane") == "observe_only"),
"no_write_replay_allowed_category_count": sum(1 for item in categories if item.get("permission_lane") == "no_write_replay_allowed"),
"proposal_only_category_count": sum(1 for item in categories if item.get("permission_lane") == "proposal_only"),
"human_approval_required_category_count": sum(1 for item in categories if item.get("permission_lane") == "human_approval_required"),
"explicitly_blocked_category_count": sum(1 for item in categories if item.get("permission_lane") == "explicitly_blocked"),
"agent_role_count": len(roles),
"gate_transition_count": len(gates),
"operator_decision_template_count": len(templates),
}
mismatches = sorted(field for field, value in expected.items() if rollups.get(field) != value)
if mismatches:
raise ValueError(f"{label}: rollup counts must match source arrays: {mismatches}")
approval_category_ids = sorted(
item.get("category_id") for item in categories if item.get("permission_lane") == "human_approval_required"
)
if sorted(rollups.get("human_approval_required_category_ids") or []) != approval_category_ids:
raise ValueError(f"{label}: human_approval_required_category_ids must match categories")
blocked_category_ids = sorted(
item.get("category_id") for item in categories if item.get("permission_lane") == "explicitly_blocked"
)
if sorted(rollups.get("explicitly_blocked_category_ids") or []) != blocked_category_ids:
raise ValueError(f"{label}: explicitly_blocked_category_ids must match categories")
zero_pairs = {
"runtime_execution_count": truth.get("runtime_execution_count_24h"),
"gateway_queue_write_count": truth.get("gateway_queue_write_count_24h"),
"telegram_send_count": truth.get("telegram_send_count_24h"),
"telegram_bot_api_call_count": truth.get("telegram_bot_api_call_count_24h"),
"delivery_receipt_write_count": truth.get("delivery_receipt_write_count_24h"),
"ai_runtime_worker_run_count": truth.get("ai_runtime_worker_run_count_24h"),
"medium_low_auto_execution_count": truth.get("medium_low_auto_execution_count_24h"),
"post_action_verifier_live_readback_count": truth.get("post_action_verifier_live_readback_count_24h"),
"production_write_count": truth.get("production_write_count_24h"),
"secret_value_read_count": truth.get("secret_value_read_count_24h"),
"paid_provider_call_count": truth.get("paid_provider_call_count_24h"),
"host_or_cluster_command_count": truth.get("host_or_cluster_command_count_24h"),
"destructive_operation_count": truth.get("destructive_operation_count_24h"),
}
non_zero = sorted(field for field, value in zero_pairs.items() if rollups.get(field) != 0 or value != 0)
if non_zero:
raise ValueError(f"{label}: rollup live counts must remain zero: {non_zero}")
def _is_redacted_sha256(value: Any) -> bool:
if not isinstance(value, str):
return False
prefix = "sha256:"
if not value.startswith(prefix):
return False
digest = value[len(prefix) :]
return len(digest) == 64 and all(char in "0123456789abcdef" for char in digest)

View File

@@ -0,0 +1,130 @@
import copy
import json
import pytest
from src.services.ai_agent_operation_permission_model import (
load_latest_ai_agent_operation_permission_model,
)
def _write_snapshot(tmp_path, payload):
path = tmp_path / "ai_agent_operation_permission_model_2026-06-12.json"
path.write_text(json.dumps(payload), encoding="utf-8")
return path
def test_load_latest_ai_agent_operation_permission_model():
data = load_latest_ai_agent_operation_permission_model()
assert data["schema_version"] == "ai_agent_operation_permission_model_v1"
assert data["program_status"]["current_task_id"] == "P2-101"
assert data["program_status"]["next_task_id"] == "P2-102"
assert data["program_status"]["overall_completion_percent"] == 97
assert data["operation_permission_truth"]["permission_model_ready"] is True
assert data["operation_permission_truth"]["operation_category_matrix_ready"] is True
assert data["operation_permission_truth"]["runtime_execution_enabled"] is False
assert data["operation_permission_truth"]["gateway_queue_write_enabled"] is False
assert data["operation_permission_truth"]["telegram_send_enabled"] is False
assert data["operation_permission_truth"]["telegram_bot_api_call_enabled"] is False
assert data["operation_permission_truth"]["ai_runtime_worker_enabled"] is False
assert data["operation_permission_truth"]["medium_low_auto_worker_enabled"] is False
assert data["operation_permission_truth"]["production_write_enabled"] is False
assert data["operation_permission_truth"]["secret_value_read_enabled"] is False
assert data["operation_permission_truth"]["destructive_operation_enabled"] is False
assert data["rollups"]["permission_lane_count"] == 5
assert data["rollups"]["operation_category_count"] == 13
assert data["rollups"]["observe_only_category_count"] == 2
assert data["rollups"]["no_write_replay_allowed_category_count"] == 2
assert data["rollups"]["proposal_only_category_count"] == 2
assert data["rollups"]["human_approval_required_category_count"] == 4
assert data["rollups"]["explicitly_blocked_category_count"] == 3
assert data["rollups"]["agent_role_count"] == 3
assert data["rollups"]["gate_transition_count"] == 8
assert data["rollups"]["operator_decision_template_count"] == 5
assert data["rollups"]["runtime_execution_count"] == 0
assert data["rollups"]["gateway_queue_write_count"] == 0
assert data["rollups"]["telegram_send_count"] == 0
assert data["rollups"]["production_write_count"] == 0
assert data["rollups"]["destructive_operation_count"] == 0
def test_rejects_runtime_execution_enabled(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["operation_permission_truth"]["runtime_execution_enabled"] = True
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="live execution/send/write flags"):
load_latest_ai_agent_operation_permission_model(tmp_path)
def test_rejects_gateway_queue_write_count(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["operation_permission_truth"]["gateway_queue_write_count_24h"] = 1
bad["rollups"]["gateway_queue_write_count"] = 1
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="live execution/send/write counts"):
load_latest_ai_agent_operation_permission_model(tmp_path)
def test_rejects_lane_live_execution(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["permission_lanes"][0]["live_execution_allowed"] = True
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="live_execution_allowed"):
load_latest_ai_agent_operation_permission_model(tmp_path)
def test_rejects_category_telegram_send(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["operation_categories"][0]["telegram_send_allowed"] = True
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="telegram_send_allowed"):
load_latest_ai_agent_operation_permission_model(tmp_path)
def test_rejects_agent_self_approval(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["agent_permission_roles"][0]["self_approval_allowed"] = True
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="self_approval_allowed"):
load_latest_ai_agent_operation_permission_model(tmp_path)
def test_rejects_gate_opening_live_execution(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["gate_transitions"][0]["opens_live_execution"] = True
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="opens_live_execution"):
load_latest_ai_agent_operation_permission_model(tmp_path)
def test_rejects_template_runtime_action(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["operator_decision_templates"][0]["creates_runtime_action"] = True
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="creates_runtime_action"):
load_latest_ai_agent_operation_permission_model(tmp_path)
def test_rejects_rollup_mismatch(tmp_path):
data = load_latest_ai_agent_operation_permission_model()
bad = copy.deepcopy(data)
bad["rollups"]["operation_category_count"] = 999
_write_snapshot(tmp_path, bad)
with pytest.raises(ValueError, match="rollup counts"):
load_latest_ai_agent_operation_permission_model(tmp_path)

View File

@@ -0,0 +1,38 @@
from fastapi.testclient import TestClient
from src.main import app
def test_get_ai_agent_operation_permission_model_api():
client = TestClient(app)
response = client.get("/api/v1/agents/agent-operation-permission-model")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "ai_agent_operation_permission_model_v1"
assert data["program_status"]["current_task_id"] == "P2-101"
assert data["program_status"]["next_task_id"] == "P2-102"
assert data["program_status"]["overall_completion_percent"] == 97
assert data["operation_permission_truth"]["permission_model_ready"] is True
assert data["operation_permission_truth"]["runtime_execution_enabled"] is False
assert data["operation_permission_truth"]["gateway_queue_write_enabled"] is False
assert data["operation_permission_truth"]["telegram_send_enabled"] is False
assert data["operation_permission_truth"]["telegram_bot_api_call_enabled"] is False
assert data["operation_permission_truth"]["ai_runtime_worker_enabled"] is False
assert data["operation_permission_truth"]["medium_low_auto_worker_enabled"] is False
assert data["operation_permission_truth"]["production_write_enabled"] is False
assert data["operation_permission_truth"]["secret_value_read_enabled"] is False
assert data["operation_permission_truth"]["destructive_operation_enabled"] is False
assert data["rollups"]["permission_lane_count"] == 5
assert data["rollups"]["operation_category_count"] == 13
assert data["rollups"]["human_approval_required_category_count"] == 4
assert data["rollups"]["explicitly_blocked_category_count"] == 3
assert data["rollups"]["agent_role_count"] == 3
assert data["rollups"]["gate_transition_count"] == 8
assert data["rollups"]["operator_decision_template_count"] == 5
assert data["rollups"]["runtime_execution_count"] == 0
assert data["rollups"]["gateway_queue_write_count"] == 0
assert data["rollups"]["telegram_send_count"] == 0
assert data["rollups"]["production_write_count"] == 0
assert data["rollups"]["secret_value_read_count"] == 0
assert data["rollups"]["destructive_operation_count"] == 0

View File

@@ -4373,6 +4373,69 @@
"high": "高風險",
"critical": "關鍵阻擋"
}
},
"operationPermissionModel": {
"title": "P2-101 操作類別權限模型",
"source": "{generated} · {current} → {next}",
"truthTitle": "權限模型真相",
"boundaryTitle": "runtime 邊界",
"boundarySummary": "目前 runtime {runtime}、Gateway queue write {queue}、Telegram send {send}、production write {write}、secret read {secret};本段只定義 lane不開執行。",
"metrics": {
"overall": "P2-101 進度",
"categories": "操作類別",
"observeOnly": "只讀",
"noWrite": "no-write",
"proposals": "提案",
"humanApproval": "需人工",
"blocked": "明確阻擋",
"gates": "關卡",
"runtimeRuns": "runtime",
"queueWrites": "queue writes",
"telegramSends": "TG sends",
"productionWrites": "prod writes",
"secretReads": "secret reads",
"destructive": "破壞性"
},
"flags": {
"modelReady": "model ready: {value}",
"matrixReady": "matrix ready: {value}",
"agentMap": "agent map: {value}",
"p2Handoff": "P2-404 handoff: {value}",
"runtime": "runtime enabled: {value}",
"queueWrite": "queue write: {value}",
"send": "send: {value}",
"productionWrite": "prod write: {value}",
"secret": "secret value: {value}",
"destructive": "destructive: {value}",
"liveExecution": "live execution: {value}",
"opensLive": "opens live: {value}",
"runtimeAction": "runtime action: {value}"
},
"labels": {
"nextGate": "next gate: {value}",
"requiredEvidence": "evidence: {value}",
"blockedActions": "blocked: {value}",
"liveCount": "live {count}",
"reviewRequired": "人工審查: {value}"
},
"lanes": {
"observe_only": "只讀觀察",
"no_write_replay_allowed": "no-write replay",
"proposal_only": "提案 / SOP",
"human_approval_required": "需人工批准",
"explicitly_blocked": "明確阻擋"
},
"statuses": {
"ready_for_review": "可審查",
"blocked_until_evidence": "等證據",
"blocked_by_policy": "政策阻擋"
},
"riskTiers": {
"low": "低風險",
"medium": "中風險",
"high": "高風險",
"critical": "關鍵阻擋"
}
}
}
},

View File

@@ -4373,6 +4373,69 @@
"high": "高風險",
"critical": "關鍵阻擋"
}
},
"operationPermissionModel": {
"title": "P2-101 操作類別權限模型",
"source": "{generated} · {current} → {next}",
"truthTitle": "權限模型真相",
"boundaryTitle": "runtime 邊界",
"boundarySummary": "目前 runtime {runtime}、Gateway queue write {queue}、Telegram send {send}、production write {write}、secret read {secret};本段只定義 lane不開執行。",
"metrics": {
"overall": "P2-101 進度",
"categories": "操作類別",
"observeOnly": "只讀",
"noWrite": "no-write",
"proposals": "提案",
"humanApproval": "需人工",
"blocked": "明確阻擋",
"gates": "關卡",
"runtimeRuns": "runtime",
"queueWrites": "queue writes",
"telegramSends": "TG sends",
"productionWrites": "prod writes",
"secretReads": "secret reads",
"destructive": "破壞性"
},
"flags": {
"modelReady": "model ready: {value}",
"matrixReady": "matrix ready: {value}",
"agentMap": "agent map: {value}",
"p2Handoff": "P2-404 handoff: {value}",
"runtime": "runtime enabled: {value}",
"queueWrite": "queue write: {value}",
"send": "send: {value}",
"productionWrite": "prod write: {value}",
"secret": "secret value: {value}",
"destructive": "destructive: {value}",
"liveExecution": "live execution: {value}",
"opensLive": "opens live: {value}",
"runtimeAction": "runtime action: {value}"
},
"labels": {
"nextGate": "next gate: {value}",
"requiredEvidence": "evidence: {value}",
"blockedActions": "blocked: {value}",
"liveCount": "live {count}",
"reviewRequired": "人工審查: {value}"
},
"lanes": {
"observe_only": "只讀觀察",
"no_write_replay_allowed": "no-write replay",
"proposal_only": "提案 / SOP",
"human_approval_required": "需人工批准",
"explicitly_blocked": "明確阻擋"
},
"statuses": {
"ready_for_review": "可審查",
"blocked_until_evidence": "等證據",
"blocked_by_policy": "政策阻擋"
},
"riskTiers": {
"low": "低風險",
"medium": "中風險",
"high": "高風險",
"critical": "關鍵阻擋"
}
}
}
},

View File

@@ -42,6 +42,7 @@ import {
type AiAgentLiveReadModelGateSnapshot,
type AiAgentOwnerApprovedFixtureDryRunSnapshot,
type AiAgentOwnerApprovedLearningDryRunSnapshot,
type AiAgentOperationPermissionModelSnapshot,
type AiAgentPostWriteVerifierPackageSnapshot,
type AiAgentProactiveOperationsContractSnapshot,
type AiAgentRedisDryRunGateSnapshot,
@@ -346,6 +347,7 @@ export function AutomationInventoryTab() {
const [reportRuntimeDryRun, setReportRuntimeDryRun] = useState<AiAgentReportRuntimeDryRunSnapshot | null>(null)
const [reportRuntimeFixtureReadback, setReportRuntimeFixtureReadback] = useState<AiAgentReportRuntimeFixtureReadbackSnapshot | null>(null)
const [runtimeWorkerShadowGate, setRuntimeWorkerShadowGate] = useState<AiAgentRuntimeWorkerShadowGateSnapshot | null>(null)
const [operationPermissionModel, setOperationPermissionModel] = useState<AiAgentOperationPermissionModelSnapshot | null>(null)
const [reportTruthActionabilityReview, setReportTruthActionabilityReview] = useState<AiAgentReportTruthActionabilityReviewSnapshot | null>(null)
const [ownerDryRunPackage, setOwnerDryRunPackage] = useState<AiAgentOwnerApprovedFixtureDryRunSnapshot | null>(null)
const [hostStatefulInventory, setHostStatefulInventory] = useState<AiAgentHostStatefulVersionInventorySnapshot | null>(null)
@@ -383,6 +385,7 @@ export function AutomationInventoryTab() {
apiClient.getAiAgentReportRuntimeDryRun(),
apiClient.getAiAgentReportRuntimeFixtureReadback(),
apiClient.getAiAgentRuntimeWorkerShadowGate(),
apiClient.getAiAgentOperationPermissionModel(),
apiClient.getAiAgentReportTruthActionabilityReview(),
apiClient.getAiAgentOwnerApprovedFixtureDryRun(),
apiClient.getAiAgentHostStatefulVersionInventory(),
@@ -419,6 +422,7 @@ export function AutomationInventoryTab() {
reportRuntimeDryRunResult,
reportRuntimeFixtureReadbackResult,
runtimeWorkerShadowGateResult,
operationPermissionModelResult,
reportTruthActionabilityReviewResult,
ownerDryRunPackageResult,
hostStatefulInventoryResult,
@@ -452,6 +456,7 @@ export function AutomationInventoryTab() {
setReportRuntimeDryRun(reportRuntimeDryRunResult.status === 'fulfilled' ? reportRuntimeDryRunResult.value : null)
setReportRuntimeFixtureReadback(reportRuntimeFixtureReadbackResult.status === 'fulfilled' ? reportRuntimeFixtureReadbackResult.value : null)
setRuntimeWorkerShadowGate(runtimeWorkerShadowGateResult.status === 'fulfilled' ? runtimeWorkerShadowGateResult.value : null)
setOperationPermissionModel(operationPermissionModelResult.status === 'fulfilled' ? operationPermissionModelResult.value : null)
setReportTruthActionabilityReview(reportTruthActionabilityReviewResult.status === 'fulfilled' ? reportTruthActionabilityReviewResult.value : null)
setOwnerDryRunPackage(ownerDryRunPackageResult.status === 'fulfilled' ? ownerDryRunPackageResult.value : null)
setHostStatefulInventory(hostStatefulInventoryResult.status === 'fulfilled' ? hostStatefulInventoryResult.value : null)
@@ -483,6 +488,7 @@ export function AutomationInventoryTab() {
reportRuntimeDryRunResult,
reportRuntimeFixtureReadbackResult,
runtimeWorkerShadowGateResult,
operationPermissionModelResult,
reportTruthActionabilityReviewResult,
ownerDryRunPackageResult,
hostStatefulInventoryResult,
@@ -1047,6 +1053,42 @@ export function AutomationInventoryTab() {
.slice(0, 6)
}, [runtimeWorkerShadowGate])
const visibleOperationPermissionCategories = useMemo(() => {
if (!operationPermissionModel) return []
const lanePriority = {
explicitly_blocked: 0,
human_approval_required: 1,
proposal_only: 2,
no_write_replay_allowed: 3,
observe_only: 4,
} as Record<string, number>
const riskPriority = { critical: 0, high: 1, medium: 2, low: 3 } as Record<string, number>
return [...operationPermissionModel.operation_categories]
.sort((a, b) => {
const leftLane = lanePriority[a.permission_lane] ?? 5
const rightLane = lanePriority[b.permission_lane] ?? 5
if (leftLane !== rightLane) return leftLane - rightLane
const leftRisk = riskPriority[a.risk_tier] ?? 4
const rightRisk = riskPriority[b.risk_tier] ?? 4
if (leftRisk !== rightRisk) return leftRisk - rightRisk
return a.category_id.localeCompare(b.category_id)
})
.slice(0, 8)
}, [operationPermissionModel])
const visibleOperationPermissionGates = useMemo(() => {
if (!operationPermissionModel) return []
const statusPriority = { blocked_by_policy: 0, blocked_until_evidence: 1, ready_for_review: 2 } as Record<string, number>
return [...operationPermissionModel.gate_transitions]
.sort((a, b) => {
const left = statusPriority[a.current_status] ?? 3
const right = statusPriority[b.current_status] ?? 3
if (left !== right) return left - right
return a.gate_id.localeCompare(b.gate_id)
})
.slice(0, 8)
}, [operationPermissionModel])
const visibleReportTruthFindings = useMemo(() => {
if (!reportTruthActionabilityReview) return []
const priority = { critical: 0, high: 1, medium: 2, low: 3 } as Record<string, number>
@@ -1266,7 +1308,7 @@ export function AutomationInventoryTab() {
)
}
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !proactiveOperations || !interactionLearningProof || !liveReadModelGate || !redisDryRunGate || !learningWritebackPackage || !telegramReceiptPackage || !ownerApprovedLearningDryRun || !runtimeWriteGateReview || !postWriteVerifierPackage || !runtimeVerifierEvidenceReview || !reportAutomationReview || !reportRuntimeReadiness || !reportRuntimeDryRun || !reportRuntimeFixtureReadback || !runtimeWorkerShadowGate || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !proactiveOperations || !interactionLearningProof || !liveReadModelGate || !redisDryRunGate || !learningWritebackPackage || !telegramReceiptPackage || !ownerApprovedLearningDryRun || !runtimeWriteGateReview || !postWriteVerifierPackage || !runtimeVerifierEvidenceReview || !reportAutomationReview || !reportRuntimeReadiness || !reportRuntimeDryRun || !reportRuntimeFixtureReadback || !runtimeWorkerShadowGate || !operationPermissionModel || !reportTruthActionabilityReview || !ownerDryRunPackage || !hostStatefulInventory || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
return (
<div style={{ padding: 20 }}>
<GlassCard variant="subtle" padding="lg">
@@ -1460,6 +1502,20 @@ export function AutomationInventoryTab() {
const runtimeShadowQueueWrites = runtimeWorkerShadowGate.rollups.gateway_queue_write_count
const runtimeShadowSends = runtimeWorkerShadowGate.rollups.telegram_send_count
const runtimeShadowProductionWrites = runtimeWorkerShadowGate.rollups.production_write_count
const operationPermissionOverall = operationPermissionModel.program_status.overall_completion_percent
const operationPermissionCategories = operationPermissionModel.rollups.operation_category_count
const operationPermissionObserveOnly = operationPermissionModel.rollups.observe_only_category_count
const operationPermissionNoWrite = operationPermissionModel.rollups.no_write_replay_allowed_category_count
const operationPermissionProposalOnly = operationPermissionModel.rollups.proposal_only_category_count
const operationPermissionHumanApproval = operationPermissionModel.rollups.human_approval_required_category_count
const operationPermissionBlocked = operationPermissionModel.rollups.explicitly_blocked_category_count
const operationPermissionGates = operationPermissionModel.rollups.gate_transition_count
const operationPermissionRuntimeRuns = operationPermissionModel.rollups.runtime_execution_count
const operationPermissionQueueWrites = operationPermissionModel.rollups.gateway_queue_write_count
const operationPermissionSends = operationPermissionModel.rollups.telegram_send_count
const operationPermissionProductionWrites = operationPermissionModel.rollups.production_write_count
const operationPermissionSecretReads = operationPermissionModel.rollups.secret_value_read_count
const operationPermissionDestructive = operationPermissionModel.rollups.destructive_operation_count
const reportTruthOverall = reportTruthActionabilityReview.program_status.overall_completion_percent
const reportTruthFindings = reportTruthActionabilityReview.rollups.zero_signal_finding_count
const reportTruthCritical = reportTruthActionabilityReview.rollups.critical_finding_count
@@ -2891,6 +2947,189 @@ export function AutomationInventoryTab() {
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #bedbd0', borderRadius: 7, background: '#f7fffb', display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
<ShieldCheck size={14} style={{ color: '#0f766e' }} />
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('operationPermissionModel.title')}
</span>
</div>
<Chip
value={t('operationPermissionModel.source', {
generated: formatDateTime(operationPermissionModel.generated_at),
current: operationPermissionModel.program_status.current_task_id,
next: operationPermissionModel.program_status.next_task_id,
})}
muted
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(128px, 1fr))', gap: 10 }} className="automation-inventory-live-read-kpi-grid">
<MetricCard label={t('operationPermissionModel.metrics.overall')} value={`${operationPermissionOverall}%`} tone="warn" icon={<Gauge size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.categories')} value={operationPermissionCategories} tone="ok" icon={<Boxes size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.observeOnly')} value={operationPermissionObserveOnly} tone="ok" icon={<Target size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.noWrite')} value={operationPermissionNoWrite} tone="warn" icon={<RefreshCw size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.proposals')} value={operationPermissionProposalOnly} tone="warn" icon={<FileText size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.humanApproval')} value={operationPermissionHumanApproval} tone={operationPermissionHumanApproval > 0 ? 'danger' : 'ok'} icon={<Lock size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.blocked')} value={operationPermissionBlocked} tone={operationPermissionBlocked > 0 ? 'danger' : 'ok'} icon={<ShieldAlert size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.gates')} value={operationPermissionGates} tone="warn" icon={<Route size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.runtimeRuns')} value={operationPermissionRuntimeRuns} tone={operationPermissionRuntimeRuns === 0 ? 'warn' : 'danger'} icon={<Target size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.queueWrites')} value={operationPermissionQueueWrites} tone={operationPermissionQueueWrites === 0 ? 'warn' : 'danger'} icon={<Database size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.telegramSends')} value={operationPermissionSends} tone={operationPermissionSends === 0 ? 'warn' : 'danger'} icon={<BellRing size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.productionWrites')} value={operationPermissionProductionWrites} tone={operationPermissionProductionWrites === 0 ? 'warn' : 'danger'} icon={<HardDrive size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.secretReads')} value={operationPermissionSecretReads} tone={operationPermissionSecretReads === 0 ? 'warn' : 'danger'} icon={<Lock size={16} />} />
<MetricCard label={t('operationPermissionModel.metrics.destructive')} value={operationPermissionDestructive} tone={operationPermissionDestructive === 0 ? 'warn' : 'danger'} icon={<ShieldAlert size={16} />} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: 12 }} className="automation-inventory-live-read-grid">
<div style={{ padding: 11, border: '0.5px solid #cce5dc', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('operationPermissionModel.truthTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{operationPermissionModel.operation_permission_truth.truth_note}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t('operationPermissionModel.flags.modelReady', { value: String(operationPermissionModel.operation_permission_truth.permission_model_ready) })} />
<Chip value={t('operationPermissionModel.flags.matrixReady', { value: String(operationPermissionModel.operation_permission_truth.operation_category_matrix_ready) })} />
<Chip value={t('operationPermissionModel.flags.agentMap', { value: String(operationPermissionModel.operation_permission_truth.agent_responsibility_mapping_ready) })} />
<Chip value={t('operationPermissionModel.flags.p2Handoff', { value: String(operationPermissionModel.operation_permission_truth.p2_404_shadow_gate_handoff_ready) })} />
</div>
</div>
<div style={{ padding: 11, border: '0.5px solid #cce5dc', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<SmallLabel>{t('operationPermissionModel.boundaryTitle')}</SmallLabel>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{t('operationPermissionModel.boundarySummary', {
runtime: operationPermissionRuntimeRuns,
queue: operationPermissionQueueWrites,
send: operationPermissionSends,
write: operationPermissionProductionWrites,
secret: operationPermissionSecretReads,
})}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t('operationPermissionModel.flags.runtime', { value: String(operationPermissionModel.operation_permission_truth.runtime_execution_enabled) })} muted />
<Chip value={t('operationPermissionModel.flags.queueWrite', { value: String(operationPermissionModel.operation_permission_truth.gateway_queue_write_enabled) })} muted />
<Chip value={t('operationPermissionModel.flags.send', { value: String(operationPermissionModel.operation_permission_truth.telegram_send_enabled) })} muted />
<Chip value={t('operationPermissionModel.flags.productionWrite', { value: String(operationPermissionModel.operation_permission_truth.production_write_enabled) })} muted />
<Chip value={t('operationPermissionModel.flags.secret', { value: String(operationPermissionModel.operation_permission_truth.secret_value_read_enabled) })} muted />
<Chip value={t('operationPermissionModel.flags.destructive', { value: String(operationPermissionModel.operation_permission_truth.destructive_operation_enabled) })} muted />
</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{operationPermissionModel.permission_lanes.map(lane => (
<div key={lane.lane_id} style={{ padding: 10, border: '0.5px solid #cce5dc', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{lane.display_name}
</span>
<Chip value={t(`operationPermissionModel.lanes.${lane.lane_id}` as never)} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{lane.summary}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{lane.allowed_outputs.slice(0, 3).map(item => (
<Chip key={item} value={item} />
))}
</div>
<Chip value={t('operationPermissionModel.labels.nextGate', { value: lane.required_gate_before_promotion })} muted />
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleOperationPermissionCategories.map(category => (
<div key={category.category_id} style={{ padding: 10, border: '0.5px solid #cce5dc', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{category.display_name}
</span>
<Chip value={redisDryRunValueLabel('agents', category.primary_agent)} muted />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<Chip value={t(`operationPermissionModel.lanes.${category.permission_lane}` as never)} muted={category.permission_lane !== 'explicitly_blocked'} />
<Chip value={t(`operationPermissionModel.riskTiers.${category.risk_tier}` as never)} muted={category.risk_tier === 'low'} />
<Chip value={t('operationPermissionModel.flags.liveExecution', { value: String(category.live_execution_allowed) })} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{t('operationPermissionModel.labels.requiredEvidence', { value: category.required_evidence.slice(0, 3).join(' / ') })}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#b45309', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{t('operationPermissionModel.labels.blockedActions', { value: category.blocked_actions.slice(0, 3).join(' / ') })}
</span>
<Chip value={t('operationPermissionModel.labels.nextGate', { value: category.next_gate })} muted />
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{operationPermissionModel.agent_permission_roles.map(role => (
<div key={role.agent_id} style={{ padding: 10, border: '0.5px solid #cce5dc', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{role.display_name}
</span>
<Chip value={t('operationPermissionModel.labels.liveCount', { count: role.live_action_count_24h })} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{role.permission_responsibility}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{role.allowed_lanes.slice(0, 3).map(item => (
<Chip key={item} value={t(`operationPermissionModel.lanes.${item}` as never)} />
))}
{role.blocked_now.slice(0, 2).map(item => (
<Chip key={item} value={item} muted />
))}
</div>
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{visibleOperationPermissionGates.map(gate => (
<div key={gate.gate_id} style={{ padding: 10, border: '0.5px solid #cce5dc', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{gate.display_name}
</span>
<Chip value={t(`operationPermissionModel.statuses.${gate.current_status}` as never)} muted={gate.current_status !== 'blocked_by_policy'} />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{gate.required_before}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#2563eb', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{gate.next_safe_step}
</span>
<Chip value={t('operationPermissionModel.flags.opensLive', { value: String(gate.opens_live_execution) })} muted />
</div>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }} className="automation-inventory-live-read-card-grid">
{operationPermissionModel.operator_decision_templates.map(template => (
<div key={template.template_id} style={{ padding: 10, border: '0.5px solid #cce5dc', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center', minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflowWrap: 'anywhere' }}>
{template.display_name}
</span>
<Chip value={t('operationPermissionModel.labels.reviewRequired', { value: String(template.requires_human_review) })} muted />
</div>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#64727a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{template.when_to_use}
</span>
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#2563eb', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{template.human_instruction}
</span>
<Chip value={t('operationPermissionModel.flags.runtimeAction', { value: String(template.creates_runtime_action) })} muted />
</div>
))}
</div>
</div>
<div style={{ padding: 12, border: '0.5px solid #d8c6a6', borderRadius: 7, background: '#fffdf7', display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>

View File

@@ -347,6 +347,11 @@ export const apiClient = {
return handleResponse<AiAgentRuntimeWorkerShadowGateSnapshot>(res)
},
async getAiAgentOperationPermissionModel() {
const res = await fetch(`${API_BASE_URL}/agents/agent-operation-permission-model`)
return handleResponse<AiAgentOperationPermissionModelSnapshot>(res)
},
async getAiAgentOwnerApprovedFixtureDryRun() {
const res = await fetch(`${API_BASE_URL}/agents/agent-owner-approved-fixture-dry-run`)
return handleResponse<AiAgentOwnerApprovedFixtureDryRunSnapshot>(res)
@@ -2602,6 +2607,147 @@ export interface AiAgentRuntimeWorkerShadowGateSnapshot {
}
}
export interface AiAgentOperationPermissionModelSnapshot {
schema_version: 'ai_agent_operation_permission_model_v1'
generated_at: string
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: 'P2-101'
next_task_id: 'P2-102'
read_only_mode: true
runtime_authority: 'operation_permission_model_only_no_live_execution_or_send'
status_note: string
}
source_refs: string[]
operation_permission_truth: {
permission_model_ready: true
operation_category_matrix_ready: true
risk_tier_mapping_ready: true
agent_responsibility_mapping_ready: true
approval_gate_mapping_ready: true
manual_sop_lane_ready: true
p2_404_shadow_gate_handoff_ready: true
runtime_execution_enabled: false
gateway_queue_write_enabled: false
telegram_send_enabled: false
telegram_bot_api_call_enabled: false
delivery_receipt_write_enabled: false
ai_runtime_worker_enabled: false
medium_low_auto_worker_enabled: false
post_action_verifier_live_readback_enabled: false
production_write_enabled: false
secret_value_read_enabled: false
paid_provider_call_enabled: false
host_or_cluster_command_enabled: false
destructive_operation_enabled: false
work_window_transcript_display_allowed: false
runtime_execution_count_24h: number
gateway_queue_write_count_24h: number
telegram_send_count_24h: number
telegram_bot_api_call_count_24h: number
delivery_receipt_write_count_24h: number
ai_runtime_worker_run_count_24h: number
medium_low_auto_execution_count_24h: number
post_action_verifier_live_readback_count_24h: number
production_write_count_24h: number
secret_value_read_count_24h: number
paid_provider_call_count_24h: number
host_or_cluster_command_count_24h: number
destructive_operation_count_24h: number
truth_note: string
}
permission_lanes: Array<{
lane_id: 'observe_only' | 'no_write_replay_allowed' | 'proposal_only' | 'human_approval_required' | 'explicitly_blocked'
display_name: string
summary: string
allowed_outputs: string[]
required_gate_before_promotion: string
live_execution_allowed: false
production_write_allowed: false
}>
operation_categories: Array<{
category_id: string
display_name: string
risk_tier: 'low' | 'medium' | 'high' | 'critical'
permission_lane: 'observe_only' | 'no_write_replay_allowed' | 'proposal_only' | 'human_approval_required' | 'explicitly_blocked'
primary_agent: 'openclaw' | 'hermes' | 'nemotron'
allowed_outputs: string[]
blocked_actions: string[]
required_evidence: string[]
next_gate: string
queue_write_allowed: false
telegram_send_allowed: false
production_write_allowed: false
secret_value_read_allowed: false
destructive_action_allowed: false
live_execution_allowed: false
evidence_hash: string
}>
agent_permission_roles: Array<{
agent_id: 'openclaw' | 'hermes' | 'nemotron'
display_name: string
permission_responsibility: string
allowed_lanes: string[]
blocked_now: string[]
self_approval_allowed: false
live_action_count_24h: number
}>
gate_transitions: Array<{
gate_id: string
display_name: string
current_status: 'ready_for_review' | 'blocked_until_evidence' | 'blocked_by_policy'
required_before: string
next_safe_step: string
opens_live_execution: false
}>
operator_decision_templates: Array<{
template_id: string
display_name: string
when_to_use: string
human_instruction: string
creates_runtime_action: false
requires_human_review: true
}>
display_redaction_contract: {
redaction_required: true
raw_prompt_display_allowed: false
private_reasoning_display_allowed: false
secret_value_display_allowed: false
raw_telegram_payload_display_allowed: false
work_window_transcript_display_allowed: false
allowed_display_fields: string[]
blocked_display_fields: string[]
}
rollups: {
permission_lane_count: number
operation_category_count: number
observe_only_category_count: number
no_write_replay_allowed_category_count: number
proposal_only_category_count: number
human_approval_required_category_count: number
explicitly_blocked_category_count: number
human_approval_required_category_ids: string[]
explicitly_blocked_category_ids: string[]
agent_role_count: number
gate_transition_count: number
operator_decision_template_count: number
runtime_execution_count: number
gateway_queue_write_count: number
telegram_send_count: number
telegram_bot_api_call_count: number
delivery_receipt_write_count: number
ai_runtime_worker_run_count: number
medium_low_auto_execution_count: number
post_action_verifier_live_readback_count: number
production_write_count: number
secret_value_read_count: number
paid_provider_call_count: number
host_or_cluster_command_count: number
destructive_operation_count: number
}
}
export interface AiAgentOwnerApprovedFixtureDryRunSnapshot {
schema_version: 'ai_agent_owner_approved_fixture_dry_run_v1'
generated_at: string