chore(fleet): sync operator memory, hatch menu dialogs, and watchdog alerts
This commit is contained in:
@@ -35,6 +35,58 @@ TARGET_MD_FILES = [
|
||||
"IDENTITY.md",
|
||||
]
|
||||
|
||||
|
||||
class MDValidationError(ValueError):
|
||||
"""An md account/filename/path failed safety validation.
|
||||
|
||||
box-ctl.py maps this to BAD_NAME; it is always raised before any
|
||||
gateway call or filesystem write.
|
||||
"""
|
||||
|
||||
|
||||
MD_ACCOUNT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$")
|
||||
MD_FILENAME_RE = re.compile(r"^[A-Za-z0-9_.-]{1,128}$")
|
||||
MD_SUBPATH_RE = re.compile(r"^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*$")
|
||||
|
||||
|
||||
def validate_account(account: str) -> str:
|
||||
"""Reject account values that could escape the cookies/config path."""
|
||||
if not isinstance(account, str) or not MD_ACCOUNT_RE.fullmatch(account):
|
||||
raise MDValidationError(
|
||||
"Invalid agent account %r: must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$"
|
||||
% (account,))
|
||||
return account
|
||||
|
||||
|
||||
def validate_filename(filename: str, template_only: bool = False) -> str:
|
||||
"""Reject filenames that could escape the md directory.
|
||||
|
||||
Template flows (diff/amend/append/pull) additionally require one of
|
||||
TARGET_MD_FILES, since they index into shared/operators/.
|
||||
"""
|
||||
if template_only:
|
||||
if filename not in TARGET_MD_FILES:
|
||||
raise MDValidationError(
|
||||
"Unknown shared template %r: must be one of %s"
|
||||
% (filename, sorted(TARGET_MD_FILES)))
|
||||
return filename
|
||||
if not isinstance(filename, str) or filename in (".", "..") \
|
||||
or not MD_FILENAME_RE.fullmatch(filename):
|
||||
raise MDValidationError(
|
||||
"Invalid filename %r: plain basename, no directories" % (filename,))
|
||||
return filename
|
||||
|
||||
|
||||
def validate_subpath(path: str) -> str:
|
||||
"""Reject list paths that escape the container root ('' = root)."""
|
||||
if path in (None, ""):
|
||||
return ""
|
||||
if not isinstance(path, str) or not MD_SUBPATH_RE.fullmatch(path) \
|
||||
or ".." in path.split("/"):
|
||||
raise MDValidationError(
|
||||
"Invalid list path %r: subdir without '..'" % (path,))
|
||||
return path
|
||||
|
||||
# Tunnel / Port inventory
|
||||
TUNNEL_PORTS = {
|
||||
"muse-main": {"port": 2224, "terminal": 7681, "user": "muse"},
|
||||
@@ -49,6 +101,7 @@ TUNNEL_PORTS = {
|
||||
|
||||
def get_gateway(account: str) -> "Gateway":
|
||||
"""Obtain an authenticated Gateway connection for an account."""
|
||||
validate_account(account)
|
||||
if not Gateway:
|
||||
raise RuntimeError("muse_cli.gateway module is not available")
|
||||
conf_dir = Path.home() / ".config" / "muse-cli" / account
|
||||
@@ -63,6 +116,7 @@ def get_gateway(account: str) -> "Gateway":
|
||||
|
||||
def list_files(account: str, path: str = "") -> list:
|
||||
"""List files in the agent container filesystem via Hatch."""
|
||||
path = validate_subpath(path)
|
||||
gw = get_gateway(account)
|
||||
res = gw.call_json("fs.list", body={"path": path})
|
||||
return res.get("entries", [])
|
||||
@@ -70,6 +124,7 @@ def list_files(account: str, path: str = "") -> list:
|
||||
|
||||
def read_md(account: str, filename: str, max_bytes: int = 200000) -> dict:
|
||||
"""Read a markdown file from the agent container via Hatch."""
|
||||
validate_filename(filename)
|
||||
gw = get_gateway(account)
|
||||
offset = 0
|
||||
chunks = []
|
||||
@@ -100,6 +155,7 @@ def read_md(account: str, filename: str, max_bytes: int = 200000) -> dict:
|
||||
|
||||
def write_md(account: str, filename: str, text: str, overwrite: bool = True, append: bool = False) -> dict:
|
||||
"""Write content to a file in the agent container via Hatch."""
|
||||
validate_filename(filename)
|
||||
gw = get_gateway(account)
|
||||
body = {
|
||||
"path": filename,
|
||||
@@ -121,6 +177,8 @@ def write_md(account: str, filename: str, text: str, overwrite: bool = True, app
|
||||
def audit_agents(accounts: list = None) -> dict:
|
||||
"""Audit markdown files and operational DRIVE across fleet agents."""
|
||||
accounts = accounts or VALID_ACCOUNTS
|
||||
for acct in accounts:
|
||||
validate_account(acct)
|
||||
results = {}
|
||||
|
||||
for acct in accounts:
|
||||
@@ -213,6 +271,7 @@ def audit_agents(accounts: list = None) -> dict:
|
||||
|
||||
def diff_md(account: str, filename: str) -> dict:
|
||||
"""Compare an agent's container file against the shared operator template."""
|
||||
validate_filename(filename, template_only=True)
|
||||
local_path = SHARED_OPERATORS / filename
|
||||
if not local_path.exists():
|
||||
raise FileNotFoundError(f"Local template {local_path} not found")
|
||||
@@ -242,6 +301,7 @@ def diff_md(account: str, filename: str) -> dict:
|
||||
def amend_md(filename: str, content: str, author: str = "operator", reason: str = "") -> dict:
|
||||
"""Amend a centralized shared operator template in shared/operators/ with safety validation and git commit."""
|
||||
import subprocess
|
||||
validate_filename(filename, template_only=True)
|
||||
|
||||
local_path = SHARED_OPERATORS / filename
|
||||
if not local_path.exists():
|
||||
@@ -296,6 +356,7 @@ def amend_md(filename: str, content: str, author: str = "operator", reason: str
|
||||
|
||||
def append_md(filename: str, text: str, author: str = "operator", section: str = None) -> dict:
|
||||
"""Safely append an amendment or lesson to a centralized shared template."""
|
||||
validate_filename(filename, template_only=True)
|
||||
local_path = SHARED_OPERATORS / filename
|
||||
if not local_path.exists():
|
||||
raise FileNotFoundError(f"Shared operator file {filename} does not exist in {SHARED_OPERATORS}")
|
||||
@@ -311,6 +372,7 @@ def append_md(filename: str, text: str, author: str = "operator", section: str =
|
||||
|
||||
def pull_md(account: str, filename: str) -> dict:
|
||||
"""Pull the canonical centralized template from shared/operators/ into an agent's container."""
|
||||
validate_filename(filename, template_only=True)
|
||||
local_path = SHARED_OPERATORS / filename
|
||||
if not local_path.exists():
|
||||
raise FileNotFoundError(f"Shared operator file {filename} does not exist in {SHARED_OPERATORS}")
|
||||
|
||||
Reference in New Issue
Block a user