mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 09:28:34 +00:00
feat(extensions): load published OpenClaw runtimes
This commit is contained in:
parent
76d7a33b3b
commit
012c7ce034
@ -11,7 +11,9 @@ from typing import Any
|
|||||||
from nanobot.extensions.codec import MANIFEST_FILENAME, load_manifest
|
from nanobot.extensions.codec import MANIFEST_FILENAME, load_manifest
|
||||||
from nanobot.extensions.manifest import (
|
from nanobot.extensions.manifest import (
|
||||||
ContributionKind,
|
ContributionKind,
|
||||||
|
DependencyKind,
|
||||||
ExtensionContribution,
|
ExtensionContribution,
|
||||||
|
ExtensionDependency,
|
||||||
ExtensionManifest,
|
ExtensionManifest,
|
||||||
ExtensionRuntime,
|
ExtensionRuntime,
|
||||||
)
|
)
|
||||||
@ -72,7 +74,10 @@ def _adapt_pi(package: dict[str, Any]) -> AdaptedPackage:
|
|||||||
|
|
||||||
def _adapt_openclaw(root: Path, package: dict[str, Any]) -> AdaptedPackage:
|
def _adapt_openclaw(root: Path, package: dict[str, Any]) -> AdaptedPackage:
|
||||||
openclaw = package["openclaw"]
|
openclaw = package["openclaw"]
|
||||||
entries = _string_list(openclaw.get("extensions"), "openclaw.extensions")
|
entries = _optional_string_list(
|
||||||
|
openclaw.get("runtimeExtensions"),
|
||||||
|
"openclaw.runtimeExtensions",
|
||||||
|
) or _string_list(openclaw.get("extensions"), "openclaw.extensions")
|
||||||
plugin_path = root / "openclaw.plugin.json"
|
plugin_path = root / "openclaw.plugin.json"
|
||||||
plugin = _read_json(plugin_path, "openclaw.plugin.json") if plugin_path.is_file() else {}
|
plugin = _read_json(plugin_path, "openclaw.plugin.json") if plugin_path.is_file() else {}
|
||||||
plugin_id = str(plugin.get("id") or package.get("name") or "openclaw-plugin")
|
plugin_id = str(plugin.get("id") or package.get("name") or "openclaw-plugin")
|
||||||
@ -97,6 +102,7 @@ def _adapt_openclaw(root: Path, package: dict[str, Any]) -> AdaptedPackage:
|
|||||||
runtime=ExtensionRuntime.OPENCLAW,
|
runtime=ExtensionRuntime.OPENCLAW,
|
||||||
entries=tuple(entries),
|
entries=tuple(entries),
|
||||||
contributions=contributions,
|
contributions=contributions,
|
||||||
|
dependencies=_openclaw_dependencies(package, openclaw),
|
||||||
description=str(
|
description=str(
|
||||||
plugin.get("description") or package.get("description") or ""
|
plugin.get("description") or package.get("description") or ""
|
||||||
),
|
),
|
||||||
@ -108,6 +114,23 @@ def _adapt_openclaw(root: Path, package: dict[str, Any]) -> AdaptedPackage:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _openclaw_dependencies(
|
||||||
|
package: dict[str, Any],
|
||||||
|
openclaw: dict[str, Any],
|
||||||
|
) -> tuple[ExtensionDependency, ...]:
|
||||||
|
build = openclaw.get("build")
|
||||||
|
version = build.get("openclawVersion") if isinstance(build, dict) else None
|
||||||
|
peers = package.get("peerDependencies")
|
||||||
|
peer_version = peers.get("openclaw") if isinstance(peers, dict) else None
|
||||||
|
return (
|
||||||
|
ExtensionDependency(
|
||||||
|
kind=DependencyKind.NPM,
|
||||||
|
name="openclaw",
|
||||||
|
specifier=str(version or peer_version or "latest"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _openclaw_contributions(
|
def _openclaw_contributions(
|
||||||
plugin: dict[str, Any],
|
plugin: dict[str, Any],
|
||||||
) -> tuple[ExtensionContribution, ...]:
|
) -> tuple[ExtensionContribution, ...]:
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from nanobot.extensions.discovery import (
|
|||||||
ExtensionDiscoveryResult,
|
ExtensionDiscoveryResult,
|
||||||
discover_manifest_root,
|
discover_manifest_root,
|
||||||
)
|
)
|
||||||
|
from nanobot.extensions.manifest import DependencyKind, ExtensionManifest
|
||||||
from nanobot.extensions.package_adapter import AdaptedPackage, adapt_package
|
from nanobot.extensions.package_adapter import AdaptedPackage, adapt_package
|
||||||
from nanobot.extensions.registry import ExtensionScope
|
from nanobot.extensions.registry import ExtensionScope
|
||||||
|
|
||||||
@ -216,7 +217,7 @@ class ExtensionStore:
|
|||||||
)
|
)
|
||||||
if package.generated:
|
if package.generated:
|
||||||
dump_manifest(package.manifest, staging / MANIFEST_FILENAME)
|
dump_manifest(package.manifest, staging / MANIFEST_FILENAME)
|
||||||
_install_node_dependencies(staging)
|
_install_node_dependencies(staging, package.manifest)
|
||||||
integrity = _tree_hash(staging)
|
integrity = _tree_hash(staging)
|
||||||
if target.exists():
|
if target.exists():
|
||||||
target.rename(backup)
|
target.rename(backup)
|
||||||
@ -285,25 +286,46 @@ _DEFAULT_RECORD = InstalledExtension(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _install_node_dependencies(root: Path) -> None:
|
def _install_node_dependencies(root: Path, manifest: ExtensionManifest) -> None:
|
||||||
package_path = root / "package.json"
|
package_path = root / "package.json"
|
||||||
if not package_path.is_file():
|
if not package_path.is_file():
|
||||||
return
|
return
|
||||||
package = json.loads(package_path.read_text(encoding="utf-8"))
|
package = json.loads(package_path.read_text(encoding="utf-8"))
|
||||||
dependencies = package.get("dependencies") if isinstance(package, dict) else None
|
dependencies = package.get("dependencies") if isinstance(package, dict) else None
|
||||||
if not dependencies:
|
if dependencies:
|
||||||
return
|
_run(
|
||||||
_run(
|
[
|
||||||
[
|
"npm",
|
||||||
"npm",
|
"install",
|
||||||
"install",
|
"--omit=dev",
|
||||||
"--omit=dev",
|
"--ignore-scripts",
|
||||||
"--ignore-scripts",
|
"--no-audit",
|
||||||
"--no-audit",
|
"--no-fund",
|
||||||
"--no-fund",
|
],
|
||||||
],
|
cwd=root,
|
||||||
cwd=root,
|
)
|
||||||
)
|
for dependency in manifest.dependencies:
|
||||||
|
if dependency.kind is not DependencyKind.NPM or dependency.optional:
|
||||||
|
continue
|
||||||
|
spec = (
|
||||||
|
f"{dependency.name}@{dependency.specifier}"
|
||||||
|
if dependency.specifier
|
||||||
|
else dependency.name
|
||||||
|
)
|
||||||
|
_run(
|
||||||
|
[
|
||||||
|
"npm",
|
||||||
|
"install",
|
||||||
|
"--save-prod",
|
||||||
|
"--save-exact",
|
||||||
|
"--ignore-scripts",
|
||||||
|
"--no-audit",
|
||||||
|
"--no-fund",
|
||||||
|
"--",
|
||||||
|
spec,
|
||||||
|
],
|
||||||
|
cwd=root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _run(command: list[str], *, cwd: Path | None = None) -> str:
|
def _run(command: list[str], *, cwd: Path | None = None) -> str:
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from nanobot.extensions import (
|
from nanobot.extensions import (
|
||||||
ContributionKind,
|
ContributionKind,
|
||||||
|
DependencyKind,
|
||||||
ExtensionRuntime,
|
ExtensionRuntime,
|
||||||
adapt_package,
|
adapt_package,
|
||||||
)
|
)
|
||||||
@ -34,7 +35,11 @@ def test_adapts_openclaw_contracts_without_loading_code(tmp_path: Path) -> None:
|
|||||||
{
|
{
|
||||||
"name": "@openclaw/search-plugin",
|
"name": "@openclaw/search-plugin",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"openclaw": {"extensions": ["./index.ts"]},
|
"peerDependencies": {"openclaw": ">=2.0.0"},
|
||||||
|
"openclaw": {
|
||||||
|
"extensions": ["./index.ts"],
|
||||||
|
"runtimeExtensions": ["./dist/index.js"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -55,6 +60,9 @@ def test_adapts_openclaw_contracts_without_loading_code(tmp_path: Path) -> None:
|
|||||||
result = adapt_package(tmp_path)
|
result = adapt_package(tmp_path)
|
||||||
|
|
||||||
assert result.manifest.id == "openclaw.search"
|
assert result.manifest.id == "openclaw.search"
|
||||||
|
assert result.manifest.activation_entries == ("./dist/index.js",)
|
||||||
|
assert result.manifest.dependencies[0].kind is DependencyKind.NPM
|
||||||
|
assert result.manifest.dependencies[0].specifier == ">=2.0.0"
|
||||||
assert {
|
assert {
|
||||||
(item.kind, item.name) for item in result.manifest.contributions
|
(item.kind, item.name) for item in result.manifest.contributions
|
||||||
} == {
|
} == {
|
||||||
|
|||||||
@ -3,8 +3,13 @@ from pathlib import Path
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from nanobot.extensions import (
|
from nanobot.extensions import (
|
||||||
|
DependencyKind,
|
||||||
|
ExtensionDependency,
|
||||||
|
ExtensionManifest,
|
||||||
|
ExtensionRuntime,
|
||||||
ExtensionSourceKind,
|
ExtensionSourceKind,
|
||||||
ExtensionStore,
|
ExtensionStore,
|
||||||
|
dump_manifest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -93,3 +98,35 @@ def test_store_restores_previous_package_when_registry_write_fails(
|
|||||||
|
|
||||||
restored = json.loads(installed_package.read_text())
|
restored = json.loads(installed_package.read_text())
|
||||||
assert restored["version"] == "1.0.0"
|
assert restored["version"] == "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_installs_declared_npm_runtime_dependency(tmp_path: Path) -> None:
|
||||||
|
source = tmp_path / "source"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "index.mjs").write_text("export default function () {}")
|
||||||
|
(source / "package.json").write_text("{}")
|
||||||
|
dump_manifest(
|
||||||
|
ExtensionManifest(
|
||||||
|
id="openclaw.test",
|
||||||
|
name="OpenClaw test",
|
||||||
|
version="1.0.0",
|
||||||
|
runtime=ExtensionRuntime.OPENCLAW,
|
||||||
|
entry="./index.mjs",
|
||||||
|
dependencies=(
|
||||||
|
ExtensionDependency(
|
||||||
|
kind=DependencyKind.NPM,
|
||||||
|
name="openclaw",
|
||||||
|
specifier="2026.7.1",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
source / "nanobot.extension.json",
|
||||||
|
)
|
||||||
|
store = ExtensionStore(tmp_path / "extensions")
|
||||||
|
|
||||||
|
with patch("nanobot.extensions.store._run") as run:
|
||||||
|
store.install_local(source)
|
||||||
|
|
||||||
|
command = run.call_args.args[0]
|
||||||
|
assert "--save-prod" in command
|
||||||
|
assert "openclaw@2026.7.1" in command
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user