feat(plugins): discover packages and persist state
This commit is contained in:
137
app-instance/backend/beaver/plugins/models.py
Normal file
137
app-instance/backend/beaver/plugins/models.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""Models for declarative Beaver plugin packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginSkillDeclaration:
|
||||
name: str
|
||||
relative_path: str
|
||||
root: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginManifest:
|
||||
schema_version: int
|
||||
plugin_id: str
|
||||
name: str
|
||||
version: str
|
||||
root: Path
|
||||
manifest_path: Path
|
||||
display_path: str
|
||||
skills: tuple[PluginSkillDeclaration, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginSkillFileDigest:
|
||||
path: str
|
||||
size: int
|
||||
executable: bool
|
||||
content_hash: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginSkillTreeDigest:
|
||||
skill_content_hash: str
|
||||
skill_tree_hash: str
|
||||
files: tuple[PluginSkillFileDigest, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginDiscoveryError:
|
||||
path: Path
|
||||
display_path: str
|
||||
message: str
|
||||
plugin_id: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginDiscoveryResult:
|
||||
manifests: dict[str, PluginManifest]
|
||||
errors: list[PluginDiscoveryError]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PluginSkillBinding:
|
||||
accepted_upstream_tree_hash: str | None = None
|
||||
observed_upstream_tree_hash: str | None = None
|
||||
accepted_beaver_version: str | None = None
|
||||
current_beaver_version: str | None = None
|
||||
pending_candidate_id: str | None = None
|
||||
status: str = "discovered"
|
||||
last_error: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"accepted_upstream_tree_hash": self.accepted_upstream_tree_hash,
|
||||
"observed_upstream_tree_hash": self.observed_upstream_tree_hash,
|
||||
"accepted_beaver_version": self.accepted_beaver_version,
|
||||
"current_beaver_version": self.current_beaver_version,
|
||||
"pending_candidate_id": self.pending_candidate_id,
|
||||
"status": self.status,
|
||||
"last_error": self.last_error,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any] | None) -> "PluginSkillBinding":
|
||||
data = payload if isinstance(payload, dict) else {}
|
||||
return cls(
|
||||
accepted_upstream_tree_hash=_optional_str(data.get("accepted_upstream_tree_hash")),
|
||||
observed_upstream_tree_hash=_optional_str(data.get("observed_upstream_tree_hash")),
|
||||
accepted_beaver_version=_optional_str(data.get("accepted_beaver_version")),
|
||||
current_beaver_version=_optional_str(data.get("current_beaver_version")),
|
||||
pending_candidate_id=_optional_str(data.get("pending_candidate_id")),
|
||||
status=str(data.get("status") or "discovered"),
|
||||
last_error=_optional_str(data.get("last_error")),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PluginState:
|
||||
plugin_id: str
|
||||
enabled: bool = False
|
||||
updates_paused: bool = False
|
||||
installed_version: str | None = None
|
||||
manifest_path: str | None = None
|
||||
status: str = "discovered"
|
||||
last_error: str | None = None
|
||||
skills: dict[str, PluginSkillBinding] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"enabled": self.enabled,
|
||||
"updates_paused": self.updates_paused,
|
||||
"installed_version": self.installed_version,
|
||||
"manifest_path": self.manifest_path,
|
||||
"status": self.status,
|
||||
"last_error": self.last_error,
|
||||
"skills": {name: binding.to_dict() for name, binding in sorted(self.skills.items())},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, plugin_id: str, payload: dict[str, Any] | None) -> "PluginState":
|
||||
data = payload if isinstance(payload, dict) else {}
|
||||
raw_skills = data.get("skills") if isinstance(data.get("skills"), dict) else {}
|
||||
return cls(
|
||||
plugin_id=plugin_id,
|
||||
enabled=bool(data.get("enabled", False)),
|
||||
updates_paused=bool(data.get("updates_paused", False)),
|
||||
installed_version=_optional_str(data.get("installed_version")),
|
||||
manifest_path=_optional_str(data.get("manifest_path")),
|
||||
status=str(data.get("status") or "discovered"),
|
||||
last_error=_optional_str(data.get("last_error")),
|
||||
skills={
|
||||
str(name): PluginSkillBinding.from_dict(binding if isinstance(binding, dict) else {})
|
||||
for name, binding in raw_skills.items()
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _optional_str(value: Any) -> str | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return str(value)
|
||||
Reference in New Issue
Block a user