fix: plugin allowlist matching and windows session paths (#72)
This commit is contained in:
@@ -123,7 +123,7 @@ takopi opencode
|
|||||||
takopi pi
|
takopi pi
|
||||||
```
|
```
|
||||||
|
|
||||||
list available plugins (engines/transports), and override in a run:
|
list available plugins (engines/transports/commands), and override in a run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
takopi plugins
|
takopi plugins
|
||||||
@@ -147,7 +147,7 @@ if you prefer no notifications, `--no-final-notify` edits the progress message i
|
|||||||
|
|
||||||
## plugins
|
## plugins
|
||||||
|
|
||||||
Takopi supports entrypoint-based plugins for engines and transports.
|
Takopi supports entrypoint-based plugins for engines, transports, and command backends.
|
||||||
|
|
||||||
See:
|
See:
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,10 @@ def parse_context_line(
|
|||||||
branch = tokens[1][1:]
|
branch = tokens[1][1:]
|
||||||
project_key = project.lower()
|
project_key = project.lower()
|
||||||
if project_key not in projects.projects:
|
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)
|
ctx = RunContext(project=project_key, branch=branch)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from importlib.metadata import EntryPoint, entry_points
|
from importlib.metadata import EntryPoint, entry_points
|
||||||
|
import re
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from .ids import ID_PATTERN, is_valid_id
|
from .ids import ID_PATTERN, is_valid_id
|
||||||
@@ -11,6 +12,8 @@ ENGINE_GROUP = "takopi.engine_backends"
|
|||||||
TRANSPORT_GROUP = "takopi.transport_backends"
|
TRANSPORT_GROUP = "takopi.transport_backends"
|
||||||
COMMAND_GROUP = "takopi.command_backends"
|
COMMAND_GROUP = "takopi.command_backends"
|
||||||
|
|
||||||
|
_CANONICAL_NAME_RE = re.compile(r"[-_.]+")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class PluginLoadError:
|
class PluginLoadError:
|
||||||
@@ -111,7 +114,11 @@ def entrypoint_distribution_name(ep: EntryPoint) -> str | None:
|
|||||||
def normalize_allowlist(allowlist: Iterable[str] | None) -> set[str] | None:
|
def normalize_allowlist(allowlist: Iterable[str] | None) -> set[str] | None:
|
||||||
if allowlist is None:
|
if allowlist is None:
|
||||||
return 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
|
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)
|
dist_name = entrypoint_distribution_name(ep)
|
||||||
if dist_name is None:
|
if dist_name is None:
|
||||||
return False
|
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]:
|
def _entrypoint_sort_key(ep: EntryPoint) -> tuple[str, str, str]:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path, PurePath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -445,10 +445,10 @@ class PiRunner(ResumeTokenMixin, JsonlSubprocessRunner):
|
|||||||
return f'"{escaped}"'
|
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")
|
agent_dir = os.environ.get("PI_CODING_AGENT_DIR")
|
||||||
base = Path(agent_dir).expanduser() if agent_dir else Path.home() / ".pi" / "agent"
|
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
|
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
|
from unittest.mock import patch
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from takopi.model import ActionEvent, CompletedEvent, ResumeToken, StartedEvent
|
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
|
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)
|
default_session_dir.assert_called_once_with(project_cwd)
|
||||||
assert str(session_root) in session_path
|
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"]
|
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:
|
def test_validator_errors_are_captured(monkeypatch) -> None:
|
||||||
entrypoints = [
|
entrypoints = [
|
||||||
FakeEntryPoint(
|
FakeEntryPoint(
|
||||||
|
|||||||
Reference in New Issue
Block a user