feat: session bundle import/export to skip Playwright auto-login
Adds a lightweight way to pre-seed a Lingma workDir with an existing logged-in session: - New module session_bundle.py packs/unpacks only the four cache files that make up a Lingma login (id, user, quota, config.json). Everything else (db, logs, index, diagnosis) stays local so bundles stay tiny and never leak session-specific artefacts. - Safety: path-traversal/symlink members are rejected; size is capped; refuses to export from a workDir that isn't actually logged in; sensitive cache/user is chmod'd 0600 on restore. - LingmaAccount gains optional session_bundle_b64 / session_bundle_file; LINGMA_SESSION_BUNDLE[_FILE] env provide the singleton fallback. Credentials become optional when a bundle is supplied. - LingmaPool.start() restores the bundle into each instance workDir only if it isn't already logged in, so persistent volumes aren't clobbered and a corrupt bundle falls back to Playwright gracefully. - POST /internal/session/export returns the bundle as base64; ?instance= selects a specific pool instance. Requires an authed, already-logged-in instance to prevent exporting empties. - README + .env.example document the end-to-end flow. Made-with: Cursor
This commit is contained in:
@@ -9,6 +9,11 @@ from dataclasses import dataclass, field
|
||||
class LingmaAccount:
|
||||
username: str
|
||||
password: str
|
||||
# Optional: pre-captured Lingma session to skip Playwright auto-login.
|
||||
# Either inline base64 of a tar.gz bundle, or a path on disk holding the
|
||||
# same. Inline wins if both are set.
|
||||
session_bundle_b64: str = ""
|
||||
session_bundle_file: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -72,8 +77,19 @@ def _parse_accounts(raw: str) -> list[LingmaAccount]:
|
||||
if isinstance(item, dict):
|
||||
u = str(item.get("username", "")).strip()
|
||||
p = str(item.get("password", "")).strip()
|
||||
if u and p:
|
||||
out.append(LingmaAccount(u, p))
|
||||
bundle = str(item.get("session_bundle", "")).strip()
|
||||
bundle_file = str(item.get("session_bundle_file", "")).strip()
|
||||
# Username/password become optional when a bundle is supplied:
|
||||
# Playwright login is only needed if there's no pre-captured session.
|
||||
if (u and p) or bundle or bundle_file:
|
||||
out.append(
|
||||
LingmaAccount(
|
||||
username=u,
|
||||
password=p,
|
||||
session_bundle_b64=bundle,
|
||||
session_bundle_file=bundle_file,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
out: list[LingmaAccount] = []
|
||||
@@ -97,11 +113,29 @@ def load_settings() -> Settings:
|
||||
)
|
||||
|
||||
accounts = _parse_accounts(os.getenv("LINGMA_ACCOUNTS", ""))
|
||||
|
||||
# LINGMA_SESSION_BUNDLE / LINGMA_SESSION_BUNDLE_FILE are singleton envs
|
||||
# that attach a session to the first account (or implicitly create one
|
||||
# when neither LINGMA_ACCOUNTS nor LINGMA_USERNAME is provided -- common
|
||||
# "I just want to skip Playwright" case).
|
||||
fallback_bundle = os.getenv("LINGMA_SESSION_BUNDLE", "").strip()
|
||||
fallback_bundle_file = os.getenv("LINGMA_SESSION_BUNDLE_FILE", "").strip()
|
||||
|
||||
if not accounts:
|
||||
u = os.getenv("LINGMA_USERNAME", "").strip()
|
||||
p = os.getenv("LINGMA_PASSWORD", "").strip()
|
||||
if u and p:
|
||||
accounts.append(LingmaAccount(u, p))
|
||||
elif fallback_bundle or fallback_bundle_file:
|
||||
# Bundle-only login: no creds needed.
|
||||
accounts.append(LingmaAccount(username="", password=""))
|
||||
|
||||
if accounts and (fallback_bundle or fallback_bundle_file):
|
||||
# Only fill on account[0] if it doesn't already carry one (accounts
|
||||
# loaded from LINGMA_ACCOUNTS JSON may have per-entry bundles).
|
||||
if not accounts[0].session_bundle_b64 and not accounts[0].session_bundle_file:
|
||||
accounts[0].session_bundle_b64 = fallback_bundle
|
||||
accounts[0].session_bundle_file = fallback_bundle_file
|
||||
|
||||
explicit_count = os.getenv("LINGMA_INSTANCE_COUNT", "").strip()
|
||||
if explicit_count:
|
||||
|
||||
Reference in New Issue
Block a user