mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
138 lines
6.1 KiB
Python
138 lines
6.1 KiB
Python
"""Lay down the backend's data dir before it boots, so the run is ready on the first tick.
|
|
|
|
Everything here is written pre-boot on purpose: the workflow store and the settings
|
|
store both load from disk once at startup, so seeding files is cheaper and more
|
|
deterministic than replaying create/PATCH calls over HTTP (no aux LLM naming call,
|
|
no schedule normalization, no chance of the container inventing a second workflow).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from typing import Any, Dict
|
|
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.dashboards.models import Dashboard
|
|
from backend.apps.settings.models import AppSettings
|
|
from runner.run_spec import CLOUD_RUN_DASHBOARD_ID, RunSpec
|
|
|
|
# 9Router provider id -> the AppSettings field the backend reads a raw key from.
|
|
API_KEY_SETTINGS_FIELD: Dict[str, str] = {
|
|
"anthropic": "anthropic_api_key",
|
|
"openai": "openai_api_key",
|
|
"gemini": "google_api_key",
|
|
"google": "google_api_key",
|
|
"openrouter": "openrouter_api_key",
|
|
}
|
|
|
|
# One synthetic id for every cloud run. Without it each ephemeral container mints a fresh uuid and analytics sees a brand-new "install" per run.
|
|
CLOUD_RUNNER_INSTALLATION_ID = "openswarm-cloud-runner"
|
|
|
|
# Told to the agent in as many words, because it cannot find this out any other way and the
|
|
# consequence of not knowing is a report written to a folder that is deleted minutes later.
|
|
DELIVERY_NOTE = (
|
|
"You are running in the OpenSwarm cloud on a throwaway machine. Your working directory is "
|
|
"the ONLY place that survives: every file you save there is delivered back to the user, and "
|
|
"everything else on this machine is destroyed the moment this run ends. So when a task asks "
|
|
"for a document, spreadsheet, image or archive, write it to a plainly named file in your "
|
|
"working directory rather than only pasting it into your reply. Do not write deliverables to "
|
|
"/tmp or to your home directory; they will not come back."
|
|
)
|
|
|
|
|
|
@typechecked
|
|
def p_write_json(path: str, payload: Any) -> None:
|
|
"""Atomic, owner-only write; these files hold API keys."""
|
|
directory = os.path.dirname(path)
|
|
os.makedirs(directory, mode=0o700, exist_ok=True)
|
|
handle, temp_path = tempfile.mkstemp(dir=directory, prefix=".seed-", suffix=".json")
|
|
try:
|
|
with os.fdopen(handle, "w", encoding="utf-8") as stream:
|
|
json.dump(payload, stream, indent=2)
|
|
os.chmod(temp_path, 0o600)
|
|
os.replace(temp_path, path)
|
|
except BaseException:
|
|
if os.path.exists(temp_path):
|
|
os.unlink(temp_path)
|
|
raise
|
|
|
|
|
|
@typechecked
|
|
def unavailable_apps_note(spec: RunSpec) -> str:
|
|
"""Name the user's connected apps this run cannot reach, so silence is not mistaken for absence.
|
|
|
|
Their MCP credentials never leave the laptop, so the servers are not here and never will be
|
|
mid-run. Without this sentence the agent has no way to know the app exists, and "update my
|
|
Notion" comes back as a confident paragraph about Notion rather than an admission.
|
|
"""
|
|
names = [server.name for server in spec.unavailable_mcp_servers]
|
|
if not names:
|
|
return ""
|
|
return (
|
|
"These apps are connected on the user's own computer but NOT reachable from this cloud "
|
|
f"run, because their sign-in details stay on that computer: {', '.join(sorted(names))}. "
|
|
"If a task needs one of them, say plainly that it cannot be done from a cloud run and "
|
|
"that it has to run on their machine. Never guess at, invent, or describe from memory "
|
|
"what one of those apps contains."
|
|
)
|
|
|
|
|
|
@typechecked
|
|
def settings_for_run(spec: RunSpec, workspace: str) -> AppSettings:
|
|
"""The AppSettings a cloud run needs: this workflow's model, this run's keys, no telemetry.
|
|
|
|
`default_folder` is what makes the run's files findable afterwards. Left unset, the agent
|
|
falls back to $HOME and the launcher reroutes it into a per-session scratch directory whose
|
|
name nothing outside the backend can predict, so the harvest would have nowhere to look.
|
|
"""
|
|
settings = AppSettings()
|
|
settings.default_model = spec.workflow.model
|
|
settings.connection_mode = "own_key"
|
|
settings.analytics_opt_in = False
|
|
settings.installation_id = CLOUD_RUNNER_INSTALLATION_ID
|
|
settings.default_folder = workspace
|
|
additions = [DELIVERY_NOTE, unavailable_apps_note(spec)]
|
|
settings.default_system_prompt = "\n\n".join(
|
|
part for part in [settings.default_system_prompt or "", *additions] if part
|
|
).strip()
|
|
for credential in spec.credentials:
|
|
if credential.auth_type != "api_key":
|
|
continue
|
|
field = API_KEY_SETTINGS_FIELD.get(credential.provider)
|
|
if field is None:
|
|
raise ValueError(
|
|
f"no settings field for api_key provider {credential.provider!r}; "
|
|
f"supported: {', '.join(sorted(set(API_KEY_SETTINGS_FIELD)))}"
|
|
)
|
|
setattr(settings, field, credential.api_key)
|
|
return settings
|
|
|
|
|
|
@typechecked
|
|
def seed_data_root(data_root: str, workspace: str, spec: RunSpec) -> None:
|
|
"""Write the workflow, settings and dashboard records the backend will read at boot.
|
|
|
|
The dashboard exists so the Electron window has somewhere to land and browser cards have
|
|
somewhere to render. Writing it here rather than letting the backend's first-boot migration
|
|
invent one keeps its id knowable before anything has started.
|
|
|
|
The workspace sits OUTSIDE the data root deliberately: it is the agent's own folder, and a
|
|
Glob or Grep run inside it should not sweep up the settings file its API keys live in.
|
|
"""
|
|
os.makedirs(workspace, mode=0o700, exist_ok=True)
|
|
workflow = spec.workflow_for_disk()
|
|
p_write_json(
|
|
os.path.join(data_root, "workflows", f"{workflow.id}.json"),
|
|
workflow.model_dump(mode="json"),
|
|
)
|
|
p_write_json(
|
|
os.path.join(data_root, "settings", "settings.json"),
|
|
settings_for_run(spec, workspace).model_dump(mode="json"),
|
|
)
|
|
dashboard = Dashboard(id=CLOUD_RUN_DASHBOARD_ID, name=spec.workflow.title or "Cloud run")
|
|
p_write_json(
|
|
os.path.join(data_root, "dashboards", f"{dashboard.id}.json"),
|
|
dashboard.model_dump(mode="json"),
|
|
)
|