xcorecli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xcli/__init__.py +0 -0
- xcli/_run.py +14 -0
- xcli/_xcore.py +56 -0
- xcli/init/__init__.py +0 -0
- xcli/init/cli.py +11 -0
- xcli/init/manager.py +141 -0
- xcli/init/upgrade.py +198 -0
- xcli/main.py +64 -0
- xcli/manager/__init__.py +0 -0
- xcli/manager/cli.py +537 -0
- xcli/marketplace/__init__.py +0 -0
- xcli/marketplace/cli.py +44 -0
- xcli/migrations/__init__.py +3 -0
- xcli/migrations/cli.py +292 -0
- xcli/migrations/runtime.py +198 -0
- xcli/plugin/__init__.py +0 -0
- xcli/plugin/cli.py +538 -0
- xcli/plugin/scaffold.py +213 -0
- xcli/sandbox/__init__.py +0 -0
- xcli/sandbox/cli.py +124 -0
- xcli/worker/__init__.py +0 -0
- xcli/worker/cli.py +569 -0
- xcli/worker/worker.py +565 -0
- xcorecli-1.0.0.dist-info/METADATA +11 -0
- xcorecli-1.0.0.dist-info/RECORD +27 -0
- xcorecli-1.0.0.dist-info/WHEEL +4 -0
- xcorecli-1.0.0.dist-info/entry_points.txt +3 -0
xcli/plugin/scaffold.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
# ── Templates src/main.py ─────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
_TRUSTED_TEMPLATE = '''\
|
|
8
|
+
from xcore.kernel.api.contract import TrustedBase, error, ok
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Plugin(TrustedBase):
|
|
12
|
+
"""
|
|
13
|
+
{description}
|
|
14
|
+
Execution mode : trusted — full service injection via self.get_service().
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
async def on_load(self) -> None:
|
|
18
|
+
# Access services here, e.g.:
|
|
19
|
+
# self.db = self.get_service("db")
|
|
20
|
+
# self.cache = self.get_service("cache")
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
async def on_reload(self) -> None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
async def on_unload(self) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
async def handle(self, action: str, payload: dict) -> dict:
|
|
30
|
+
match action:
|
|
31
|
+
case "ping":
|
|
32
|
+
return ok(message="pong")
|
|
33
|
+
case _:
|
|
34
|
+
return error(f"unknown action: {{action!r}}")
|
|
35
|
+
|
|
36
|
+
def get_router(self):
|
|
37
|
+
"""Expose custom HTTP routes under /plugin/{name}/."""
|
|
38
|
+
from fastapi import APIRouter
|
|
39
|
+
|
|
40
|
+
router = APIRouter(prefix="/", tags=["{name}"])
|
|
41
|
+
|
|
42
|
+
@router.get("/")
|
|
43
|
+
async def index():
|
|
44
|
+
return {{"plugin": "{name}", "status": "ok"}}
|
|
45
|
+
|
|
46
|
+
return router
|
|
47
|
+
'''
|
|
48
|
+
|
|
49
|
+
_SANDBOXED_TEMPLATE = '''\
|
|
50
|
+
from xcore.kernel.api.contract import error, ok
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Plugin:
|
|
54
|
+
"""
|
|
55
|
+
{description}
|
|
56
|
+
Execution mode : sandboxed — runs in an isolated subprocess.
|
|
57
|
+
No direct service injection; use IPC if needed.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
_config: dict = {{}}
|
|
61
|
+
|
|
62
|
+
async def handle(self, action: str, payload: dict) -> dict:
|
|
63
|
+
match action:
|
|
64
|
+
case "ping":
|
|
65
|
+
return ok(message="pong")
|
|
66
|
+
case _:
|
|
67
|
+
return error(f"unknown action: {{action!r}}")
|
|
68
|
+
'''
|
|
69
|
+
|
|
70
|
+
_LEGACY_TEMPLATE = '''\
|
|
71
|
+
from xcore.kernel.api.contract import error, ok
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Plugin:
|
|
75
|
+
"""
|
|
76
|
+
{description}
|
|
77
|
+
Execution mode : legacy — duck-typed, minimal lifecycle hooks.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_config: dict = {{}}
|
|
81
|
+
|
|
82
|
+
async def on_init(self) -> None:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
async def on_start(self) -> None:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
async def on_stop(self) -> None:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
async def handle(self, action: str, payload: dict) -> dict:
|
|
92
|
+
match action:
|
|
93
|
+
case "ping":
|
|
94
|
+
return ok(message="pong")
|
|
95
|
+
case _:
|
|
96
|
+
return error(f"unknown action: {{action!r}}")
|
|
97
|
+
'''
|
|
98
|
+
|
|
99
|
+
_TEMPLATES = {
|
|
100
|
+
"trusted": _TRUSTED_TEMPLATE,
|
|
101
|
+
"sandboxed": _SANDBOXED_TEMPLATE,
|
|
102
|
+
"legacy": _LEGACY_TEMPLATE,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Manifest builder ──────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def _build_manifest(cfg: dict) -> dict:
|
|
109
|
+
manifest: dict = {
|
|
110
|
+
"name": cfg["name"],
|
|
111
|
+
"version": cfg["version"],
|
|
112
|
+
"author": cfg["author"],
|
|
113
|
+
"description": cfg["description"],
|
|
114
|
+
"framework_version": cfg["framework_version"],
|
|
115
|
+
"execution_mode": cfg["execution_mode"],
|
|
116
|
+
"entry_point": cfg["entry_point"],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if cfg.get("allowed_imports"):
|
|
120
|
+
manifest["allowed_imports"] = cfg["allowed_imports"]
|
|
121
|
+
|
|
122
|
+
if cfg.get("requires"):
|
|
123
|
+
manifest["requires"] = cfg["requires"]
|
|
124
|
+
|
|
125
|
+
if cfg.get("env"):
|
|
126
|
+
manifest["env"] = cfg["env"]
|
|
127
|
+
manifest["envconfiguration"] = {"inject": True, "env_file": ".env"}
|
|
128
|
+
|
|
129
|
+
resources: dict = {}
|
|
130
|
+
if cfg.get("timeout_seconds", 10) != 10:
|
|
131
|
+
resources["timeout_seconds"] = cfg["timeout_seconds"]
|
|
132
|
+
if cfg.get("max_memory_mb", 128) != 128:
|
|
133
|
+
resources["max_memory_mb"] = cfg["max_memory_mb"]
|
|
134
|
+
if cfg.get("max_disk_mb", 50) != 50:
|
|
135
|
+
resources["max_disk_mb"] = cfg["max_disk_mb"]
|
|
136
|
+
rl_calls = cfg.get("rate_limit_calls", 100)
|
|
137
|
+
rl_period = cfg.get("rate_limit_period", 60)
|
|
138
|
+
if rl_calls != 100 or rl_period != 60:
|
|
139
|
+
resources["rate_limit"] = {"calls": rl_calls, "period_seconds": rl_period}
|
|
140
|
+
if resources:
|
|
141
|
+
manifest["resources"] = resources
|
|
142
|
+
|
|
143
|
+
runtime: dict = {}
|
|
144
|
+
if not cfg.get("healthcheck_enabled", True):
|
|
145
|
+
runtime["health_check"] = {"enabled": False}
|
|
146
|
+
if cfg.get("retry_attempts", 1) > 1:
|
|
147
|
+
runtime.setdefault("retry", {})["max_attempts"] = cfg["retry_attempts"]
|
|
148
|
+
if runtime:
|
|
149
|
+
manifest["runtime"] = runtime
|
|
150
|
+
|
|
151
|
+
allowed_paths = cfg.get("filesystem_allowed", ["data/"])
|
|
152
|
+
denied_paths = cfg.get("filesystem_denied", ["src/"])
|
|
153
|
+
if allowed_paths != ["data/"] or denied_paths != ["src/"]:
|
|
154
|
+
manifest["filesystem"] = {
|
|
155
|
+
"allowed_paths": allowed_paths,
|
|
156
|
+
"denied_paths": denied_paths,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if cfg.get("permissions"):
|
|
160
|
+
manifest["permissions"] = cfg["permissions"]
|
|
161
|
+
|
|
162
|
+
return manifest
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── File writer ───────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def scaffold(cfg: dict, target_dir: Path) -> list[Path]:
|
|
168
|
+
"""Generate the plugin directory structure and return created paths."""
|
|
169
|
+
cfg = {
|
|
170
|
+
"version": "0.1.0",
|
|
171
|
+
"author": "unknown",
|
|
172
|
+
"description": "",
|
|
173
|
+
"framework_version": ">=2.0",
|
|
174
|
+
"entry_point": "src/main.py",
|
|
175
|
+
"allowed_imports": [],
|
|
176
|
+
"requires": [],
|
|
177
|
+
"env": {},
|
|
178
|
+
"permissions": [],
|
|
179
|
+
**cfg,
|
|
180
|
+
}
|
|
181
|
+
created: list[Path] = []
|
|
182
|
+
|
|
183
|
+
def write(path: Path, content: str) -> None:
|
|
184
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
path.write_text(content, encoding="utf-8")
|
|
186
|
+
created.append(path)
|
|
187
|
+
|
|
188
|
+
# plugin.yaml
|
|
189
|
+
manifest = _build_manifest(cfg)
|
|
190
|
+
write(
|
|
191
|
+
target_dir / "plugin.yaml",
|
|
192
|
+
yaml.dump(manifest, default_flow_style=False, allow_unicode=True, sort_keys=False),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# src/__init__.py
|
|
196
|
+
write(target_dir / "src" / "__init__.py", "")
|
|
197
|
+
|
|
198
|
+
# src/main.py
|
|
199
|
+
template = _TEMPLATES[cfg["execution_mode"]]
|
|
200
|
+
write(
|
|
201
|
+
target_dir / "src" / "main.py",
|
|
202
|
+
template.format(name=cfg["name"], description=cfg["description"] or ""),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# data/.gitkeep
|
|
206
|
+
write(target_dir / "data" / ".gitkeep", "")
|
|
207
|
+
|
|
208
|
+
# .env (only if env vars defined)
|
|
209
|
+
if cfg.get("env"):
|
|
210
|
+
env_lines = "\n".join(f'{k}={v}' for k, v in cfg["env"].items())
|
|
211
|
+
write(target_dir / ".env", env_lines + "\n")
|
|
212
|
+
|
|
213
|
+
return created
|
xcli/sandbox/__init__.py
ADDED
|
File without changes
|
xcli/sandbox/cli.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from typer import Typer
|
|
11
|
+
|
|
12
|
+
from xcli._run import ns, run
|
|
13
|
+
|
|
14
|
+
_CTX = {"help_option_names": ["-h", "--help"]}
|
|
15
|
+
app = Typer(help="Sandbox inspection and runtime.", context_settings=_CTX)
|
|
16
|
+
console = Console()
|
|
17
|
+
err = Console(stderr=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── run ───────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
@app.command("run")
|
|
23
|
+
def sandbox_run(name: str) -> None:
|
|
24
|
+
"""Launch a plugin in an isolated sandbox."""
|
|
25
|
+
from xcore.cli.sandbox_cmd import handle_sandbox
|
|
26
|
+
run(handle_sandbox(ns(subcommand="run", name=name)))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── call ──────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
@app.command("call")
|
|
32
|
+
def sandbox_call(
|
|
33
|
+
name: str,
|
|
34
|
+
action: str,
|
|
35
|
+
payload: str = typer.Option("{}", "--payload", "-p", help="JSON payload"),
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Start a plugin sandbox, call one action, print the result, then stop."""
|
|
38
|
+
try:
|
|
39
|
+
data = json.loads(payload)
|
|
40
|
+
except json.JSONDecodeError as e:
|
|
41
|
+
err.print(f"[red]Invalid JSON payload:[/red] {e}")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
|
|
44
|
+
async def _run():
|
|
45
|
+
from xcore.configurations.loader import ConfigLoader
|
|
46
|
+
from xcore.kernel.sandbox.process_manager import SandboxConfig, SandboxProcessManager
|
|
47
|
+
from xcore.kernel.security.validation import ManifestValidator
|
|
48
|
+
from xcli._xcore import _NullCtx
|
|
49
|
+
|
|
50
|
+
cfg = ConfigLoader.load(None)
|
|
51
|
+
plugin_dir = Path(cfg.plugins.directory) / name
|
|
52
|
+
|
|
53
|
+
if not plugin_dir.is_dir():
|
|
54
|
+
err.print(f"[red]Plugin '[cyan]{name}[/cyan]' not found in {cfg.plugins.directory}[/red]")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
manifest, _, _ = ManifestValidator().load_and_validate(plugin_dir)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
err.print(f"[red]Invalid manifest:[/red] {escape(str(e))}")
|
|
61
|
+
raise typer.Exit(1)
|
|
62
|
+
|
|
63
|
+
if manifest.execution_mode.value != "sandboxed":
|
|
64
|
+
err.print(
|
|
65
|
+
f"[yellow]⚠[/yellow] Plugin '[cyan]{name}[/cyan]' is mode "
|
|
66
|
+
f"[magenta]{manifest.execution_mode.value}[/magenta], not sandboxed. "
|
|
67
|
+
f"Use [dim]xcli plugin call[/dim] instead."
|
|
68
|
+
)
|
|
69
|
+
raise typer.Exit(1)
|
|
70
|
+
|
|
71
|
+
config = SandboxConfig(
|
|
72
|
+
timeout=manifest.resources.timeout_seconds,
|
|
73
|
+
max_restarts=0,
|
|
74
|
+
startup_timeout=5.0,
|
|
75
|
+
)
|
|
76
|
+
mgr = SandboxProcessManager(manifest, _NullCtx(), config)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with console.status(f"Starting sandbox [cyan]{name}[/cyan]..."):
|
|
80
|
+
await mgr.start()
|
|
81
|
+
|
|
82
|
+
console.print(f"[dim]→ {name}.{action}({payload})[/dim]")
|
|
83
|
+
resp = await mgr._channel.call(action, data)
|
|
84
|
+
|
|
85
|
+
if resp.success:
|
|
86
|
+
console.print(Panel(
|
|
87
|
+
json.dumps(resp.data, indent=2, ensure_ascii=False),
|
|
88
|
+
title=f"[green]{name}.{action}[/green]",
|
|
89
|
+
border_style="green",
|
|
90
|
+
))
|
|
91
|
+
else:
|
|
92
|
+
console.print(Panel(
|
|
93
|
+
escape(str(resp.data)),
|
|
94
|
+
title=f"[red]{name}.{action} — error[/red]",
|
|
95
|
+
border_style="red",
|
|
96
|
+
))
|
|
97
|
+
finally:
|
|
98
|
+
await mgr.stop()
|
|
99
|
+
console.print(f"[dim]Sandbox {name} stopped.[/dim]")
|
|
100
|
+
|
|
101
|
+
run(_run())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ── limits / network / fs ─────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
@app.command("limits")
|
|
107
|
+
def limits(name: str) -> None:
|
|
108
|
+
"""Show resource limits declared in the plugin manifest."""
|
|
109
|
+
from xcore.cli.sandbox_cmd import handle_sandbox
|
|
110
|
+
run(handle_sandbox(ns(subcommand="limits", name=name)))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command("network")
|
|
114
|
+
def network(name: str) -> None:
|
|
115
|
+
"""Show the network policy for a plugin."""
|
|
116
|
+
from xcore.cli.sandbox_cmd import handle_sandbox
|
|
117
|
+
run(handle_sandbox(ns(subcommand="network", name=name)))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command("fs")
|
|
121
|
+
def fs(name: str) -> None:
|
|
122
|
+
"""Show the filesystem policy for a plugin."""
|
|
123
|
+
from xcore.cli.sandbox_cmd import handle_sandbox
|
|
124
|
+
run(handle_sandbox(ns(subcommand="fs", name=name)))
|
xcli/worker/__init__.py
ADDED
|
File without changes
|