Live¶
map_inventory, collect_assets, run_scanners and run_pipeline are the only tools that reach outside the server (openWorldHint: true). The fifth tool in this category, rate_limit_status, only reports the guard and throttling state. The other four call provider APIs with the credentials configured on the server (cloud_access) and, for scanners, start external binaries (exec). They run for minutes, report progress per phase, and put their result in the workspace as a new active dataset; the tool result is a summary plus resource links, never the whole inventory. The engine is looked up as cloudg.api.CloudGEngine at call time, so tests and embedders can replace it.
The order of checks in every live call is: arguments, then the result dataset's name (format, and no clash unless replace), then force permission, then the credential preflight, then the live operation guard, and only then the engine. Everything that can be refused is refused before a single cloud API call.
Each call works on a deep copy of the workspace configuration, so providers, regions and services overrides never change the server's config. providers must be a subset of aws, azure, gcp; regions replaces the region list of every selected provider.
Credential preflight¶
Before any API call, map_inventory, collect_assets and run_pipeline (and run_scanners when Prowler or ScoutSuite is among the enabled scanners) check the configured credentials locally, unless preflight is false. For AWS: explicit access keys pass; a role ARN with a web identity token file passes when the file exists; otherwise the boto3 default chain (profile, environment, instance role) must yield credentials. For Azure, one token is requested for https://management.azure.com/.default. For GCP, the credentials must load. The whole check is bounded at 25 seconds. A failure ends the call before any collector starts, with the reason per provider in error_data.providers. The real result of the AWS check with an empty AWS config:
{
"tool": "map_inventory",
"arguments": {
"providers": [
"aws"
]
},
"isError": true,
"text": "Cannot collect live data. aws: no credentials found (set aws.profile / access keys in the cloudg config, AWS_PROFILE / AWS_ACCESS_KEY_ID in the server's environment, or run on a host with an instance role). Fix the server's credentials, choose other providers, or work offline with load_dataset. Pass preflight=false to skip this check.",
"_meta": {
"cloudg/error_code": -32603,
"cloudg/error_data": {
"providers": {
"aws": "no credentials found (set aws.profile / access keys in the cloudg config, AWS_PROFILE / AWS_ACCESS_KEY_ID in the server's environment, or run on a host with an instance role)"
}
}
}
}
When the check does not finish in time the message is Credential check did not finish within 25s; the cloud identity endpoints look unreachable. Pass preflight=false to try anyway.
Live operation guard¶
An agent can ask for a collection far more often than a person running cloudg map. Every live call therefore goes through the workspace's LiveOperationGuard (cloudg/resilience/guard.py, reachable as Workspace.live_guard and replaceable there). The guard works on scopes: one per configured account, profile, subscription or project (aws/123456789012, aws/profile:prod, gcp/org:42), or the bare provider name when the config names none. In the sample run the config named no AWS account, so the scope was aws.
- Single-flight: a call identical to one already running (same operation, providers, scopes and collection arguments such as
regionsorservices; the datasetnameis not part of the comparison) waits for it and returns the same result with"joined": trueand anote. Only one engine runs. - Timeout: an operation still running after
operation_timeout_seconds(ratelimit.live_operation_timeout_seconds, default 3600) is cancelled and its scopes are freed; the caller gets reasontimeout. An operation whose callers have all gone (each cancelled by the client or a tool timeout) is cancelled too. - Concurrency caps: at most
max_concurrent_per_scope(default 1) live operations per scope andmax_concurrent_total(default 2) overall. A different operation on a busy scope is refused with reasonbusy. - Cooldown: after an operation on a scope finishes (successfully or not), a new one on that scope is refused for
cooldown_seconds(default 120, overridable per provider) with reasoncooldown. - Caller quota: with
caller_max_operationsset, a caller gets at most that many new live operations percaller_window_seconds(reasonquota). It is off by default.
The defaults come from the ratelimit section of the cloudg config (live_cooldown_seconds, live_max_concurrent, live_max_concurrent_total, live_caller_max_operations, live_caller_window_seconds, live_operation_timeout_seconds, and ratelimit.<provider>.live_cooldown_seconds). run_scanners always runs under the guard, so identical scans share one run, but it claims the cloud scopes (and their cooldowns) only when Prowler or ScoutSuite runs; the offline scanners have no scope.
A refusal is a tool error with code -31029. error_data carries reason, message, retry_after_seconds, scopes and caller (each only when set), and use_dataset: the most recent live dataset already loaded whose providers include every provider of the refused call, which the message tells the caller to query instead. A second map_inventory on the same scope right after the first:
{
"tool": "map_inventory",
"arguments": {
"providers": [
"aws"
],
"name": "live-aws-2"
},
"isError": true,
"text": "live collection of aws ran recently; cooling down, retry in 120s or use the cached dataset. The last live result is already loaded as dataset 'live-aws'; query it instead of collecting again. An admin or operator can pass force=true to skip the cooldown.",
"_meta": {
"cloudg/error_code": -31029,
"cloudg/error_data": {
"reason": "cooldown",
"message": "live collection of aws ran recently; cooling down, retry in 120s or use the cached dataset",
"retry_after_seconds": 120.0,
"scopes": [
"aws"
],
"caller": "local",
"use_dataset": "live-aws"
}
}
}
force: true skips the cooldown, but only for a principal with the admin or operator role. Anyone else gets an access error, raised before the preflight:
{
"tool": "map_inventory",
"arguments": {
"providers": [
"aws"
],
"name": "live-aws-2",
"force": true
},
"isError": true,
"text": "force=true (skip the live cooldown) needs the admin or operator role. Use the dataset from the last collection, or wait for the cooldown to end.",
"_meta": {
"cloudg/error_code": -31001
}
}
With the admin or operator role the same call skips the cooldown and runs. Two map_inventory(providers=["gcp"]) calls started 50 ms apart, the first with name: "shared" and the second without a name, produced one engine run. The second call got the first call's result, dataset shared included, plus the join marker:
Called with the arguments below, map_inventory returned this structuredContent.
{
"dataset": "shared",
"active": true,
"tool": "map_inventory",
"duration_s": 0.3,
"summary": {
"total_assets": 44,
"total_edges": 56,
"total_findings": 0,
"open_findings": 0,
"severity_breakdown": {},
"accounts": 5,
"regions": 5,
"internet_exposed": 6,
"cross_account_edges": 3,
"assets_by_type": {
"SECURITY_GROUP": 4,
"IAM_ROLE": 4,
"CLOUD_ACCOUNT": 3,
"EC2": 3,
"S3_BUCKET": 3,
"...": "10 more"
},
"providers": [
"aws",
"... 2 more"
]
},
"collection_failures": [
{
"service": "rds",
"error": "AccessDenied: rds:DescribeDBInstances"
}
],
"errors": [
"aws:rds: AccessDenied"
],
"next_steps": [
"dataset_summary",
"... 2 more"
],
"joined": true,
"note": "An identical live operation was already running; this call shares its result instead of collecting again."
}
While that operation held the gcp scope, collect_assets(providers=["gcp"]) was refused as busy:
{
"tool": "collect_assets",
"arguments": {
"providers": [
"gcp"
],
"name": "busy-try"
},
"isError": true,
"text": "a live operation is already running for gcp; retry when it finishes or use the cached dataset. The last live result is already loaded as dataset 'live-aws'; query it instead of collecting again.",
"_meta": {
"cloudg/error_code": -31029,
"cloudg/error_data": {
"reason": "busy",
"message": "a live operation is already running for gcp; retry when it finishes or use the cached dataset",
"scopes": [
"gcp"
],
"caller": "local",
"use_dataset": "live-aws"
}
}
}
rate_limit_status (and the cloudg://ratelimit resource) shows the scopes cooling down, so a caller can check before asking.
Progress¶
The phase hook of the engine is wired to MCP progress notifications. progress is the phase number from this table, capped just below total, and the call ends with progress == total and the message done:
| Phase | Step |
|---|---|
inventory_mapping, collection |
1 |
scanning, ingest |
2 |
analysis |
3 |
normalisation |
4 |
reporting |
5 |
total is 2 for map_inventory and collect_assets, 3 for run_scanners and 6 for run_pipeline. map_inventory also sends 0.5 with mapping inventory before the engine starts. Engine errors reported through the engine's error hook are collected and returned in errors as phase: message strings.
What the result looks like¶
map_inventory, collect_assets and run_pipeline return: dataset (the new name), active: true, tool, duration_s, summary (total_assets, total_edges, total_findings, open_findings, severity_breakdown, accounts, regions, internet_exposed, cross_account_edges, assets_by_type, providers), the tool-specific fields below, and next_steps. They link cloudg://datasets/{name}/summary (priority 1.0) and cloudg://workspace.
Without name, the dataset is called <prefix>-<YYYYmmdd-HHMMSS> (inventory-, collect-, pipeline-). An explicit name is checked before anything else runs: a malformed name, or one already loaded without replace: true, fails without creating an engine. In the capture run neither of these calls created one:
{
"tool": "map_inventory",
"arguments": {
"name": "my dataset"
},
"isError": true,
"text": "Invalid dataset name: use 1-64 letters, digits, '.', '_' or '-'.",
"_meta": {
"cloudg/error_code": -32602,
"cloudg/error_data": {
"value": "my dataset"
}
}
}
{
"tool": "map_inventory",
"arguments": {
"providers": [
"aws"
],
"name": "sample",
"force": true
},
"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": "sample",
"suggested": "sample-2"
}
}
}
map_inventory¶
Category live · sensitivity confidential · capabilities cloud_access, read_state, write_state · readOnly false, destructive false, idempotent false, openWorld true · outputSchema no · timeout 3600 s
The complete live inventory with read-only API calls: deep collectors, catch-all sweeps, relationship linking, and organization discovery across accounts when configured. No scanners. Adds collection_failures (first 20 failed services from the coverage records) and errors.
| Argument | Type | Default | Constraints | Description |
|---|---|---|---|---|
providers |
string[] or null | null |
Subset of aws, azure, gcp; empty = the configured providers. | |
regions |
string[] or null | null |
Regions to collect for every selected provider; empty = configured regions. | |
services |
string[] or null | null |
Service families: all, network, compute, containers, kubernetes, serverless, integration, data, storage, identity, security, dns, iac, logging. | |
tagging_sweep |
boolean or null | null |
AWS Resource Groups Tagging API sweep; default from config. | |
name |
string | "" |
Dataset name for the result; default ' |
|
replace |
boolean | false |
Overwrite an existing dataset with the same name (otherwise a name clash is an error, reported before any cloud call). | |
preflight |
boolean | true |
Check the configured credentials before calling any cloud API, so a missing profile fails in seconds instead of after every collector has retried. | |
force |
boolean | false |
Skip the live cooldown for this call. Only honoured for callers with the admin or operator role; others get an access error. |
The examples in this section all ran against the stubbed engine described in How the examples were produced.
Called with the arguments below, map_inventory returned this structuredContent.
{
"dataset": "live-aws",
"active": true,
"tool": "map_inventory",
"duration_s": 0.0,
"summary": {
"total_assets": 44,
"total_edges": 56,
"total_findings": 0,
"open_findings": 0,
"severity_breakdown": {},
"accounts": 5,
"regions": 5,
"internet_exposed": 6,
"cross_account_edges": 3,
"assets_by_type": {
"SECURITY_GROUP": 4,
"IAM_ROLE": 4,
"CLOUD_ACCOUNT": 3,
"EC2": 3,
"S3_BUCKET": 3,
"...": "10 more"
},
"providers": [
"aws",
"azure",
"... 1 more"
]
},
"collection_failures": [
{
"service": "rds",
"error": "AccessDenied: rds:DescribeDBInstances"
}
],
"errors": [
"aws:rds: AccessDenied"
],
"next_steps": [
"dataset_summary",
"find_assets",
"... 2 more"
]
}
The result also carried these resource links in content:
[
{
"type": "resource_link",
"uri": "cloudg://datasets/live-aws/summary",
"name": "live-aws summary",
"title": "Dataset summary",
"mimeType": "application/json",
"annotations": {
"priority": 1.0
}
},
{
"type": "resource_link",
"uri": "cloudg://workspace",
"name": "workspace",
"mimeType": "application/json"
}
]
The progress notifications received during the call (progress, total, message) were:
[
[
0.5,
2.0,
"mapping inventory"
],
[
1.0,
2.0,
"phase: inventory_mapping"
],
[
2.0,
2.0,
"done"
]
]
{
"tool": "map_inventory",
"arguments": {
"providers": [
"oracle"
]
},
"isError": true,
"text": "Unknown provider(s) oracle. Use aws, azure, gcp.",
"_meta": {
"cloudg/error_code": -32602
}
}
collect_assets¶
Category live · sensitivity confidential · capabilities cloud_access, read_state, write_state · readOnly false, destructive false, idempotent false, openWorld true · outputSchema no · timeout 3600 s
The standard multi-provider collection that cloudg collect runs (core services, network edges, IAM). Lighter than map_inventory. Adds errors.
| Argument | Type | Default | Constraints | Description |
|---|---|---|---|---|
providers |
string[] or null | null |
Subset of aws, azure, gcp; empty = the configured providers. | |
regions |
string[] or null | null |
Regions to collect for every selected provider; empty = configured regions. | |
name |
string | "" |
Dataset name for the result; default ' |
|
replace |
boolean | false |
Overwrite an existing dataset with the same name (otherwise a name clash is an error, reported before any cloud call). | |
preflight |
boolean | true |
Check the configured credentials before calling any cloud API, so a missing profile fails in seconds instead of after every collector has retried. | |
force |
boolean | false |
Skip the live cooldown for this call. Only honoured for callers with the admin or operator role; others get an access error. |
Called with the arguments below, collect_assets returned this structuredContent.
{
"dataset": "collected",
"active": true,
"tool": "collect_assets",
"duration_s": 0.0,
"summary": {
"total_assets": 44,
"total_edges": 56,
"total_findings": 0,
"open_findings": 0,
"severity_breakdown": {},
"accounts": 5,
"regions": 5,
"internet_exposed": 6,
"cross_account_edges": 3,
"assets_by_type": {
"SECURITY_GROUP": 4,
"IAM_ROLE": 4,
"CLOUD_ACCOUNT": 3,
"EC2": 3,
"S3_BUCKET": 3,
"...": "10 more"
},
"providers": [
"aws",
"gcp"
]
},
"errors": [],
"next_steps": [
"dataset_summary",
"find_assets",
"... 2 more"
]
}
The result also carried these resource links in content:
[
{
"type": "resource_link",
"uri": "cloudg://datasets/collected/summary",
"name": "collected summary",
"title": "Dataset summary",
"mimeType": "application/json",
"annotations": {
"priority": 1.0
}
},
{
"type": "resource_link",
"uri": "cloudg://workspace",
"name": "workspace",
"mimeType": "application/json"
}
]
The progress notifications received during the call (progress, total, message) were:
run_scanners¶
Category live · sensitivity confidential · capabilities cloud_access, exec, read_fs, read_state, write_fs, write_state · readOnly false, destructive false, idempotent false, openWorld true · outputSchema no · timeout 7200 s
Runs security scanners plus cloudg's reachability analysis against an existing dataset's assets and adds the findings to it. Scanner output files go to <output_dir>/scans. iac_dir (for Checkov) must be inside an allowed root. Only Prowler and ScoutSuite need cloud credentials, so the preflight runs only when one of them is selected; Checkov, Trivy and the IAM linter run offline. With normalise the dataset's findings are deduplicated and compliance-mapped afterwards (merging the compliance results as normalise_findings does). Container image names that start with - are refused, so an image argument can never be read as a scanner option. Unlike the other three it does not create a dataset.
| Argument | Type | Default | Constraints | Description |
|---|---|---|---|---|
scanners |
string[] or null | null |
Subset of prowler, scoutsuite, checkov, trivy, iam; empty = configured. | |
iac_dir |
string | "" |
IaC directory for Checkov (inside an allowed root). | |
images |
string[] or null | null |
Container images for Trivy. | |
normalise |
boolean | true |
Deduplicate and compliance-map the dataset's findings after the scan. | |
dataset |
string | "" |
Dataset name (see list_datasets). Empty = the active dataset. | |
preflight |
boolean | true |
Check the configured credentials before calling any cloud API, so a missing profile fails in seconds instead of after every collector has retried. | |
force |
boolean | false |
Skip the live cooldown for this call. Only honoured for callers with the admin or operator role; others get an access error. |
Returns dataset, scanners (the enabled list), new_findings, severity_breakdown of the new findings, findings_before, findings_after, output_dir, duration_s, errors and next_steps, and links the dataset summary and the workspace. The stubbed engine's scan returns one HIGH finding:
Called with the arguments below, run_scanners returned this structuredContent.
{
"dataset": "sample",
"scanners": [
"prowler",
"iam"
],
"new_findings": 1,
"severity_breakdown": {
"HIGH": 1
},
"findings_before": 12,
"findings_after": 13,
"output_dir": "<estate>/out/scans",
"duration_s": 1.2,
"errors": [],
"next_steps": [
"top_risks",
"list_findings(min_severity='HIGH')",
"compliance_summary"
]
}
The result also carried these resource links in content:
[
{
"type": "resource_link",
"uri": "cloudg://datasets/sample/summary",
"name": "sample summary",
"title": "Dataset summary",
"mimeType": "application/json",
"annotations": {
"priority": 1.0
}
},
{
"type": "resource_link",
"uri": "cloudg://workspace",
"name": "workspace",
"mimeType": "application/json"
}
]
The progress notifications received during the call (progress, total, message) were:
{
"tool": "run_scanners",
"arguments": {
"scanners": [
"nmap"
]
},
"isError": true,
"text": "Unknown scanner(s) nmap. Use: prowler, scoutsuite, checkov, trivy, iam",
"_meta": {
"cloudg/error_code": -32602
}
}
{
"tool": "run_scanners",
"arguments": {
"iac_dir": "/etc"
},
"isError": true,
"text": "Path /etc is outside the allowed roots (<estate>). Ask the operator to add its directory to the workspace's allowed roots.",
"_meta": {
"cloudg/error_code": -31001
}
}
run_pipeline¶
Category live · sensitivity confidential · capabilities cloud_access, exec, read_state, write_fs, write_state · readOnly false, destructive false, idempotent false, openWorld true · outputSchema no · timeout 7200 s
The whole cloudg pipeline in one call: collect, scan, analyse (graph, ontology, RAG, Terraform), normalise, then JSON and HTML reports under <output_dir>/<subdir>. Adds report_paths, attack_paths_found and errors (pipeline errors plus hook errors).
| Argument | Type | Default | Constraints | Description |
|---|---|---|---|---|
providers |
string[] or null | null |
Subset of aws, azure, gcp; empty = the configured providers. | |
regions |
string[] or null | null |
Regions to collect for every selected provider; empty = configured regions. | |
subdir |
string | "pipeline" |
pattern ^[A-Za-z0-9_.\-/]{0,200}$ |
Output sub-directory for reports. |
name |
string | "" |
Dataset name for the result; default ' |
|
replace |
boolean | false |
Overwrite an existing dataset with the same name (otherwise a name clash is an error, reported before any cloud call). | |
preflight |
boolean | true |
Check the configured credentials before calling any cloud API, so a missing profile fails in seconds instead of after every collector has retried. | |
force |
boolean | false |
Skip the live cooldown for this call. Only honoured for callers with the admin or operator role; others get an access error. |
Called with the arguments below, run_pipeline returned this structuredContent.
{
"dataset": "full",
"active": true,
"tool": "run_pipeline",
"duration_s": 0.0,
"summary": {
"total_assets": 44,
"total_edges": 56,
"total_findings": 12,
"open_findings": 11,
"severity_breakdown": {
"CRITICAL": 2,
"HIGH": 5,
"MEDIUM": 2,
"LOW": 1,
"INFO": 1
},
"accounts": 5,
"regions": 5,
"internet_exposed": 6,
"cross_account_edges": 3,
"assets_by_type": {
"SECURITY_GROUP": 4,
"IAM_ROLE": 4,
"CLOUD_ACCOUNT": 3,
"EC2": 3,
"S3_BUCKET": 3,
"...": "10 more"
},
"providers": [
"aws"
]
},
"report_paths": {
"json": "<estate>/out/pipeline/findings.json"
},
"attack_paths_found": 1,
"errors": [],
"next_steps": [
"dataset_summary",
"find_assets",
"... 2 more"
]
}
The result also carried these resource links in content:
[
{
"type": "resource_link",
"uri": "cloudg://datasets/full/summary",
"name": "full summary",
"title": "Dataset summary",
"mimeType": "application/json",
"annotations": {
"priority": 1.0
}
},
{
"type": "resource_link",
"uri": "cloudg://workspace",
"name": "workspace",
"mimeType": "application/json"
}
]
The progress notifications received during the call (progress, total, message) were:
[
[
1.0,
6.0,
"phase: collection"
],
[
2.0,
6.0,
"phase: scanning"
],
[
3.0,
6.0,
"phase: analysis"
],
"... 3 more"
]
{
"tool": "run_pipeline",
"arguments": {
"subdir": "../../x"
},
"isError": true,
"text": "Output path ../../x escapes the output directory <estate>/out.",
"_meta": {
"cloudg/error_code": -31001
}
}
rate_limit_status¶
Category live · sensitivity internal · capabilities read_state · readOnly true, destructive false, idempotent true, openWorld false · outputSchema no · timeout: layer default (300 s)
The live guard and cloud API throttling state, without calling any cloud API. live_guard has in_flight, active_total, active_by_scope, cooling_down (scope to seconds left), started and joined counters, and the settings (cooldown_seconds, provider_cooldowns, max_concurrent_per_scope, max_concurrent_total, caller_max_operations). throttling is the summary of cloudg's throttling governor (cloudg.resilience): call, throttle, retry and give-up totals, recent messages, skipped services, per-scope figures, tripped circuit breakers and slowed rate buckets. The same object is served as cloudg://ratelimit. The tool is in the live category, so exclude_categories=["live"] removes it too, but it needs only read_state, so profiles that deny cloud access keep it.
Arguments: none.
Right after the first map_inventory of the capture run:
Called with the arguments below, rate_limit_status returned this structuredContent.
{
"live_guard": {
"in_flight": 0,
"active_total": 0,
"active_by_scope": {},
"cooling_down": {
"aws": 120.0
},
"started": 1,
"joined": 0,
"cooldown_seconds": 120.0,
"provider_cooldowns": {},
"max_concurrent_per_scope": 1,
"max_concurrent_total": 2,
"caller_max_operations": 0
},
"throttling": {
"totals": {
"calls": 0,
"throttled": 0,
"transient_errors": 0,
"retries": 0,
"gave_up": 0,
"rejected": 0,
"breaker_trips": 0,
"wait_seconds": 0
},
"messages": [],
"skipped": {},
"scopes": {},
"breakers": {},
"slowed_buckets": {}
}
}