feat: intégration TikTak #197-200 + scoped tokens, webhooks filtrés, listes mtime/size ; fix e2e-server BUG-112
CI / lint (push) Successful in 3m17s
CI / security (push) Failing after 1m51s
CI / test (push) Successful in 5m14s
CI / build (push) Successful in 1m50s
CI / e2e (push) Successful in 18m18s

This commit is contained in:
2026-10-14 11:42:53 -04:00
parent 2be0b15bcf
commit f783394f09
59 changed files with 3233 additions and 77 deletions
+2
View File
@@ -19,6 +19,7 @@ from backend.tools import service as _service # noqa: F401 (registers tools)
from backend.tools import spreadsheets as _spreadsheets # noqa: F401 (registers existing-workbook tools #153 A6)
from backend.tools import web as _web # noqa: F401 (registers web tools)
from backend.tools.context import (
FILES_SCOPE_TOOLS,
ToolConfirmationRequired,
ToolContext,
ToolError,
@@ -52,6 +53,7 @@ from backend.tools.registry import (
from backend.tools.schemas import ToolResult
__all__ = [
"FILES_SCOPE_TOOLS",
"ToolConfirmationRequired",
"ToolContext",
"ToolError",
+49
View File
@@ -117,6 +117,29 @@ class ToolRateLimitError(ToolError):
self.retry_after = retry_after
#: Tools a ``files``-scoped API token (#199, synchro TikTak) may invoke :
#: vault file read/write surface only — no external calls (git/web), no
#: notifications, no scheduler, no cross-vault analysis beyond file content.
FILES_SCOPE_TOOLS: frozenset[str] = frozenset({
# Lecture / recherche de fichiers
"list_vaults", "list_directory", "list_all_files",
"read_file", "read_file_raw", "get_backlinks",
"get_graph", "search_fulltext", "search_advanced", "search_paths",
"list_tags", "suggest_tags", "list_recent",
"list_backups", "diff_backup", "restore_backup",
"find_duplicates",
# Mutations de fichiers
"create_file", "create_directory", "edit_file", "append_to_file",
"rename_file", "rename_directory", "move_path", "replace_in_files",
"delete_file", "delete_directory", "merge_duplicate_notes",
# Tableurs & documents (création/édition de fichiers du vault)
"create_xlsx", "create_docx", "create_csv", "create_pdf",
"list_xlsx_sheets", "xlsx_to_markdown", "search_workbook",
"analyze_range", "update_xlsx_cells", "append_xlsx_rows",
"edit_xlsx_structure",
})
@dataclass
class ToolContext:
"""Carries the caller identity and execution options for a tool call.
@@ -190,6 +213,32 @@ class ToolContext:
details={"vault": vault},
)
def require_tool_allowed(self, tool: str) -> None:
"""Raise :class:`ToolPermissionError` when the caller's token scope
forbids *tool* (#199).
A ``files``-scoped token is confined to :data:`FILES_SCOPE_TOOLS`
(vault file surface). Full-scope and scopeless callers (session
tokens, pre-#199 API keys) are unaffected ; an unknown scope fails
closed.
"""
token_scope = self.user.get("_token_scope") or "full"
if token_scope == "full":
return
if token_scope == "files":
if tool not in FILES_SCOPE_TOOLS:
raise ToolPermissionError(
f"Token à portée limitée : outil '{tool}' hors périmètre 'files'",
code="token_scope_denied",
details={"tool": tool, "scope": "files"},
)
return
raise ToolPermissionError(
"Token à portée inconnue : appel refusé",
code="token_scope_denied",
details={"tool": tool},
)
def resolve_safe_path(vault_root: Path, relative_path: str | None) -> Path:
"""Resolve a vault-relative path, rejecting traversal outside the vault.
+8
View File
@@ -216,6 +216,14 @@ def call_tool(
_audit(ctx, spec, arguments, ok=False, error=e.code)
raise
# #199 : les tokens à portée limitée sont confinés à la surface fichiers,
# y compris pour les outils sans vault (git, web, notify…).
try:
ctx.require_tool_allowed(name)
except ToolPermissionError as e:
_audit(ctx, spec, arguments, ok=False, error=e.code)
raise
if spec.risk == ToolRisk.DANGEROUS and vault and vault != "all":
try:
ctx.require_destructive_allowed(vault)
+11 -4
View File
@@ -123,10 +123,17 @@ def list_vaults(ctx: ToolContext, _params: ListVaultsInput) -> list[dict[str, An
def list_directory(ctx: ToolContext, params: ListDirectoryInput) -> list[dict[str, Any]]:
"""Return the entries of a vault directory (direct children only)."""
data = browse_directory(params.vault, params.path)
return [
{"name": item["name"], "path": item["path"], "type": item["type"]}
for item in data["items"]
]
# #197 : même contrat que la REST — la taille et le mtime permettent la
# réconciliation externe sans relire chaque fichier.
out = []
for item in data["items"]:
entry = {"name": item["name"], "path": item["path"], "type": item["type"]}
if item["type"] == "file":
entry["size"] = item.get("size")
entry["mtime"] = item.get("modified")
entry["mtime_iso"] = item.get("modified_iso")
out.append(entry)
return out
@tool(