feat(config,mcps,policy,compose): komodo alerts with hold, vibegram and ha python_tools, vault rw home zones
This commit is contained in:
@@ -67,6 +67,7 @@ MUTATING = frozenset(
|
||||
}
|
||||
)
|
||||
COPYING = frozenset({"cp", "install", "rsync"})
|
||||
DELETING = frozenset({"rm", "rmdir", "unlink", "shred", "dd", "truncate"})
|
||||
INPLACE = frozenset({"sed", "perl"})
|
||||
REDIRECTS = frozenset({">", ">>", ">|", "&>", "&>>", ">&"})
|
||||
SEPARATORS = frozenset({";", "&&", "||", "|", "&", "(", ")"})
|
||||
@@ -74,18 +75,36 @@ SEPARATORS = frozenset({";", "&&", "||", "|", "&", "(", ")"})
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Zones:
|
||||
"""Зоны vault: ``write`` - полная запись, ``create`` - только новые файлы."""
|
||||
"""Зоны vault.
|
||||
|
||||
``write`` - всё, включая удаление; ``create`` - только новые файлы;
|
||||
``edit`` - создавать, править, переносить можно, удалять - нет;
|
||||
``protected`` - не трогать. Приоритет: write > protected > create > edit;
|
||||
путь в vault вне всех зон - отказ. Vault целиком в ``edit`` - это
|
||||
«дом» диспетчера (решение h, 2026-08-30): заметки, задачи, люди - его;
|
||||
удаление чужого и `.obsidian`/`мета` остаются за Бобром.
|
||||
"""
|
||||
|
||||
vault: Path
|
||||
write: tuple[Path, ...]
|
||||
create: tuple[Path, ...]
|
||||
create: tuple[Path, ...] = ()
|
||||
edit: tuple[Path, ...] = ()
|
||||
protected: tuple[Path, ...] = ()
|
||||
|
||||
def verdict(self, path: Path, *, creating: bool) -> Deny | None:
|
||||
def verdict(
|
||||
self, path: Path, *, creating: bool, deleting: bool = False
|
||||
) -> Deny | None:
|
||||
if not _under(path, self.vault):
|
||||
return None
|
||||
if any(_under(path, zone) for zone in self.write):
|
||||
return None
|
||||
rel = path.relative_to(self.vault)
|
||||
for zone in self.protected:
|
||||
if _under(path, zone):
|
||||
return Deny(
|
||||
reason=f"{rel}: «{zone.relative_to(self.vault)}» не трогаем - "
|
||||
"это зона Бобра"
|
||||
)
|
||||
for zone in self.create:
|
||||
if _under(path, zone):
|
||||
if creating and not path.exists():
|
||||
@@ -94,6 +113,17 @@ class Zones:
|
||||
reason=f"{rel}: в «{zone.relative_to(self.vault)}» можно только "
|
||||
"создавать новые файлы, существующие не трогаем"
|
||||
)
|
||||
for zone in self.edit:
|
||||
if _under(path, zone):
|
||||
if deleting:
|
||||
zones = ", ".join(
|
||||
f"«{z.relative_to(self.vault)}»" for z in self.write
|
||||
)
|
||||
return Deny(
|
||||
reason=f"{rel}: удалять можно только в {zones}; чужие "
|
||||
"заметки не удаляем - скажи Бобру, он сам"
|
||||
)
|
||||
return None
|
||||
zones = ", ".join(f"«{z.relative_to(self.vault)}»" for z in self.write)
|
||||
return Deny(reason=f"{rel}: vault только на чтение; писать можно в {zones}")
|
||||
|
||||
@@ -125,8 +155,10 @@ def bash_zones(zones: Zones) -> PolicyRule:
|
||||
command = call.input.get("command")
|
||||
if not isinstance(command, str):
|
||||
return None
|
||||
for target, creating in _bash_targets(command):
|
||||
deny = zones.verdict(call.resolve(target), creating=creating)
|
||||
for target, creating, deleting in _bash_targets(command):
|
||||
deny = zones.verdict(
|
||||
call.resolve(target), creating=creating, deleting=deleting
|
||||
)
|
||||
if deny is not None:
|
||||
return Deny(reason=f"Bash: {deny.reason}")
|
||||
return None
|
||||
@@ -134,7 +166,8 @@ def bash_zones(zones: Zones) -> PolicyRule:
|
||||
return rule
|
||||
|
||||
|
||||
def _bash_targets(command: str) -> Iterator[tuple[str, bool]]:
|
||||
def _bash_targets(command: str) -> Iterator[tuple[str, bool, bool]]:
|
||||
"""``(путь, создаёт, удаляет)`` для каждого пути, который команда трогает."""
|
||||
for segment in _segments(command):
|
||||
plain: list[str] = []
|
||||
i = 0
|
||||
@@ -142,7 +175,7 @@ def _bash_targets(command: str) -> Iterator[tuple[str, bool]]:
|
||||
word = segment[i]
|
||||
if word in REDIRECTS:
|
||||
if i + 1 < len(segment) and _pathlike(segment[i + 1]):
|
||||
yield segment[i + 1], True
|
||||
yield segment[i + 1], True, False
|
||||
i += 2
|
||||
continue
|
||||
plain.append(word)
|
||||
@@ -156,7 +189,7 @@ def _bash_targets(command: str) -> Iterator[tuple[str, bool]]:
|
||||
if word in COPYING:
|
||||
args = args[-1:]
|
||||
for arg in args:
|
||||
yield arg, False
|
||||
yield arg, False, word in DELETING
|
||||
break
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user