The workspace¶
11.1 What it holds¶
cloudg.mcp.state.Workspace is the in-memory state every request shares: named datasets,
which one is active, the directories tools may touch, and change listeners. A Dataset is
one loaded view of infrastructure (assets, edges, findings, compliance results, coverage,
organization, unresolved references) plus where it came from. Derived structures such as
lookup indexes, the NetworkX graph, the dependency graph, reachability, centrality and the
RDF ontology are built on first use, cached, and dropped when the dataset changes
(dataset.version goes up).
from cloudg.mcp import Workspace
Workspace(config=None, *, allowed_roots=None, output_dir=None, max_datasets=16)
| Argument | Default | Meaning |
|---|---|---|
config |
CloudGConfig() |
Used by the live tools and for the default report directory |
allowed_roots |
$CLOUDG_MCP_ALLOWED_ROOTS, else the current directory plus config.report.output_dir, either left out when it is / or the home directory |
Directories tools may read from and write to |
output_dir |
config.report.output_dir |
Where export tools write. Added to the allowed roots if it is not inside one |
max_datasets |
16 | Beyond this, the oldest non-active dataset is evicted |
Every tool takes an optional dataset argument; empty means the active one. Methods you will
use from Python:
| Method | Effect |
|---|---|
ws.load(path, name=None, *, kind="auto", activate=True, normalise=True, replace=False) |
Path-checked load; returns the Dataset |
ws.add(dataset, *, activate=True, replace=False) |
Add a Dataset you built yourself |
ws.check_name(name, *, replace=False) |
Validate a new dataset name before doing any work |
ws.add_inventory(result, name=None) |
Add an InventoryResult (for example from CloudGEngine.map_inventory) |
ws.get(name=None) |
The named or active dataset; NoDatasetError with a hint when there is none |
ws.select(name), ws.remove(name) |
Change the active dataset; unload one |
ws.snapshot(name, new_name, *, replace=False) |
Copy a dataset under a new name, not activated |
ws.diff(base, target=None, limit=50) |
Added, removed and changed assets, edges and findings |
ws.names(), ws.datasets(), ws.active_name, len(ws), name in ws |
Inspection |
ws.status() |
What workspace_status returns |
ws.check_path(path, *, must_exist=True), ws.output_path(subpath="") |
Path safety (11.3) |
ws.on_change(fn), ws.notify(kind, uri=None), ws.mutated(dataset) |
Change events |
ws.unique_name(base) |
A free dataset name derived from base |
Dataset names must match ^[A-Za-z0-9_.-]{1,64}$. An existing name is only overwritten with
replace=True; the load_dataset, snapshot_dataset and live tools take a replace
argument for the same purpose. Without it, a clash is an error that suggests a free name, from
a map_inventory call naming an existing dataset:
{"isError": true,
"text": "A dataset with that name already exists. Choose another name (the error data suggests a free one), or pass replace=true to overwrite it.",
"_meta": {"cloudg/error_code": -32602,
"cloudg/error_data": {"existing": "live-1", "suggested": "live-1-2"}}}
(Shape condensed from the ToolResult.)
11.2 Loaders and auto-detection¶
load(path, kind="auto") decides what the file holds:
| Input | Detected kind |
|---|---|
Directory with inventory-map.json |
inventory (and a findings.json next to it is merged in) |
Directory with findings.json |
report |
Directory containing scoutsuite_results*.js |
scoutsuite |
Directory containing results_json.json |
checkov |
File ending .js or named scoutsuite_results* |
scoutsuite |
JSON object with findings and (metadata or summary) |
report (cloudg's findings.json) |
JSON object with assets and edges and (unresolved_references or regions) |
inventory |
JSON object with any of assets, edges, findings |
generic |
JSON object with Results and (ArtifactName, SchemaVersion or ArtifactType) |
trivy |
JSON object with check_type, or whose results is an object |
checkov |
JSON object with Findings |
prowler |
JSON list whose first item has check_type |
checkov |
JSON list whose first item has ProductArn or SchemaVersion (ASFF) |
prowler |
JSON list whose first item is an OCSF record (finding_info, class_uid, category_uid, type_uid or severity_id, and no ProductArn), Prowler 4's default output |
prowler, parsed as OCSF |
JSON list whose first item has severity, title or resource_id |
generic |
| JSON Lines (first line parses, whole file does not) | prowler (ASFF or OCSF records) |
Anything else raises InvalidArgumentsError asking for an explicit kind, one of
inventory, report, generic, prowler, scoutsuite, checkov, trivy. A Prowler output
directory needs kind="prowler"; its *.json files are read as ASFF or OCSF, record by
record. Scanner output is normalized (deduplicated and mapped to compliance frameworks) unless
normalise=False. Prowler's passing checks give PASS results for the ruleset controls that
only they cover. A
report that has graph but no edges gets its edges rebuilt from the D3 graph.
The default dataset name is the parent directory's name for inventory-map.json and the file
stem otherwise, made unique with -2, -3 and so on.
11.3 Allowed roots and path safety¶
Every path a tool reads (load_dataset, ingest_reports, the Checkov IaC directory) goes
through check_path: relative paths are taken relative to the first allowed root, the path is
resolved with symlinks followed, and it must end up inside one of the roots. Otherwise the
tool fails with AccessDeniedError (-31001), and the message lists the roots. Writes go
through output_path(subpath), which must stay under output_dir; .. escapes are refused
the same way.
cloudg.mcp.core.AccessDeniedError: Path .../docest/inventory/inventory-map.json is outside the
allowed roots (/run/media/morpheuslord/Personal_Files/Projects/cloudg,
/run/media/morpheuslord/Personal_Files/Projects/cloudg/reports). Ask the operator to add its
directory to the workspace's allowed roots.
The default roots are the process's current directory, which is wherever the client launched
the server, and the configured report directory. Either one is left out when it is / or the
home directory, so a client that starts the server from your home directory does not expose
all of it; with both left out the workspace refuses to start until roots are configured. Desktop clients do not let you choose it, so set the roots explicitly for them:
{
"mcpServers": {
"cloudg": {
"command": "cloudg-mcp",
"args": ["serve", "--policy", "standard"],
"env": {"CLOUDG_MCP_ALLOWED_ROOTS": "/srv/cloudg/reports:/srv/cloudg/scans"}
}
}
}
Use ; instead of : as the separator on Windows. Paths given with cloudg mcp serve
--dataset skip the check, because the operator chose them.
11.4 Snapshots and diffs¶
snapshot_dataset(new_name, dataset="") freezes a copy; after re-collecting or ingesting new
findings, diff_datasets(base, target="", limit=50) compares the two: assets added, removed
and changed (matched on ARN), edges added and removed, findings new, resolved or with changed
severity, and assets that became internet-exposed. The drift_review prompt wraps this.
11.5 Change events¶
Workspace changes reach the layer's listeners and, through the adapters, the clients. Loading
a dataset in the in-process example produced these (kind, uri) events:
[("resources", None),
("resource", "cloudg://workspace"),
("resource", "cloudg://datasets"),
("resource", "cloudg://datasets/prod/summary")]
select and in-place mutations send the three resource events without the list change;
remove sends the list change and the two workspace URIs.