···4646 ozone_allowed_labels: str = ""
4747 """comma separated list of labels that Phoebe is allowed to apply. both specified to the agent via prompting and validated before applying labels directly"""
48484949+ # osprey config
4950 osprey_base_url: str = ""
5051 """the base url for your osprey instance"""
5252+ osprey_repo_url: str = "https://github.com/roostorg/osprey"
5353+ """the url to fetch the osprey codebase from. used for letting the agent validate written rules directly"""
5454+ osprey_ruleset_url: str = "https://github.com/haileyok/atproto-ruleset"
5555+ """the url to fetch the osprey ruleset you are running. used when validating written rules (i.e. for having the needed features available for validation)"""
51565257 model_config = SettingsConfigDict(env_file=".env")
5358
+131-1
src/osprey/osprey.py
···11+import asyncio
22+import logging
33+from pathlib import Path
14import httpx
2536from src.osprey.config import OspreyConfig
47from src.osprey.udfs import UdfCatalog
5899+logger = logging.getLogger(__name__)
1010+1111+DATA_DIR = Path("./data")
1212+1313+OSPREY_REPO_PATH = Path("./data/osprey")
1414+1515+OSPREY_RULESET_PATH = Path("./data/ruleset")
1616+617718class Osprey:
88- def __init__(self, http_client: httpx.AsyncClient, base_url: str) -> None:
1919+ def __init__(
2020+ self,
2121+ http_client: httpx.AsyncClient,
2222+ base_url: str,
2323+ osprey_repo_url: str,
2424+ osprey_ruleset_url: str,
2525+ ) -> None:
926 self._http_client = http_client
1027 self._base_url = base_url
2828+ self._osprey_repo_url = osprey_repo_url
2929+ self._osprey_ruleset_url = osprey_ruleset_url
3030+3131+ async def initialize(self):
3232+ DATA_DIR.mkdir(exist_ok=True)
3333+3434+ if not OSPREY_REPO_PATH.exists():
3535+ logging.info(
3636+ f"Fetching Osprey repo from '{self._osprey_repo_url}' and saving to '{OSPREY_REPO_PATH}'"
3737+ )
3838+ await self._fetch_osprey_repo()
3939+ else:
4040+ logging.info("Osprey repo was already available, not fetching...")
4141+4242+ if not OSPREY_RULESET_PATH.exists():
4343+ logging.info(
4444+ f"Fetching Osprey ruleset from '{self._osprey_ruleset_url}' and saving to '{OSPREY_RULESET_PATH}'"
4545+ )
4646+ await self._fetch_osprey_ruleset()
4747+ else:
4848+ logging.info("Osprey ruleset was already available, not fetching...")
4949+5050+ logging.info("syncing python deps for osprey repo...")
5151+ await self._repo_deps()
5252+5353+ logging.info("verifying current ruleset validates properly...")
5454+ await self.validate_rules()
11551256 async def get_udfs(self) -> UdfCatalog:
1357 """gets the udf documentation from the given osprey instance"""
···2468 resp = await self._http_client.get(url)
2569 resp.raise_for_status()
2670 return OspreyConfig.model_validate(resp.json())
7171+7272+ async def _fetch_osprey_repo(self):
7373+ """fetches the osprey repo from the input http git url"""
7474+ process = await asyncio.create_subprocess_exec(
7575+ "git",
7676+ "clone",
7777+ self._osprey_repo_url,
7878+ str(OSPREY_REPO_PATH),
7979+ stderr=asyncio.subprocess.PIPE,
8080+ )
8181+8282+ assert process.stderr is not None
8383+8484+ await process.wait()
8585+8686+ if process.returncode != 0:
8787+ stderr_content = await process.stderr.read()
8888+ stderr_str = stderr_content.decode().strip()
8989+ raise RuntimeError(
9090+ f"Failed to fetch Osprey repo from specified url: {stderr_str}"
9191+ )
9292+9393+ async def _fetch_osprey_ruleset(self):
9494+ """Fetches the osprey ruleset from the input http git url"""
9595+ process = await asyncio.create_subprocess_exec(
9696+ "git",
9797+ "clone",
9898+ self._osprey_ruleset_url,
9999+ str(OSPREY_RULESET_PATH),
100100+ stderr=asyncio.subprocess.PIPE,
101101+ )
102102+103103+ assert process.stderr is not None
104104+105105+ await process.wait()
106106+107107+ if process.returncode != 0:
108108+ stderr_content = await process.stderr.read()
109109+ stderr_str = stderr_content.decode().strip()
110110+ raise RuntimeError(
111111+ f"Failed to fetch Osprey ruleset from specified url: {stderr_str}"
112112+ )
113113+114114+ async def _repo_deps(self):
115115+ """syncs deps with uv for the osprey repo"""
116116+ process = await asyncio.create_subprocess_exec(
117117+ "uv",
118118+ "sync",
119119+ "--frozen",
120120+ stderr=asyncio.subprocess.PIPE,
121121+ cwd=OSPREY_REPO_PATH,
122122+ )
123123+124124+ assert process.stderr is not None
125125+126126+ await process.wait()
127127+128128+ if process.returncode != 0:
129129+ stderr_content = await process.stderr.read()
130130+ stderr_str = stderr_content.decode().strip()
131131+ raise RuntimeError(
132132+ f"failed to sync python deps in osprey repo: {stderr_str}"
133133+ )
134134+135135+ async def validate_rules(self):
136136+ """validates the rules that are in the specified ruleset directory. returns error if speicifed, otherwise None"""
137137+ # uv run osprey-cli push-rules ../atproto-ruleset --dry-run
138138+ process = await asyncio.create_subprocess_exec(
139139+ "uv",
140140+ "run",
141141+ "osprey-cli",
142142+ "push-rules",
143143+ "../ruleset",
144144+ "--dry-run", # doesn't actually push rules, only validates
145145+ stderr=asyncio.subprocess.PIPE,
146146+ cwd=OSPREY_REPO_PATH,
147147+ )
148148+149149+ assert process.stderr is not None
150150+151151+ await process.wait()
152152+153153+ if process.returncode != 0:
154154+ stderr_content = await process.stderr.read()
155155+ stderr_str = stderr_content.decode().strip()
156156+ raise RuntimeError(f"WARNING! Rule validation failed! Error: {stderr_str}")