fix: plugin allowlist matching and windows session paths (#72)
This commit is contained in:
@@ -123,7 +123,7 @@ takopi opencode
|
||||
takopi pi
|
||||
```
|
||||
|
||||
list available plugins (engines/transports), and override in a run:
|
||||
list available plugins (engines/transports/commands), and override in a run:
|
||||
|
||||
```sh
|
||||
takopi plugins
|
||||
@@ -147,7 +147,7 @@ if you prefer no notifications, `--no-final-notify` edits the progress message i
|
||||
|
||||
## plugins
|
||||
|
||||
Takopi supports entrypoint-based plugins for engines and transports.
|
||||
Takopi supports entrypoint-based plugins for engines, transports, and command backends.
|
||||
|
||||
See:
|
||||
|
||||
|
||||
@@ -126,7 +126,10 @@ def parse_context_line(
|
||||
branch = tokens[1][1:]
|
||||
project_key = project.lower()
|
||||
if project_key not in projects.projects:
|
||||
raise DirectiveError(f"unknown project {project!r} in ctx line")
|
||||
raise DirectiveError(
|
||||
f"unknown project {project!r} in ctx line; start a new thread or "
|
||||
"add it back to your config"
|
||||
)
|
||||
ctx = RunContext(project=project_key, branch=branch)
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from importlib.metadata import EntryPoint, entry_points
|
||||
import re
|
||||
from typing import Any, Callable
|
||||
|
||||
from .ids import ID_PATTERN, is_valid_id
|
||||
@@ -11,6 +12,8 @@ ENGINE_GROUP = "takopi.engine_backends"
|
||||
TRANSPORT_GROUP = "takopi.transport_backends"
|
||||
COMMAND_GROUP = "takopi.command_backends"
|
||||
|
||||
_CANONICAL_NAME_RE = re.compile(r"[-_.]+")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PluginLoadError:
|
||||
@@ -111,7 +114,11 @@ def entrypoint_distribution_name(ep: EntryPoint) -> str | None:
|
||||
def normalize_allowlist(allowlist: Iterable[str] | None) -> set[str] | None:
|
||||
if allowlist is None:
|
||||
return None
|
||||
cleaned = {item.strip().lower() for item in allowlist if item and item.strip()}
|
||||
cleaned = {
|
||||
_CANONICAL_NAME_RE.sub("-", item.strip()).lower()
|
||||
for item in allowlist
|
||||
if item and item.strip()
|
||||
}
|
||||
return cleaned or None
|
||||
|
||||
|
||||
@@ -121,7 +128,7 @@ def is_entrypoint_allowed(ep: EntryPoint, allowlist: set[str] | None) -> bool:
|
||||
dist_name = entrypoint_distribution_name(ep)
|
||||
if dist_name is None:
|
||||
return False
|
||||
return dist_name.lower() in allowlist
|
||||
return _CANONICAL_NAME_RE.sub("-", dist_name).lower() in allowlist
|
||||
|
||||
|
||||
def _entrypoint_sort_key(ep: EntryPoint) -> tuple[str, str, str]:
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePath
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -445,10 +445,10 @@ class PiRunner(ResumeTokenMixin, JsonlSubprocessRunner):
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def _default_session_dir(cwd: Path) -> Path:
|
||||
def _default_session_dir(cwd: PurePath) -> Path:
|
||||
agent_dir = os.environ.get("PI_CODING_AGENT_DIR")
|
||||
base = Path(agent_dir).expanduser() if agent_dir else Path.home() / ".pi" / "agent"
|
||||
safe_path = f"--{str(cwd).lstrip('/\\\\').replace('/', '-').replace('\\\\', '-').replace(':', '-')}--"
|
||||
safe_path = f"--{str(cwd).lstrip('/\\\\').replace('/', '-').replace('\\', '-').replace(':', '-')}--"
|
||||
return base / "sessions" / safe_path
|
||||
|
||||
|
||||
|
||||
+16
-2
@@ -1,11 +1,17 @@
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from unittest.mock import patch
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
|
||||
from takopi.model import ActionEvent, CompletedEvent, ResumeToken, StartedEvent
|
||||
from takopi.runners.pi import ENGINE, PiRunner, PiStreamState, translate_pi_event
|
||||
from takopi.runners.pi import (
|
||||
ENGINE,
|
||||
PiRunner,
|
||||
PiStreamState,
|
||||
_default_session_dir,
|
||||
translate_pi_event,
|
||||
)
|
||||
from takopi.schemas import pi as pi_schema
|
||||
|
||||
|
||||
@@ -152,3 +158,11 @@ def test_session_path_prefers_run_base_dir(tmp_path: Path) -> None:
|
||||
|
||||
default_session_dir.assert_called_once_with(project_cwd)
|
||||
assert str(session_root) in session_path
|
||||
|
||||
|
||||
def test_session_path_sanitizes_windows_separators() -> None:
|
||||
cwd = PureWindowsPath("C:\\foo\\bar")
|
||||
session_dir = _default_session_dir(cwd)
|
||||
name = session_dir.name
|
||||
assert "\\" not in name
|
||||
assert ":" not in name
|
||||
|
||||
@@ -97,6 +97,23 @@ def test_allowlist_filters_by_distribution(monkeypatch) -> None:
|
||||
assert ids == ["codex"]
|
||||
|
||||
|
||||
def test_allowlist_canonicalizes_distribution_names(monkeypatch) -> None:
|
||||
entrypoints = [
|
||||
FakeEntryPoint(
|
||||
"slack",
|
||||
"takopi.transport.slack:BACKEND",
|
||||
plugins.TRANSPORT_GROUP,
|
||||
dist_name="takopi-transport-slack",
|
||||
)
|
||||
]
|
||||
install_entrypoints(monkeypatch, entrypoints)
|
||||
|
||||
ids = plugins.list_ids(
|
||||
plugins.TRANSPORT_GROUP, allowlist=["takopi_transport.slack"]
|
||||
)
|
||||
assert ids == ["slack"]
|
||||
|
||||
|
||||
def test_validator_errors_are_captured(monkeypatch) -> None:
|
||||
entrypoints = [
|
||||
FakeEntryPoint(
|
||||
|
||||
Reference in New Issue
Block a user