Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,14 @@ apis:
anthropic:
model: "claude-3-5-sonnet-20241022"
max_tokens: 4096
temperature: 0.0
temperature: 0.0

# Obsidian 集成(可选)
obsidian:
enabled: false
vault_path: ""
root_dir: "PaperBot"
paper_template_path: null
auto_export_on_save: true
auto_sync_tracks: true
export_limit: 200
50 changes: 50 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ class ReportEngineConf(BaseModel):
model_tiers: Dict[str, str] = Field(default_factory=dict)


class ObsidianConfig(BaseModel):
model_config = ConfigDict(extra="ignore")

enabled: bool = False
vault_path: str = ""
root_dir: str = "PaperBot"
paper_template_path: Optional[str] = None
auto_export_on_save: bool = True
auto_sync_tracks: bool = True
export_limit: int = 200


class CollabHostConfig(BaseModel):
model_config = ConfigDict(extra="ignore")

Expand All @@ -150,6 +162,7 @@ class Settings(BaseModel):
logging: LoggingConfig = Field(default_factory=LoggingConfig)
api: APIConfig = Field(default_factory=APIConfig)
report_engine: ReportEngineConf = Field(default_factory=ReportEngineConf)
obsidian: ObsidianConfig = Field(default_factory=ObsidianConfig)
collab: Dict[str, Any] = Field(
default_factory=lambda: {
"enabled": False,
Expand Down Expand Up @@ -213,6 +226,7 @@ def from_dict(cls, config_data: Dict[str, Any]) -> Settings:
"output",
"logging",
"report_engine",
"obsidian",
"mode",
"offline",
):
Expand Down Expand Up @@ -310,6 +324,13 @@ def load_environment_variables(self) -> None:
re_max = os.getenv("PAPERBOT_RE_MAX_WORDS")
re_scenario = os.getenv("PAPERBOT_RE_SCENARIO")
re_tiers = os.getenv("PAPERBOT_RE_MODEL_TIERS")
obsidian_enabled = os.getenv("PAPERBOT_OBSIDIAN_ENABLED")
obsidian_vault = os.getenv("PAPERBOT_OBSIDIAN_VAULT_PATH")
obsidian_root = os.getenv("PAPERBOT_OBSIDIAN_ROOT_DIR")
obsidian_template = os.getenv("PAPERBOT_OBSIDIAN_PAPER_TEMPLATE")
obsidian_auto_export = os.getenv("PAPERBOT_OBSIDIAN_AUTO_EXPORT")
obsidian_auto_sync_tracks = os.getenv("PAPERBOT_OBSIDIAN_AUTO_SYNC_TRACKS")
obsidian_export_limit = os.getenv("PAPERBOT_OBSIDIAN_EXPORT_LIMIT")
if re_enabled is not None:
self.report_engine.enabled = re_enabled.lower() in ("1", "true", "yes", "on")
if re_api:
Expand Down Expand Up @@ -339,6 +360,34 @@ def load_environment_variables(self) -> None:
tiers[k.strip()] = v.strip()
self.report_engine.model_tiers = tiers

if obsidian_enabled is not None:
self.obsidian.enabled = obsidian_enabled.lower() in ("1", "true", "yes", "on")
if obsidian_vault:
self.obsidian.vault_path = obsidian_vault
if obsidian_root:
self.obsidian.root_dir = obsidian_root
if obsidian_template:
self.obsidian.paper_template_path = obsidian_template
if obsidian_auto_export is not None:
self.obsidian.auto_export_on_save = obsidian_auto_export.lower() in (
"1",
"true",
"yes",
"on",
)
if obsidian_auto_sync_tracks is not None:
self.obsidian.auto_sync_tracks = obsidian_auto_sync_tracks.lower() in (
"1",
"true",
"yes",
"on",
)
if obsidian_export_limit:
try:
self.obsidian.export_limit = max(1, int(obsidian_export_limit))
except ValueError:
pass
Comment on lines +388 to +389

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Silently ignoring a ValueError here can hide configuration issues from the user. If PAPERBOT_OBSIDIAN_EXPORT_LIMIT is set to a non-integer value, it will be ignored, and the default value will be used without any warning. It would be better to log a warning to inform the user about the invalid configuration value.


# Collab host LLM
host_api = os.getenv("PAPERBOT_HOST_API_KEY")
host_model = os.getenv("PAPERBOT_HOST_MODEL")
Expand Down Expand Up @@ -366,6 +415,7 @@ def to_dict(self) -> Dict[str, Any]:
"logging": self.logging.model_dump(),
"api": self.api.model_dump(),
"report_engine": self.report_engine.model_dump(),
"obsidian": self.obsidian.model_dump(),
"collab": self.collab,
"conferences": {name: conf.model_dump() for name, conf in self.conferences.items()},
}
Expand Down
11 changes: 11 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ PAPERBOT_INTELLIGENCE_REDDIT_SUBREDDITS=MachineLearning,LocalLLaMA,OpenAI
# PAPERBOT_DB_URL=postgresql+psycopg://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres?sslmode=require
PAPERBOT_DB_URL=

# ----------------------------
# Obsidian export (optional)
# ----------------------------
PAPERBOT_OBSIDIAN_ENABLED=false
PAPERBOT_OBSIDIAN_VAULT_PATH=
PAPERBOT_OBSIDIAN_ROOT_DIR=PaperBot
PAPERBOT_OBSIDIAN_PAPER_TEMPLATE=
PAPERBOT_OBSIDIAN_AUTO_EXPORT=true
PAPERBOT_OBSIDIAN_AUTO_SYNC_TRACKS=true
PAPERBOT_OBSIDIAN_EXPORT_LIMIT=200

# ----------------------------
# Report Engine (optional)
# ----------------------------
Expand Down
45 changes: 44 additions & 1 deletion src/paperbot/api/routes/research.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
from paperbot.context_engine.track_router import TrackRouter
from paperbot.domain.paper_identity import normalize_arxiv_id, normalize_doi
from paperbot.infrastructure.api_clients.semantic_scholar import SemanticScholarClient
from paperbot.infrastructure.exporters.obsidian_sync import (
export_track_snapshot,
obsidian_auto_export_enabled,
)
from paperbot.infrastructure.stores.memory_store import SqlAlchemyMemoryStore
from paperbot.infrastructure.stores.research_store import SqlAlchemyResearchStore
from paperbot.infrastructure.stores.workflow_metric_store import WorkflowMetricStore
Expand Down Expand Up @@ -59,6 +63,26 @@ def _get_track_router() -> TrackRouter:
memory_store=_get_memory_store(),
)
return _track_router


def _schedule_obsidian_export(
background_tasks: BackgroundTasks,
*,
user_id: str,
track_id: int,
for_tracks: bool = False,
) -> None:
if track_id <= 0:
return
if not obsidian_auto_export_enabled(for_tracks=for_tracks):
return
background_tasks.add_task(
export_track_snapshot,
user_id=user_id,
track_id=track_id,
)


ENABLE_ANCHOR_AUTHORS = os.getenv("PAPERBOT_ENABLE_ANCHOR_AUTHORS", "true").lower() == "true"

_DISCOVERY_STOPWORDS: Set[str] = {
Expand Down Expand Up @@ -304,6 +328,12 @@ def create_track(req: TrackCreateRequest, background_tasks: BackgroundTasks):
_schedule_embedding_precompute(
background_tasks, user_id=req.user_id, track_ids=[int(track.get("id") or 0)]
)
_schedule_obsidian_export(
background_tasks,
user_id=req.user_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The research API endpoints for creating tracks, updating tracks, and adding paper feedback use a user-supplied user_id (either from query parameters or the request body) without any authentication or authorization checks. This allows an attacker to access, modify, or trigger actions (like Obsidian exports) for any other user's data by simply providing their user_id.

This pull request adds automatic Obsidian snapshot exports to these endpoints, which also use the unverified user_id, extending the impact of this vulnerability.

Remediation:

  1. Implement a robust authentication system (e.g., JWT, OAuth2).
  2. Replace the user-supplied user_id in API requests with the identity of the currently authenticated user.
  3. Verify that the authenticated user has the necessary permissions to access or modify the requested resource.

track_id=int(track.get("id") or 0),
for_tracks=True,
)
return TrackResponse(track=track)


Expand Down Expand Up @@ -449,6 +479,12 @@ def update_track(
if not track:
raise HTTPException(status_code=404, detail="Track not found")
_schedule_embedding_precompute(background_tasks, user_id=user_id, track_ids=[track_id])
_schedule_obsidian_export(
background_tasks,
user_id=user_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The user_id parameter is taken directly from the query string and used to schedule an Obsidian export without verifying if the requester is authorized to act on behalf of that user. This is a classic IDOR vulnerability.

track_id=track_id,
for_tracks=True,
)
return TrackResponse(track=track)


Expand Down Expand Up @@ -982,7 +1018,7 @@ class PaperFeedbackResponse(BaseModel):


@router.post("/research/papers/feedback", response_model=PaperFeedbackResponse)
def add_paper_feedback(req: PaperFeedbackRequest):
def add_paper_feedback(req: PaperFeedbackRequest, background_tasks: BackgroundTasks):
set_trace_id() # Initialize trace_id for this request
Logger.info(f"Received paper feedback request, action={req.action}", file=LogFiles.HARVEST)
research_store = _get_research_store()
Expand Down Expand Up @@ -1068,6 +1104,13 @@ def add_paper_feedback(req: PaperFeedbackRequest):
Logger.info("Paper feedback recorded successfully", file=LogFiles.HARVEST)
normalized_action = research_store._normalize_feedback_action(req.action)
current_action = research_store._effective_feedback_action(normalized_action)
if normalized_action in {"save", "unsave"}:
_schedule_obsidian_export(
background_tasks,
user_id=req.user_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The req.user_id from the request body is used to trigger an Obsidian export without any authentication check. An attacker can provide any user_id to trigger exports for other users.

track_id=int(track_id),
for_tracks=False,
)
return PaperFeedbackResponse(
feedback=fb,
library_paper_id=library_paper_id,
Expand Down
1 change: 1 addition & 0 deletions src/paperbot/application/ports/vault_exporter_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ def export_library_snapshot(
saved_items: List[Dict[str, Any]],
track: Optional[Dict[str, Any]] = None,
root_dir: str = "PaperBot",
paper_template_path: Optional[Path] = None,
) -> Dict[str, Any]: ...
8 changes: 7 additions & 1 deletion src/paperbot/infrastructure/exporters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""Filesystem exporters for external knowledge tools."""

from .obsidian_exporter import ObsidianFilesystemExporter
from .obsidian_sync import export_track_snapshot, get_obsidian_config, obsidian_auto_export_enabled

__all__ = ["ObsidianFilesystemExporter"]
__all__ = [
"ObsidianFilesystemExporter",
"export_track_snapshot",
"get_obsidian_config",
"obsidian_auto_export_enabled",
]
Loading
Loading