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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ include = ["leapflow*"]
"leapflow.gateway.action_packs" = ["*.yaml"]
"leapflow.dashboard.templates" = ["*.yaml"]
"leapflow.dashboard.static" = ["*"]
"leapflow.plugins.dsh" = ["*.js"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
29 changes: 29 additions & 0 deletions src/leapflow/cli/commands/slash_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,22 @@ async def build_plugin_payload(ctx: "Context", args: str) -> dict[str, Any]:
"generation": fiber.generation if fiber else None,
},
}
descriptor = getattr(plugin, "descriptor", None)
if descriptor is not None and hasattr(descriptor, "to_dict"):
descriptor_data = descriptor.to_dict()
response["dsh"] = {
"source_kind": descriptor_data.get("source_kind"),
"bundle_sha256": descriptor_data.get("bundle_sha256"),
"entry_point": descriptor_data.get("entry_point"),
"verdict": (
"partial"
if descriptor_data.get("client_components")
else "adaptable"
),
"limitations": descriptor_data.get("limitations", []),
"client_components": descriptor_data.get("client_components", []),
"runtime": "node",
}
# Additive trust info
try:
from leapflow.learning.plugin_advisor import get_default_advisor
Expand Down Expand Up @@ -1679,6 +1695,19 @@ def render_plugin_payload(console: "LeapConsole", payload: dict[str, Any]) -> No
deps = payload.get("dependencies") or []
if deps:
info.append(f"Deps: {', '.join(deps)}\n")
dsh = payload.get("dsh") or {}
if dsh:
info.append(f"Runtime: {dsh.get('runtime', 'node')}\n")
info.append(f"Source: {dsh.get('source_kind', 'unknown')}\n")
info.append(f"Verdict: {dsh.get('verdict', 'adaptable')}\n")
for limitation in dsh.get("limitations") or []:
info.append(f"Limitation: {limitation}\n")
for component in dsh.get("client_components") or []:
info.append(
f"Client: {component.get('name', 'client')} "
f"({component.get('status', 'unsupported')}) — "
f"{component.get('reason', '')}\n"
)
tools = payload.get("tools") or []
if tools:
info.append(f"Tools ({len(tools)}):")
Expand Down
18 changes: 18 additions & 0 deletions src/leapflow/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ class Settings:
# When non-empty, marketplace installs MUST carry a valid signature from
# one of these keys; empty tuple -> checksum-only integrity verification.
plugin_marketplace_trusted_pubkeys: tuple[str, ...] = ()
# Restricted DeepSeek Harness / Cordis bridge runtime. These are cold-path
# process limits; changing them requires rebuilding daemon-owned plugin
# wrappers and therefore takes effect after daemon restart.
plugins_dsh_invoke_timeout_s: float = 30.0
plugins_dsh_discovery_timeout_s: float = 10.0
plugins_dsh_max_message_bytes: int = 1_000_000
plugins_dsh_max_stderr_bytes: int = 64_000
plugins_dsh_max_memory_mb: int = 128
runtime_dir: Path = field(default_factory=lambda: _bootstrap_profile_layout().runtime_dir)

# Audit
Expand Down Expand Up @@ -921,6 +929,11 @@ def _build_settings_from_env(
tools_lint_command = os.getenv("LEAPFLOW_TOOLS_LINT_COMMAND", "").strip()
tools_terminal_session_enabled = os.getenv("LEAPFLOW_TOOLS_TERMINAL_SESSION_ENABLED", "1").strip().lower() in ("1", "true", "yes")
tools_verify_edits = os.getenv("LEAPFLOW_TOOLS_VERIFY_EDITS", "1").strip().lower() in ("1", "true", "yes")
plugins_dsh_invoke_timeout_s = float(os.getenv("LEAPFLOW_PLUGINS_DSH_INVOKE_TIMEOUT_S", "30"))
plugins_dsh_discovery_timeout_s = float(os.getenv("LEAPFLOW_PLUGINS_DSH_DISCOVERY_TIMEOUT_S", "10"))
plugins_dsh_max_message_bytes = int(os.getenv("LEAPFLOW_PLUGINS_DSH_MAX_MESSAGE_BYTES", "1000000"))
plugins_dsh_max_stderr_bytes = int(os.getenv("LEAPFLOW_PLUGINS_DSH_MAX_STDERR_BYTES", "64000"))
plugins_dsh_max_memory_mb = int(os.getenv("LEAPFLOW_PLUGINS_DSH_MAX_MEMORY_MB", "128"))
web_transport = os.getenv("LEAPFLOW_WEB_TRANSPORT", "auto").strip().lower() or "auto"
web_timeout_s = float(os.getenv("LEAPFLOW_WEB_TIMEOUT_S", "20"))
web_max_bytes = int(os.getenv("LEAPFLOW_WEB_MAX_BYTES", "2000000"))
Expand Down Expand Up @@ -1277,6 +1290,11 @@ def _tuple_env(key: str, default: tuple) -> tuple:
tools_lint_command=tools_lint_command,
tools_terminal_session_enabled=tools_terminal_session_enabled,
tools_verify_edits=tools_verify_edits,
plugins_dsh_invoke_timeout_s=plugins_dsh_invoke_timeout_s,
plugins_dsh_discovery_timeout_s=plugins_dsh_discovery_timeout_s,
plugins_dsh_max_message_bytes=plugins_dsh_max_message_bytes,
plugins_dsh_max_stderr_bytes=plugins_dsh_max_stderr_bytes,
plugins_dsh_max_memory_mb=plugins_dsh_max_memory_mb,
web_transport=web_transport,
web_timeout_s=web_timeout_s,
web_max_bytes=web_max_bytes,
Expand Down
8 changes: 7 additions & 1 deletion src/leapflow/config_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ class ConfigSnapshot:
"signal.noise_path_fragments": "Path fragments treated as monitor/display noise for fs.change events, e.g. OS caches and tool state directories.",
"signal.noise_dir_names": "Directory names treated as monitor/display noise for fs.change events.",
"signal.noise_suffixes": "Filename suffixes treated as transient fs.change noise, e.g. WAL/SHM/journal/temp/log files.",
"plugins.dsh_invoke_timeout_s": "Maximum seconds for one restricted DSH tool invocation; requires daemon restart.",
"plugins.dsh_discovery_timeout_s": "Maximum seconds for restricted DSH runtime discovery; requires daemon restart.",
"plugins.dsh_max_message_bytes": "Maximum bytes in one DSH worker NDJSON protocol message; requires daemon restart.",
"plugins.dsh_max_stderr_bytes": "Bounded diagnostic stderr tail retained from one DSH worker; requires daemon restart.",
"plugins.dsh_max_memory_mb": "V8 old-space ceiling in megabytes for each DSH worker process; requires daemon restart.",
}

_SECTION_CATEGORIES = {
Expand All @@ -197,6 +202,7 @@ class ConfigSnapshot:
"attention": "Perception",
"recording": "Recording",
"video": "Recording",
"plugins": "Plugins",
"learnability": "Learning",
"learn": "Learning",
"skill": "Skills",
Expand Down Expand Up @@ -241,7 +247,7 @@ class ConfigSnapshot:
}

_PARTIAL_RELOAD_SECTIONS = frozenset({"runtime", "mock", "gateway", "hub", "scheduler", "observer", "cua", "use", "dashboard"})
_RESTART_REQUIRED_SECTIONS = frozenset({"daemon"})
_RESTART_REQUIRED_SECTIONS = frozenset({"daemon", "plugins"})

_PROFILE_FILE_BY_SECTION = {
"llm": "llm.yaml",
Expand Down
6 changes: 6 additions & 0 deletions src/leapflow/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ def plugins_dir(self) -> Path:
# means installed plugins are per-profile and never mutate site-packages.
return self.root / "plugins"

@property
def dsh_plugins_dir(self) -> Path:
"""Profile-owned source bundles for restricted DSH bridge plugins."""
return self.plugins_dir / "dsh"

@property
def plugin_proposals_path(self) -> Path:
# Profile-scoped review queue for capability-gap → plugin proposals.
Expand Down Expand Up @@ -467,6 +472,7 @@ def ensure(self) -> None:
self.global_memory_dir,
self.skills_dir,
self.plugins_dir,
self.dsh_plugins_dir,
self.audit_dir,
self.history_dir,
self.runtime_dir,
Expand Down
25 changes: 24 additions & 1 deletion src/leapflow/learning/compatibility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,31 @@
from leapflow.learning.compatibility.pipeline import assess_plugin
from leapflow.learning.compatibility.protocol import (
CompatibilityReport,
ComponentCompatibility,
ComponentKind,
ComponentStatus,
ExecutionPlan,
PluginManifestInput,
PluginSourceKind,
Verdict,
)
from leapflow.learning.compatibility.source_inspector import (
SourceInspection,
SourceInspectionError,
inspect_plugin_source,
)

__all__ = ["assess_plugin", "CompatibilityReport", "Verdict", "PluginManifestInput"]
__all__ = [
"assess_plugin",
"CompatibilityReport",
"ComponentCompatibility",
"ComponentKind",
"ComponentStatus",
"ExecutionPlan",
"PluginManifestInput",
"PluginSourceKind",
"SourceInspection",
"SourceInspectionError",
"Verdict",
"inspect_plugin_source",
]
Loading
Loading