fix: plugin allowlist matching and windows session paths (#72)

This commit is contained in:
banteg
2026-01-09 19:25:10 +04:00
committed by GitHub
parent 2d9479541a
commit 8421ec8b4a
6 changed files with 51 additions and 10 deletions
+2 -2
View File
@@ -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:
+4 -1
View File
@@ -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
+9 -2
View File
@@ -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]:
+3 -3
View File
@@ -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
View File
@@ -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
+17
View File
@@ -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(