xcorecli 2.2.0__tar.gz → 2.3.0__tar.gz

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.
Files changed (76) hide show
  1. {xcorecli-2.2.0 → xcorecli-2.3.0}/PKG-INFO +1 -1
  2. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/commands/health.md +3 -1
  3. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/commands/init.md +13 -5
  4. xcorecli-2.3.0/docs/config/index.md +48 -0
  5. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/getting-started/configuration.md +1 -1
  6. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/getting-started/install.md +1 -1
  7. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/index.md +1 -1
  8. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/migration/index.md +17 -0
  9. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/plugin/install.md +26 -2
  10. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/plugin/marketplace.md +17 -2
  11. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/plugin/security.md +29 -0
  12. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/reference.md +35 -0
  13. xcorecli-2.3.0/docs/sandbox/index.md +86 -0
  14. {xcorecli-2.2.0 → xcorecli-2.3.0}/pyproject.toml +1 -1
  15. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/migrations/cli.py +105 -12
  16. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/migrations/runtime.py +37 -3
  17. xcorecli-2.2.0/docs/config/index.md +0 -49
  18. xcorecli-2.2.0/docs/sandbox/index.md +0 -43
  19. {xcorecli-2.2.0 → xcorecli-2.3.0}/.github/workflows/publish.yml +0 -0
  20. {xcorecli-2.2.0 → xcorecli-2.3.0}/.gitignore +0 -0
  21. {xcorecli-2.2.0 → xcorecli-2.3.0}/.python-version +0 -0
  22. {xcorecli-2.2.0 → xcorecli-2.3.0}/.vscode/configurationCache.log +0 -0
  23. {xcorecli-2.2.0 → xcorecli-2.3.0}/.vscode/dryrun.log +0 -0
  24. {xcorecli-2.2.0 → xcorecli-2.3.0}/.vscode/settings.json +0 -0
  25. {xcorecli-2.2.0 → xcorecli-2.3.0}/.vscode/targets.log +0 -0
  26. {xcorecli-2.2.0 → xcorecli-2.3.0}/README.md +0 -0
  27. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/commands/login.md +0 -0
  28. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/getting-started/auth.md +0 -0
  29. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/manager/index.md +0 -0
  30. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/manager/monitoring.md +0 -0
  31. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/manager/services.md +0 -0
  32. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/plugin/index.md +0 -0
  33. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/plugin/local.md +0 -0
  34. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/plugin/runtime.md +0 -0
  35. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/plugin/update.md +0 -0
  36. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/service/index.md +0 -0
  37. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/worker/index.md +0 -0
  38. {xcorecli-2.2.0 → xcorecli-2.3.0}/docs/worker/process.md +0 -0
  39. {xcorecli-2.2.0 → xcorecli-2.3.0}/makefile +0 -0
  40. {xcorecli-2.2.0 → xcorecli-2.3.0}/mkdocs.yml +0 -0
  41. {xcorecli-2.2.0 → xcorecli-2.3.0}/uv.lock +0 -0
  42. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/__init__.py +0 -0
  43. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/_credentials.py +0 -0
  44. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/_login.py +0 -0
  45. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/_run.py +0 -0
  46. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/_xcore.py +0 -0
  47. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/config/__init__.py +0 -0
  48. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/config/cli.py +0 -0
  49. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/config/runtime.py +0 -0
  50. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/init/__init__.py +0 -0
  51. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/init/manager.py +0 -0
  52. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/init/upgrade.py +0 -0
  53. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/main.py +0 -0
  54. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/manager/__init__.py +0 -0
  55. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/manager/cli.py +0 -0
  56. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/migrations/__init__.py +0 -0
  57. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/__init__.py +0 -0
  58. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/cli.py +0 -0
  59. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/install_commands.py +0 -0
  60. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/local_commands.py +0 -0
  61. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/marketplace_commands.py +0 -0
  62. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/runtime_commands.py +0 -0
  63. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/scaffold.py +0 -0
  64. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/security_commands.py +0 -0
  65. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/shared.py +0 -0
  66. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/plugin/update_commands.py +0 -0
  67. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/sandbox/__init__.py +0 -0
  68. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/sandbox/cli.py +0 -0
  69. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/service/__init__.py +0 -0
  70. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/service/cli.py +0 -0
  71. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/service/install_commands.py +0 -0
  72. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/service/marketplace_commands.py +0 -0
  73. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/service/shared.py +0 -0
  74. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/worker/__init__.py +0 -0
  75. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/worker/cli.py +0 -0
  76. {xcorecli-2.2.0 → xcorecli-2.3.0}/xcli/worker/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xcorecli
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: CLI for xcore — configuration, plugins, monitoring
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: alembic>=1.18.0
@@ -39,5 +39,7 @@ xcli services
39
39
 
40
40
  If `health` reports errors:
41
41
  1. Check the logs: `make logs`
42
- 2. Validate configuration: `xcli config validate`
42
+ 2. Check your marketplace credentials aren't the issue: `xcli config show`
43
+ (there's no dedicated `integration.yaml` validator — it's checked
44
+ automatically whenever `xcli manager start` reads it)
43
45
  3. Restart services: `make restart`
@@ -12,11 +12,19 @@ xcli init my-project
12
12
 
13
13
  ### Database Options
14
14
 
15
- The initialization wizard supports several database backends with pre-configured URL templates:
16
- - **SQLite**: `sqlite:///./data/xcore.db` (Default)
17
- - **PostgreSQL**: `postgresql://user:pass@localhost:5432/db`
18
- - **MySQL**: `mysql+pymysql://user:pass@localhost:3306/db`
19
- - **MariaDB**: `mysql+pymysql://user:pass@localhost:3306/db`
15
+ The initialization wizard supports several database backends with
16
+ pre-configured URL templates — always the **async** driver variant, never
17
+ a sync one, since `xcore` is async end-to-end:
18
+
19
+ - **SQLite** (default): `sqlite+aiosqlite:///./xcore.db`
20
+ - **PostgreSQL**: `postgresql+asyncpg://user:pass@localhost:5432/dbname`
21
+ - **MySQL**: `mysql+aiomysql://user:pass@localhost:3306/dbname`
22
+ - **MariaDB**: `mysql+aiomysql://user:pass@localhost:3306/dbname`
23
+
24
+ `--db-url` overrides the template entirely if you need a different
25
+ host/user/password — but keep the `+aiosqlite`/`+asyncpg`/`+aiomysql`
26
+ driver suffix; a plain `postgresql://`/`mysql://` URL uses a sync driver
27
+ that doesn't work with this framework's async SQLAlchemy engine.
20
28
 
21
29
  ### Generated Structure
22
30
 
@@ -0,0 +1,48 @@
1
+ # CLI Configuration
2
+
3
+ The `config` command group stores the two credentials `xcorecli` needs to
4
+ talk to the marketplace: an **API key** (`xdk_...`, authorizes downloads)
5
+ and a **signing key** (verifies the HMAC signature of every ZIP before
6
+ extraction). It does **not** manage `integration.yaml` or any other
7
+ project-level setting — see [Getting started → Configuration](../getting-started/configuration.md)
8
+ for that.
9
+
10
+ !!! tip "Prefer `xcli login`"
11
+ `xcli login` (device-code flow, opens a browser) fetches and stores
12
+ **both** credentials in one step, without ever printing them to the
13
+ terminal. `config set` below is the manual fallback — useful in a
14
+ non-interactive environment (CI, a container) where opening a browser
15
+ isn't possible.
16
+
17
+ ## Commands
18
+
19
+ | Command | Description |
20
+ |---------|-------------|
21
+ | `xcli config set <key> <value>` | Store a credential — `key` must be `api-key` or `signing-key`. |
22
+ | `xcli config show` | Print whether each credential is set (values are masked, never shown in full). |
23
+
24
+ ## Setting credentials manually
25
+
26
+ ```bash
27
+ xcli config set api-key xdk_...
28
+ xcli config set signing-key <your-signing-secret>
29
+ ```
30
+
31
+ Both are written to `~/.xcli/config.json` — the same file `xcli login`
32
+ writes to, so mixing the two approaches (login once, then manually rotate
33
+ one key later) is safe.
34
+
35
+ ## Checking status
36
+
37
+ ```bash
38
+ xcli config show
39
+ ```
40
+
41
+ ```
42
+ api-key: set
43
+ signing-key: not set
44
+ ```
45
+
46
+ Nothing else is configurable through this command group — there is no
47
+ `get`/`validate` sub-command, and no arbitrary `key.path value` form; `key`
48
+ is restricted to exactly `api-key` or `signing-key`.
@@ -67,4 +67,4 @@ observability:
67
67
 
68
68
  ## Validation
69
69
 
70
- `xcorecli` validates this file upon startup to ensure all required fields are present and correctly typed. Use `xcli config validate` to check your configuration manually.
70
+ `xcorecli` validates this file upon startup to ensure all required fields are present and correctly typed — there is no separate command to check it manually ahead of time; `xcli manager start` (or any command that touches the running project) is itself the validation step. This is unrelated to [`xcli config`](../config/index.md), which only stores your marketplace API/signing keys, not `integration.yaml` settings.
@@ -13,7 +13,7 @@ Getting started with `xcorecli` is straightforward. The project uses [Poetry](ht
13
13
  ### 1. Clone the Repository
14
14
 
15
15
  ```bash
16
- git clone https://github.com/your-repo/xcorecli.git
16
+ git clone https://github.com/xcore-team/xcoreCli.git
17
17
  cd xcorecli
18
18
  ```
19
19
 
@@ -17,7 +17,7 @@
17
17
  ## Quick Start Overview
18
18
 
19
19
  ```bash title="Quick Install"
20
- git clone https://github.com/your-repo/xcorecli.git
20
+ git clone https://github.com/xcore-team/xcoreCli.git
21
21
  cd xcorecli
22
22
  make install
23
23
  ```
@@ -23,6 +23,15 @@ xcli migration init
23
23
 
24
24
  This creates the `alembic/` directory, configuration files, and scans for initial models.
25
25
 
26
+ !!! tip "Custom directory name"
27
+ Pass `--dir my_migrations` to use a different directory name. The chosen
28
+ name is persisted to `migration.directory` in `integration.yaml`, so every
29
+ other `migration` command (`revision`, `upgrade`, `downgrade`, `current`,
30
+ `history`, `heads`, `stamp`) picks it up automatically — no need to repeat
31
+ `--dir` on each call. To rename an already-initialized project, use
32
+ `xcli migration rename <new_name>` instead of moving the folder by hand —
33
+ it also updates `alembic.ini` and `integration.yaml` for you.
34
+
26
35
  ## Safety & Backups
27
36
 
28
37
  Before performing dangerous operations, `xcorecli` can automatically backup your database.
@@ -99,5 +108,13 @@ Check which migrations have been applied.
99
108
  xcli migration history
100
109
  ```
101
110
 
111
+ See the database's current revision, or where the migration chain's
112
+ unapplied heads are:
113
+
114
+ ```bash
115
+ xcli migration current # what revision the DB is actually stamped at
116
+ xcli migration heads # latest revision(s) defined in the migration scripts
117
+ ```
118
+
102
119
  !!! tip "Stamping"
103
120
  Use `xcli migration stamp <id>` to mark the database at a specific revision without running the actual migration scripts.
@@ -32,14 +32,38 @@ To see all available versions for a plugin:
32
32
  xcli plugin versions name-of-plugin
33
33
  ```
34
34
 
35
- ## Installing from Git or URL
35
+ ## Installing from Git or a Local Zip
36
36
 
37
- You can install plugins directly from a Git repository or a hosted `.zip` file.
37
+ You can install plugins directly from a Git repository or a `.zip` file
38
+ instead of the marketplace — neither one goes through HMAC verification,
39
+ that's marketplace-only, so run `xcli plugin health` afterward (see below).
38
40
 
39
41
  ```bash title="Git Install"
40
42
  xcli plugin install my-plugin --source git --url https://github.com/user/plugin.git
41
43
  ```
42
44
 
45
+ ```bash title="Zip Install"
46
+ xcli plugin install my-plugin --source zip --url ./path/to/plugin.zip
47
+ ```
48
+
49
+ `--url` is required for both `git` and `zip` — omitting it fails fast
50
+ rather than falling back to the marketplace.
51
+
52
+ ## Reinstalling
53
+
54
+ ```bash
55
+ xcli plugin install name-of-plugin --force # overwrite an existing install
56
+ ```
57
+
58
+ Without `--force`, installing over an already-installed plugin (same name)
59
+ is refused with a one-line message telling you to add the flag.
60
+
61
+ !!! bug "`--no-deps` currently has no effect"
62
+ The flag is accepted (`install --no-deps`) but the installer never
63
+ reads it — confirmed in `xcli/plugin/install_commands.py`: dependencies
64
+ install the same way whether or not you pass it. Not documented as
65
+ working here on purpose; treat it as reserved for now.
66
+
43
67
  ## Management Commands
44
68
 
45
69
  ### Detailed Info
@@ -1,7 +1,8 @@
1
1
  # Plugin Marketplace
2
2
 
3
- Discover plugins available on the marketplace before installing them — all
4
- three commands here are read-only and public, no credentials required.
3
+ Discover plugins available on the marketplace before installing them.
4
+ `browse`/`search`/`info` below are read-only and public, no credentials
5
+ required; `mine` is the one exception (see below).
5
6
 
6
7
  ## Browse All
7
8
 
@@ -30,6 +31,20 @@ published versions.
30
31
  xcli plugin marketplace info name-of-plugin
31
32
  ```
32
33
 
34
+ ## Your Plugins
35
+
36
+ `browse`/`search` above only ever show **public** plugins — unauthenticated
37
+ requests, by design. To also see your own **private** plugins, use `mine`
38
+ instead, which sends your API key:
39
+
40
+ ```bash title="Your plugins"
41
+ xcli plugin marketplace mine
42
+ ```
43
+
44
+ Needs the same API key as installing does — see
45
+ [Authentication](../getting-started/auth.md). `xcli login`'s personal key
46
+ works here even without a project-scoped key for any specific plugin.
47
+
33
48
  ## What's not here
34
49
 
35
50
  Rating a plugin (`POST /plugins/{slug}/ratings`) requires a full user
@@ -29,6 +29,35 @@ For a comprehensive check of all plugins (including manifest validation and AST
29
29
  xcli plugin health
30
30
  ```
31
31
 
32
+ ## Validating Manifests
33
+
34
+ Beyond signature checks, `validate` checks the plugin manifest itself
35
+ (`plugin.yaml` structure, required fields) and can track its **IPC
36
+ surface** — the actions and events a plugin exposes to others — across
37
+ versions:
38
+
39
+ ```bash title="Validate a plugin"
40
+ xcli plugin security validate my-plugin
41
+ ```
42
+
43
+ Without a path, it scans every plugin found via `integration.yaml`
44
+ instead of just one:
45
+
46
+ ```bash
47
+ xcli plugin security validate
48
+ ```
49
+
50
+ ```bash title="Track breaking IPC changes"
51
+ xcli plugin security validate my-plugin --save # snapshot today's IPC surface
52
+ xcli plugin security validate my-plugin --check-breaking # diff against that snapshot
53
+ ```
54
+
55
+ `--save` writes the current IPC action/event schemas to a snapshot file
56
+ (default `.xcore/schemas.json`, override with `--schema-file`); a later
57
+ `--check-breaking` run reports anything removed or changed since — useful
58
+ in CI to catch an accidental breaking change to a plugin's public IPC
59
+ contract before it ships.
60
+
32
61
  ## Strict Mode
33
62
 
34
63
  Enable `strict_trusted` in your configuration to refuse any plugin that is not properly signed.
@@ -12,6 +12,15 @@ A comprehensive list of all commands available in `xcorecli`.
12
12
  - `xcli services`: Show status and details of all system services (local
13
13
  runtime — not the marketplace catalog, see `xcli service` below).
14
14
 
15
+ ## `config` Credentials
16
+
17
+ - `config set <api-key|signing-key> <value>`: Store one credential in
18
+ `~/.xcli/config.json` — the manual alternative to `xcli login` above.
19
+ - `config show`: Print whether each credential is set (masked).
20
+
21
+ Only these two keys exist — no arbitrary `integration.yaml` settings live
22
+ here, see [Configuration](getting-started/configuration.md) for that file.
23
+
15
24
  ## `manager` Administration
16
25
 
17
26
  - `manager start`: Start the FastAPI server (uvicorn).
@@ -50,10 +59,19 @@ A comprehensive list of all commands available in `xcorecli`.
50
59
  - `marketplace browse`: List published plugins (`--sort newest|downloads|rating`).
51
60
  - `marketplace search`: Search by name or description.
52
61
  - `marketplace info`: Pre-install details, including published versions.
62
+ - `marketplace mine`: List *your* plugins — public and private alike (needs
63
+ an API key; `browse`/`search`/`info` above are public, no credentials).
53
64
 
54
65
  No `rate` command — rating requires a full user session (JWT), not an API
55
66
  key; rate plugins from the XCoreHub dashboard.
56
67
 
68
+ ### `plugin security` Signing & validation
69
+ - `security sign`: HMAC-sign a plugin (`plugin.sig`).
70
+ - `security verify`: Verify a plugin's signature.
71
+ - `security validate`: Validate plugin manifest(s); `--check-breaking`
72
+ diffs IPC actions/events against a saved schema snapshot, `--save`
73
+ updates that snapshot.
74
+
57
75
  ### `plugin update` Maintenance
58
76
  - `update check`: Check for new versions.
59
77
  - `update apply`: Apply updates (--all, --dry-run).
@@ -62,7 +80,21 @@ key; rate plugins from the XCoreHub dashboard.
62
80
  - `runtime load`: Activate a plugin.
63
81
  - `runtime unload`: Deactivate a plugin.
64
82
  - `runtime reload`: Restart a plugin.
83
+ - `runtime reload-all`: Restart every active plugin at once.
65
84
  - `runtime status`: Show active plugins.
85
+ - `runtime call`: Invoke a plugin action directly (`--payload '{"k": "v"}'`).
86
+
87
+ ## `sandbox` Isolated Execution
88
+
89
+ Run a plugin in isolation and inspect its declared resource/network/
90
+ filesystem policy — none of these require the plugin to already be loaded
91
+ by a running instance.
92
+
93
+ - `sandbox run`: Launch a plugin sandboxed, keep it running (Ctrl+C to stop).
94
+ - `sandbox call`: Start sandboxed, call one action, print the result, stop.
95
+ - `sandbox limits`: Show resource limits from the manifest.
96
+ - `sandbox network`: Show the declared network policy.
97
+ - `sandbox fs`: Show the declared filesystem policy.
66
98
 
67
99
  ## `service` Marketplace Extensions
68
100
 
@@ -104,4 +136,7 @@ confused with `xcli services`/`xcli manager services` (local runtime).
104
136
  - `migration revision`: Create a new migration.
105
137
  - `migration upgrade`: Apply migrations (--backup).
106
138
  - `migration downgrade`: Rollback migrations.
139
+ - `migration current`: Show the database's current revision.
140
+ - `migration heads`: Show the migration chain's latest defined revision(s).
141
+ - `migration stamp`: Mark the DB at a revision without running scripts.
107
142
  - `migration history`: Show migration list.
@@ -0,0 +1,86 @@
1
+ # Plugin Sandboxing
2
+
3
+ The Sandbox provides a secure execution environment for third-party or untrusted plugins, ensuring they cannot compromise the host system.
4
+
5
+ ## Resource Isolation
6
+
7
+ Sandboxed plugins are restricted in their resource consumption to prevent "noisy neighbor" issues or intentional Denial of Service.
8
+
9
+ ### Limits Configuration
10
+
11
+ You can define default limits in `integration.yaml`:
12
+
13
+ ```yaml
14
+ security:
15
+ rate_limit_default:
16
+ calls: 200 # Max IPC calls
17
+ period_seconds: 60 # Per minute
18
+ ```
19
+
20
+ ## AST-Based Whitelisting
21
+
22
+ The core of the sandbox is an AST (Abstract Syntax Tree) analyzer that scans the plugin's code before execution.
23
+
24
+ - **Whitelisted**: Only modules listed in `security.allowed_imports` can be imported.
25
+ - **Blacklisted**: Modules in `security.forbidden_imports` (like `os` or `subprocess`) are explicitly blocked.
26
+
27
+ !!! danger "Sandbox Bypass"
28
+ Attempting to bypass the sandbox via reflection or other advanced Python techniques is monitored and will result in the plugin being immediately unloaded.
29
+
30
+ ## Managing the Sandbox
31
+
32
+ Use the `sandbox` command group to run a plugin in isolation and inspect the
33
+ policy declared in its manifest — none of these require the plugin to
34
+ already be loaded by a running `xcore` instance, unlike `xcli plugin
35
+ runtime`.
36
+
37
+ ### Run
38
+
39
+ Launch a plugin in an isolated sandbox process and keep it running until
40
+ you interrupt it (Ctrl+C):
41
+
42
+ ```bash
43
+ xcli sandbox run my-sandboxed-plugin
44
+ ```
45
+
46
+ ### Call
47
+
48
+ Start the sandbox, invoke a single action, print the result, then stop —
49
+ useful for testing one IPC action without keeping the process alive:
50
+
51
+ ```bash
52
+ xcli sandbox call my-sandboxed-plugin send_email --payload '{"to": "user@example.com"}'
53
+ ```
54
+
55
+ !!! warning "Sandboxed plugins only"
56
+ `sandbox call` refuses a plugin whose `execution_mode` isn't
57
+ `sandboxed` — use `xcli plugin runtime call` for a `trusted` plugin
58
+ instead.
59
+
60
+ ### Limits
61
+
62
+ Show the resource limits declared in the plugin's manifest (timeout, max
63
+ memory/disk, rate limit) — read-only, no process started:
64
+
65
+ ```bash
66
+ xcli sandbox limits my-sandboxed-plugin
67
+ ```
68
+
69
+ ### Network
70
+
71
+ Show the plugin's declared network policy (`network:` in `plugin.yaml`):
72
+
73
+ ```bash
74
+ xcli sandbox network my-sandboxed-plugin
75
+ ```
76
+
77
+ ### Filesystem
78
+
79
+ Show the plugin's declared filesystem policy (`filesystem:` in `plugin.yaml`):
80
+
81
+ ```bash
82
+ xcli sandbox fs my-sandboxed-plugin
83
+ ```
84
+
85
+ !!! info "Trusted vs. Sandboxed"
86
+ By default, plugins are treated as **Sandboxed**. You must explicitly mark a plugin as **Trusted** in its `plugin.yaml` (and usually sign it) to run it with full permissions.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "xcorecli"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "CLI for xcore — configuration, plugins, monitoring"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -17,6 +17,7 @@ from xcli.migrations.runtime import (
17
17
  discover_models,
18
18
  get_backup_dir,
19
19
  get_database_url,
20
+ get_migration_dir,
20
21
  get_scan_paths,
21
22
  list_backups,
22
23
  parse_db_url,
@@ -24,6 +25,7 @@ from xcli.migrations.runtime import (
24
25
  render_discovery_summary,
25
26
  restore_database,
26
27
  run_alembic_command,
28
+ set_migration_dir,
27
29
  )
28
30
 
29
31
  _CTX = {"help_option_names": ["-h", "--help"]}
@@ -41,7 +43,7 @@ from pathlib import Path
41
43
  import sys
42
44
 
43
45
  from alembic import context
44
- from sqlalchemy import engine_from_config, pool
46
+ from sqlalchemy import MetaData, engine_from_config, pool
45
47
 
46
48
  config = context.config
47
49
  ROOT = Path(config.config_file_name).resolve().parent if config.config_file_name else Path.cwd()
@@ -55,12 +57,47 @@ if config.config_file_name is not None:
55
57
 
56
58
  config.set_main_option("sqlalchemy.url", get_database_url())
57
59
  discovery = discover_models()
58
- target_metadata = discovery.target_metadata
60
+
61
+
62
+ def _merge_metadata(entries: list[MetaData]) -> MetaData | None:
63
+ merged = MetaData()
64
+ seen: set[str] = set()
65
+ for entry in entries:
66
+ # `sorted_tables` respecte l'ordre des dépendances (une table est copiée après celles qu'elle référence).
67
+ for table in entry.sorted_tables:
68
+ key = table.key
69
+ if key in seen:
70
+ continue
71
+ seen.add(key)
72
+ table.to_metadata(merged)
73
+ # `MetaData` n'a pas de `__bool__` : un objet vide reste « vrai », c'est `tables` qu'il faut interroger.
74
+ return merged if merged.tables else None
75
+
76
+
77
+ # Pilotes async de l'application → pilote synchrone équivalent DÉJÀ présent dans l'environnement. `aiosqlite` n'a
78
+ # pas de « pilote » : le dialecte `sqlite` est déjà synchrone, d'où la chaîne vide.
79
+ _ASYNC_TO_SYNC = {
80
+ "aiosqlite": "",
81
+ "asyncpg": "psycopg2",
82
+ "aiomysql": "pymysql",
83
+ "asyncmy": "pymysql",
84
+ }
85
+
86
+
87
+ def _sync_url(url: str) -> str:
88
+ for async_driver, sync_driver in _ASYNC_TO_SYNC.items():
89
+ if f"+{async_driver}:" in url:
90
+ # `sqlite` n'a pas de pilote : on retire le `+` plutôt que de laisser `sqlite+:` (plugin inexistant).
91
+ return url.replace(f"+{async_driver}:", f"+{sync_driver}:" if sync_driver else ":", 1)
92
+ return url
93
+
94
+
95
+ target_metadata = _merge_metadata(discovery.target_metadata)
59
96
 
60
97
 
61
98
  def run_migrations_offline() -> None:
62
99
  context.configure(
63
- url=get_database_url(),
100
+ url=_sync_url(get_database_url()),
64
101
  target_metadata=target_metadata,
65
102
  literal_binds=True,
66
103
  dialect_opts={"paramstyle": "named"},
@@ -73,7 +110,7 @@ def run_migrations_offline() -> None:
73
110
 
74
111
  def run_migrations_online() -> None:
75
112
  configuration = config.get_section(config.config_ini_section, {})
76
- configuration["sqlalchemy.url"] = get_database_url()
113
+ configuration["sqlalchemy.url"] = _sync_url(get_database_url())
77
114
  connectable = engine_from_config(
78
115
  configuration,
79
116
  prefix="sqlalchemy.",
@@ -94,6 +131,7 @@ if context.is_offline_mode():
94
131
  run_migrations_offline()
95
132
  else:
96
133
  run_migrations_online()
134
+
97
135
  """
98
136
 
99
137
  _SCRIPT_TEMPLATE = '''\
@@ -165,6 +203,9 @@ datefmt = %H:%M:%S
165
203
 
166
204
  # ── Helpers ────────────────────────────────────────────────────
167
205
 
206
+ _DIR_HELP = "Alembic directory (default: migration.directory in integration.yaml, else 'alembic')."
207
+
208
+
168
209
  def _alembic_dir(directory: str) -> Path:
169
210
  return (project_root() / directory).resolve()
170
211
 
@@ -199,10 +240,11 @@ def _do_backup(label: str = "") -> None:
199
240
 
200
241
  @app.command("init")
201
242
  def init(
202
- directory: str = typer.Option("alembic", "--dir", help="Alembic directory to create."),
243
+ directory: Optional[str] = typer.Option(None, "--dir", help="Alembic directory to create. " + _DIR_HELP),
203
244
  force: bool = typer.Option(False, "--force", help="Overwrite generated files if they already exist."),
204
245
  ) -> None:
205
246
  """Create an Alembic workspace wired to integration.yaml and all discovered models."""
247
+ directory = directory or get_migration_dir()
206
248
  root = project_root()
207
249
  alembic_dir = _alembic_dir(directory)
208
250
  versions_dir = alembic_dir / "versions"
@@ -234,6 +276,9 @@ def init(
234
276
  encoding="utf-8",
235
277
  )
236
278
 
279
+ if get_migration_dir() != directory:
280
+ set_migration_dir(directory)
281
+
237
282
  db_info = parse_db_url(db_url)
238
283
 
239
284
  console.print(f"[green]✓[/green] Alembic initialized → [cyan]{alembic_dir}[/cyan]")
@@ -297,9 +342,10 @@ def revision(
297
342
  True, "--autogenerate/--empty",
298
343
  help="Generate operations from discovered models.",
299
344
  ),
300
- directory: str = typer.Option("alembic", "--dir", help="Alembic directory."),
345
+ directory: Optional[str] = typer.Option(None, "--dir", help=_DIR_HELP),
301
346
  ) -> None:
302
347
  """Create a new Alembic revision from discovered models."""
348
+ directory = directory or get_migration_dir()
303
349
  _ensure_initialized(directory)
304
350
  with console.status("Scanning models..."):
305
351
  discovery = discover_models()
@@ -310,10 +356,11 @@ def revision(
310
356
  @app.command("upgrade")
311
357
  def upgrade(
312
358
  revision: str = typer.Argument("head", help="Target revision (head, +1, <id>)."),
313
- directory: str = typer.Option("alembic", "--dir", help="Alembic directory."),
359
+ directory: Optional[str] = typer.Option(None, "--dir", help=_DIR_HELP),
314
360
  backup: bool = typer.Option(False, "--backup", "-b", help="Backup database before upgrading."),
315
361
  ) -> None:
316
362
  """Apply migrations up to the target revision."""
363
+ directory = directory or get_migration_dir()
317
364
  _ensure_initialized(directory)
318
365
  if backup:
319
366
  _do_backup("pre-upgrade")
@@ -323,10 +370,11 @@ def upgrade(
323
370
  @app.command("downgrade")
324
371
  def downgrade(
325
372
  revision: str = typer.Argument(..., help="Target revision (-1, base, <id>)."),
326
- directory: str = typer.Option("alembic", "--dir", help="Alembic directory."),
373
+ directory: Optional[str] = typer.Option(None, "--dir", help=_DIR_HELP),
327
374
  backup: bool = typer.Option(True, "--backup/--no-backup", "-b", help="Backup database before downgrading."),
328
375
  ) -> None:
329
376
  """Rollback migrations to the target revision."""
377
+ directory = directory or get_migration_dir()
330
378
  _ensure_initialized(directory)
331
379
  if backup:
332
380
  _do_backup("pre-downgrade")
@@ -336,9 +384,10 @@ def downgrade(
336
384
  @app.command("current")
337
385
  def current(
338
386
  verbose: bool = typer.Option(False, "--verbose", "-v"),
339
- directory: str = typer.Option("alembic", "--dir"),
387
+ directory: Optional[str] = typer.Option(None, "--dir", help=_DIR_HELP),
340
388
  ) -> None:
341
389
  """Show the current database revision."""
390
+ directory = directory or get_migration_dir()
342
391
  _ensure_initialized(directory)
343
392
  run_alembic_command(directory, lambda cfg: command.current(cfg, verbose=verbose))
344
393
 
@@ -346,9 +395,10 @@ def current(
346
395
  @app.command("history")
347
396
  def history(
348
397
  verbose: bool = typer.Option(False, "--verbose", "-v"),
349
- directory: str = typer.Option("alembic", "--dir"),
398
+ directory: Optional[str] = typer.Option(None, "--dir", help=_DIR_HELP),
350
399
  ) -> None:
351
400
  """Show migration history."""
401
+ directory = directory or get_migration_dir()
352
402
  _ensure_initialized(directory)
353
403
  command.history(create_alembic_config(directory), verbose=verbose)
354
404
 
@@ -356,9 +406,10 @@ def history(
356
406
  @app.command("heads")
357
407
  def heads(
358
408
  verbose: bool = typer.Option(False, "--verbose", "-v"),
359
- directory: str = typer.Option("alembic", "--dir"),
409
+ directory: Optional[str] = typer.Option(None, "--dir", help=_DIR_HELP),
360
410
  ) -> None:
361
411
  """Show migration heads."""
412
+ directory = directory or get_migration_dir()
362
413
  _ensure_initialized(directory)
363
414
  command.heads(create_alembic_config(directory), verbose=verbose)
364
415
 
@@ -366,13 +417,55 @@ def heads(
366
417
  @app.command("stamp")
367
418
  def stamp(
368
419
  revision: str = typer.Argument(..., help="Revision to stamp without running migrations."),
369
- directory: str = typer.Option("alembic", "--dir"),
420
+ directory: Optional[str] = typer.Option(None, "--dir", help=_DIR_HELP),
370
421
  ) -> None:
371
422
  """Stamp the database at a revision without running migrations."""
423
+ directory = directory or get_migration_dir()
372
424
  _ensure_initialized(directory)
373
425
  run_alembic_command(directory, lambda cfg: command.stamp(cfg, revision))
374
426
 
375
427
 
428
+ @app.command("rename")
429
+ def rename_dir(
430
+ new_name: str = typer.Argument(..., help="New name for the Alembic directory."),
431
+ directory: Optional[str] = typer.Option(None, "--dir", help="Current directory name. " + _DIR_HELP),
432
+ ) -> None:
433
+ """Rename the Alembic directory and keep alembic.ini / integration.yaml in sync."""
434
+ directory = directory or get_migration_dir()
435
+ _ensure_initialized(directory)
436
+
437
+ root = project_root()
438
+ old_dir = _alembic_dir(directory)
439
+ new_dir = (root / new_name).resolve()
440
+
441
+ if new_dir.exists():
442
+ console.print(f"[red]Already exists:[/red] [cyan]{new_dir}[/cyan]")
443
+ raise typer.Exit(1)
444
+
445
+ old_dir.rename(new_dir)
446
+
447
+ ini_path = root / "alembic.ini"
448
+ ini_updated = False
449
+ if ini_path.exists():
450
+ text = ini_path.read_text(encoding="utf-8")
451
+ new_text = text.replace(f"script_location = {directory}", f"script_location = {new_name}", 1)
452
+ if new_text != text:
453
+ ini_path.write_text(new_text, encoding="utf-8")
454
+ ini_updated = True
455
+
456
+ set_migration_dir(new_name)
457
+
458
+ console.print(f"[green]✓[/green] Renamed [cyan]{directory}[/cyan] → [cyan]{new_name}[/cyan]")
459
+ if ini_updated:
460
+ console.print(f"[green]✓[/green] Updated [cyan]{ini_path}[/cyan]")
461
+ elif ini_path.exists():
462
+ console.print(
463
+ f"[yellow]⚠[/yellow] Could not find [dim]script_location = {directory}[/dim] in "
464
+ f"[cyan]{ini_path}[/cyan] — update it by hand."
465
+ )
466
+ console.print(f"[green]✓[/green] integration.yaml → [cyan]migration.directory: {new_name}[/cyan]")
467
+
468
+
376
469
  # ── Backup / Restore ───────────────────────────────────────────
377
470
 
378
471
  @app.command("backup")
@@ -202,9 +202,38 @@ def get_backup_dir() -> Path:
202
202
  return p.resolve()
203
203
 
204
204
 
205
- def create_alembic_config(directory: str = "alembic") -> Config:
205
+ def get_migration_dir() -> str:
206
+ """Name of the Alembic directory, persisted once by `xcli migration init`
207
+ (or `rename`) so every other command agrees on it without `--dir` having
208
+ to be repeated on each invocation."""
209
+ cfg = load_config()
210
+ return cfg.get("migration", {}).get("directory", "alembic")
211
+
212
+
213
+ def set_migration_dir(name: str) -> None:
214
+ """Persist `migration.directory` in integration.yaml/json, backing up the
215
+ previous file first — mirrors the backup-then-rewrite convention used by
216
+ `xcli init upgrade` (xcli/init/upgrade.py)."""
217
+ path = require_config_path()
218
+ cfg = load_config()
219
+ cfg.setdefault("migration", {})["directory"] = name
220
+
221
+ bak = path.with_name(path.name + ".bak")
222
+ bak.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
223
+
224
+ if path.suffix.lower() == ".json":
225
+ import json
226
+ path.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
227
+ else:
228
+ path.write_text(
229
+ yaml.dump(cfg, default_flow_style=False, allow_unicode=True, sort_keys=False),
230
+ encoding="utf-8",
231
+ )
232
+
233
+
234
+ def create_alembic_config(directory: str | None = None) -> Config:
206
235
  root = project_root()
207
- alembic_dir = root / directory
236
+ alembic_dir = root / (directory or get_migration_dir())
208
237
  cfg = Config()
209
238
  cfg.set_main_option("script_location", str(alembic_dir))
210
239
  cfg.set_main_option("sqlalchemy.url", get_database_url())
@@ -533,8 +562,13 @@ def list_backups(backup_dir: Path | None = None) -> list[Path]:
533
562
 
534
563
 
535
564
  def _iter_python_files(scan_root: Path) -> Iterator[Path]:
565
+ # `_IGNORED_PARTS` only covers the literal defaults ("alembic", "migrations") —
566
+ # a project that renamed its migration directory (migration.directory /
567
+ # `xcli migration rename`) needs that name excluded too, or model discovery
568
+ # starts importing migration scripts as if they were app modules.
569
+ ignored = _IGNORED_PARTS | {get_migration_dir()}
536
570
  for path in scan_root.rglob("*.py"):
537
- if any(part in _IGNORED_PARTS for part in path.parts):
571
+ if any(part in ignored for part in path.parts):
538
572
  continue
539
573
  yield path
540
574
 
@@ -1,49 +0,0 @@
1
- # CLI Configuration
2
-
3
- The `config` command group allows you to manage the behavior of `xcorecli` itself and its interaction with the `xcore` project.
4
-
5
- ## Commands Overview
6
-
7
- | Command | Description |
8
- |---------|-------------|
9
- | `show` | Display the current merged configuration. |
10
- | `get` | Retrieve a specific configuration value. |
11
- | `set` | Update a configuration value. |
12
- | `validate` | Check `integration.yaml` for schema compliance. |
13
-
14
- ## Managing Settings
15
-
16
- ### Viewing Configuration
17
-
18
- To see your current setup, including defaults and overrides from `integration.yaml`:
19
-
20
- ```bash
21
- xcli config show
22
- ```
23
-
24
- ### Updating Values
25
-
26
- You can modify settings directly from the CLI. These changes are typically applied to your local configuration or the project's `integration.yaml`.
27
-
28
- ```bash title="Set Value"
29
- xcli config set app.debug true
30
- ```
31
-
32
- !!! note "Layered Configuration"
33
- `xcorecli` uses a layered approach:
34
- 1. Internal Defaults
35
- 2. `integration.yaml` (Project level)
36
- 3. Local CLI Config (User level)
37
- 4. Environment Variables
38
-
39
- ## Runtime Configuration
40
-
41
- Some settings in the `runtime` section of `integration.yaml` can be adjusted without restarting the entire system, such as plugin reload intervals.
42
-
43
- ```yaml
44
- plugins:
45
- interval: 5 # Check for changes every 5 seconds
46
- ```
47
-
48
- !!! info "Dynamic Updates"
49
- Use `xcli manager services reload` to apply certain configuration changes on the fly.
@@ -1,43 +0,0 @@
1
- # Plugin Sandboxing
2
-
3
- The Sandbox provides a secure execution environment for third-party or untrusted plugins, ensuring they cannot compromise the host system.
4
-
5
- ## Resource Isolation
6
-
7
- Sandboxed plugins are restricted in their resource consumption to prevent "noisy neighbor" issues or intentional Denial of Service.
8
-
9
- ### Limits Configuration
10
-
11
- You can define default limits in `integration.yaml`:
12
-
13
- ```yaml
14
- security:
15
- rate_limit_default:
16
- calls: 200 # Max IPC calls
17
- period_seconds: 60 # Per minute
18
- ```
19
-
20
- ## AST-Based Whitelisting
21
-
22
- The core of the sandbox is an AST (Abstract Syntax Tree) analyzer that scans the plugin's code before execution.
23
-
24
- - **Whitelisted**: Only modules listed in `security.allowed_imports` can be imported.
25
- - **Blacklisted**: Modules in `security.forbidden_imports` (like `os` or `subprocess`) are explicitly blocked.
26
-
27
- !!! danger "Sandbox Bypass"
28
- Attempting to bypass the sandbox via reflection or other advanced Python techniques is monitored and will result in the plugin being immediately unloaded.
29
-
30
- ## Managing the Sandbox
31
-
32
- Use the `sandbox` command group to inspect the status of the isolation layer.
33
-
34
- ```bash
35
- # View sandbox statistics
36
- xcli sandbox stats
37
-
38
- # Inspect a specific plugin's sandbox
39
- xcli sandbox inspect my-sandboxed-plugin
40
- ```
41
-
42
- !!! info "Trusted vs. Sandboxed"
43
- By default, plugins are treated as **Sandboxed**. You must explicitly mark a plugin as **Trusted** in its `plugin.yaml` (and usually sign it) to run it with full permissions.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes