feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理 - 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类) - 技能: md-to-office (pandoc + wkhtmltopdf) - 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/ - 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
97
coworker/teams/tokens.py
Normal file
97
coworker/teams/tokens.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Board join tokens — identity for external board clients.
|
||||
|
||||
A token binds an ACTOR and a ROLE server-side: an external harness (another agent
|
||||
CLI, a headless OpenWorker, the `ocw` CLI from a second machine) presents the token
|
||||
and the server resolves who it is — the client never states its own identity, and a
|
||||
worker token cannot claim to be the lead. Authority then falls to the store, same
|
||||
as for in-app agents: the token is identity, the store is the gate.
|
||||
|
||||
Storage is hash-only (sha256): the plaintext is shown once at mint and never
|
||||
persisted, so the registry file leaking doesn't leak the credentials. Revocation is
|
||||
per-token, keyed by the display prefix.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import secrets
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from .model import Actor, Role
|
||||
|
||||
_TOKEN_PREFIX = "owb_" # OpenWorker board — greppable in configs, meaningless to guess
|
||||
|
||||
|
||||
class BoardTokens:
|
||||
def __init__(self, path: str | Path) -> None:
|
||||
self.path = Path(path).expanduser()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def mint(self, actor: str, role: str = "worker", *, label: str = "") -> str:
|
||||
"""Create a token for one actor identity; returns the plaintext ONCE."""
|
||||
actor = (actor or "").strip()
|
||||
if not actor:
|
||||
raise ValueError("actor is required")
|
||||
Role(role) # validate early — a bad role should fail at mint, not at use
|
||||
token = _TOKEN_PREFIX + secrets.token_urlsafe(32)
|
||||
with self._lock:
|
||||
entries = self._load()
|
||||
entries[_digest(token)] = {
|
||||
"actor": actor,
|
||||
"role": role,
|
||||
"label": label,
|
||||
"prefix": token[:12],
|
||||
"created_ts": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
self._save(entries)
|
||||
return token
|
||||
|
||||
def resolve(self, token: str) -> Optional[Actor]:
|
||||
if not token:
|
||||
return None
|
||||
with self._lock:
|
||||
entry = self._load().get(_digest(token))
|
||||
if entry is None:
|
||||
return None
|
||||
return Actor(id=entry["actor"], role=Role(entry["role"]))
|
||||
|
||||
def entries(self) -> list[dict[str, Any]]:
|
||||
with self._lock:
|
||||
return sorted(self._load().values(), key=lambda e: e["created_ts"])
|
||||
|
||||
def revoke(self, prefix: str) -> int:
|
||||
"""Revoke every token whose display prefix matches; returns the count."""
|
||||
prefix = (prefix or "").strip()
|
||||
if not prefix:
|
||||
return 0
|
||||
with self._lock:
|
||||
entries = self._load()
|
||||
keep = {
|
||||
key: entry
|
||||
for key, entry in entries.items()
|
||||
if not entry["prefix"].startswith(prefix)
|
||||
}
|
||||
removed = len(entries) - len(keep)
|
||||
if removed:
|
||||
self._save(keep)
|
||||
return removed
|
||||
|
||||
def _load(self) -> dict[str, dict[str, Any]]:
|
||||
try:
|
||||
return json.loads(self.path.read_text())
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
def _save(self, entries: dict[str, dict[str, Any]]) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = self.path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(entries, indent=2))
|
||||
tmp.replace(self.path)
|
||||
|
||||
|
||||
def _digest(token: str) -> str:
|
||||
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
||||
Reference in New Issue
Block a user