Repository navigation
v0.6.0 - MCP Lib + Security
Two additions and a round of graph fixes. cloudg is now a Model Context Protocol server, so AI agents can query an inventory map, its graph, findings, compliance and ontology under an access and privacy policy. Every cloud API call now goes through a shared rate limiter that slows down when a provider throttles, so an organization-wide map (or an agent asking for one too often) no longer fails or hammers the APIs.
The release also changes what the graph analysis reports for the same input. Reachability follows network-flow edges only, matches ports as numbers and gives findings stable ids; the ontology emits different triples; GraphBuilder merges parallel rule edges; the inventory mapper keeps parallel rules when it deduplicates assets; cloudg.retry.with_retry retries a different set of errors. The modules concerned are public API: cloudg.graph.ports (new), cloudg.graph.builder, cloudg.graph.reachability, cloudg.graph.ontology_rules, the ontology and RAG exporters (cloudg.graph.ontology, cloudg.graph.rag_export), cloudg.inventory.dependencies, cloudg.inventory.mapper and cloudg.retry. Embedders that store reachability findings, count graph edges or query the ontology should read Changed and Fixed below.
Added:
- The MCP layer,
cloudg.mcp, built around one class,CloudGMCPLayer. Its default catalog has 74 tools in ten categories (graph 17, inventory 11, findings 10, workspace 8, ontology 6, compliance 5, live 5, privacy 5, meta 4, export 3), 13 resources, 11 resource templates and 11 prompts (security_posture_review,attack_surface_report,blast_radius_assessment,compliance_gap_analysis,incident_triageand six more). The CLI also listscloudg://metrics. Tools return structured content, resource links and progress notifications; lists are paginated and arguments complete. A list cursor belongs to the query and dataset version that produced it: once the dataset is reloaded or replaced, the old cursor is refused with an invalid-arguments error instead of returning the wrong page. - Graph analysis in the MCP catalog uses the same rules as the library. Attack paths and
find_pathsfollow the reachability network-flow hops plus explicit identity pivots (role assumption, IAM trust, grants);internet_exposureand the edge search read Azure*ports andInternet/Any/*sources, GCP all-port rules and AWS ICMP rules, and skip egress rules;find_paths(source="internet")works on an Azure-only dataset. Findings and asset references resolve throughAssetIndex, so an ambiguous name is never matched to the first asset that has it, andreachability_findingsreturns the analyser's deterministic ids. - Tool errors do not repeat the caller's value or list close matches in their message. Both are in the error data instead (
data.value,data.suggestions), where the privacy policy can transform them. - A dependency-free native server (
cloudg.mcp.native): stdio, streamable HTTP and the deprecated SSE endpoints, bearer tokens mapped to roles,OriginandHostchecks against loopback by default, CORS,/healthz. It serves both protocol eras on the same object: the handshake versions 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and the stateless 2026-07-28 (server/discover,subscriptions/listen).HTTPConfigcaps sessions per principal (max_sessions_per_principal, 100; at the cap the principal's least recently used idle session is closed) and open connections (max_connections, 512), and gives a client 30 seconds to send its headers (header_timeout) and 120 seconds for the body (body_timeout). - Flavors and adapters:
--flavor native,sdk(the official SDK's low-level server),fastmcporauto.register_into(server)mounts the whole catalog into an mcp 2.xMCPServer, an mcp 1.xFastMCP, a low-levelServeror afastmcp.FastMCP(2.x, 3.x, 4.x) next to the host's own tools;build_server()andexport_definitions()cover the other cases. Verified with mcp 1.30.0 and 2.3.0 and fastmcp 2.14.7, 3.4.8 and 4.0.11. On the SDK and fastmcp HTTP transports a session id is bound to the principal that opened it. - Options objects for embedders:
LayerOptions(CloudGMCPLayer(options=...)) andServeOptions(serve_async(options=...)). The existing keyword arguments still work; an unknown keyword raisesTypeError. - A tool handler can take a single pydantic arguments model,
def tool(ctx, args: Model), instead of one parameter per argument. The input schema and the validation are the same as for the flat form. - Content
_metakeyscloudg/dataandcloudg/render: a resource or prompt that renders structured data as text passes the data itself, so the privacy transforms see it with its keys before it is rendered. - Policies: seven built-in profiles (
open,standard(default),strict,read_only,airgapped,audit,soc-analyst) plus policy files that extend each other, with per-role rules, sensitivity ceilings, capability denials and per-principal rate limits. Hidden primitives look the same as nonexistent ones. The textual graph and ontology exports (cloudg://graph/{format},cloudg://ontology/{format},cloudg://ontology/turtle),sparql_queryandsubgraph_exportare RESTRICTED, because the privacy layer cannot see inside one opaque string; a profile with aconfidentialceiling hides them (strict, andsoc-analystfor every role exceptlead). - Transforms, composed per principal into input and output pipelines:
redact,sanitize(fences attacker-controllable text),project,annotate,substitute,alias,regex_replace,rename_keys,template,guard_secrets(refuses secrets in arguments) anddepseudonymize, with built-in detectors for secrets and tokens, private keys, cloud credential ids, ARNs and resource ids, account ids, IP addresses, hostnames and e-mail addresses. TokenVault: format-preserving pseudonyms derived with HMAC-SHA256, stable across restarts withCLOUDG_MCP_VAULT_KEY, and saved when the server shuts down when the policy setsvault.path. A pseudonym an agent passes back as an argument is reversed before the tool runs;reveal_tokenreverses pseudonyms and the aliases introduced bysubstituteandaliastransforms.- Middleware:
AuditLogMiddleware(JSONL, argument values hashed),MetricsMiddleware(cloudg://metrics),CachingMiddleware,ConcurrencyLimitMiddleware,RetryMiddleware, and your ownasync (info, call_next)callables. - CLI:
cloudg mcp serve | tools | resources | prompts | call | read | config, also installed as thecloudg-mcpconsole script and runnable aspython -m cloudg.mcp.cloudg mcp configprints client configuration for Claude Desktop, Claude Code, Cursor and VS Code. Undercloudg mcp, the banner and UI output go to stderr so stdout stays a clean protocol channel. - The MCP workspace reads only files under its allowed roots. With none configured (
allowed_rootsorCLOUDG_MCP_ALLOWED_ROOTS) they are the working directory and the report directory, and either one is left out when it is/or the home directory. A load error never quotes the file's contents, and the search for report files in a directory is bounded. SPARQL queries stop at the row limit or after 60 seconds, and a slow analysis build on one dataset no longer blocks the others or the workspace status. - Live tools (
map_inventory,collect_assets,run_scanners,run_pipeline) check the configured credentials before any cloud call (preflight, on by default, at most 25 seconds) and run under a live operation guard: identical concurrent calls share one run, a scope collected in the last 120 seconds is refused withretry_after_secondsand a loaded live dataset that covers the same providers, and live operations are capped at one per scope and two overall. A live operation that runs longer thanratelimit.live_operation_timeout_seconds(default 3600) is cancelled, raisesLiveOperationTimeoutand frees its scopes.force=trueskips the cooldown for theadminandoperatorroles. Each live tool reports the throttling of its own run.run_scannersrefuses container image names that start with-. The newrate_limit_statustool andcloudg://ratelimitresource show the guard and throttling state without calling a cloud API. cloudg.resilience: provider-aware error classification (classify,classify_strictfor codes, classes and statuses without message text,is_provider_answer,retry_after,describe_error), adaptive token buckets per provider, account, region and service (per action for EC2) that halve on throttling and recover additively, bulkheads, circuit breakers at the granularity of the rate bucket (per action for EC2, per service elsewhere),call_with_resilience/call_with_resilience_sync/@resilientwith decorrelated-jitter backoff that never undercuts Retry-After, a per-call deadline and a per-provider retry budget, per-run telemetry (stats_scope,ResilienceStats), the process-wide governor (get_governor,configure,reset_governor) andLiveOperationGuardwith itsGuardSettings. The first two retries of every call do not draw on the shared retry budget, so one busy run cannot starve every other call of its retries. A half-open breaker lets one probe through and takes the probe back after one cooldown if it never reports; a provider's non-retryable answer (anAccessDenied, a 404) counts as proof that the service is reachable. Calls refused by an open circuit, and calls given up because the retry budget ran out, do not count as breaker failures. Reconfiguring the limits keeps the breakers, budgets and held concurrency slots of providers whose settings did not change.- Provider wiring, with rate limits, concurrency caps, breakers and the shared retry budget enforced in every SDK: botocore event hooks on every AWS session cloudg creates (the collectors' aioboto3 sessions, the base and assumed-role sessions behind STS, Organizations and Control Tower discovery and the organization fan-out); an explicit azure-core
RetryPolicyand a per-attempt policy on every Azure client, which also reads thex-ms-ratelimit-remaining-subscription-readsheader (pausing the whole subscription) and Resource Graph'sx-ms-user-quota-*headers; Resource Graph paging that honours Retry-After and quota resets, without retrying a page again on top of azure-core's own 429 retries; a google-api-coreRetrycapped per page, and per-page pacing, for Cloud Asset Inventory. An instrumented AWS client that refuses a call because a circuit is open or the retry budget is spent raises an error that is also a botocoreClientErrorwith codeThrottling, so existingexcept ClientErrorhandlers catch it. A cancelled aioboto3 call frees its concurrency slot at once. ratelimitconfig section (RateLimitConfig,ProviderRateLimitConfig,ServiceRateLimitConfig): master and per-provider switches, rates, bursts, account and provider-wide limits, concurrency, adaptive floor, breaker threshold and cooldown, retries, backoff cap, deadline, retry budget, per-service and per-operation overrides, and thelive_*settings of the guard. Unknown keys are logged and ignored.config.yamlships the section with comments.InventoryResult.throttling: the run's throttling summary (totals, per-scope counters, messages, skipped scopes, tripped breakers, slowed buckets), orNonewhen no API pushed back. It is added tosummaryandinventory-map.jsononly when present, andload()reads it back.MultiAccountCollector.resilience_statsholds the last run's stats.cloudg.graph.ports: the port and edge rules that the graph builder, the reachability analysis and the ontology share.parse_port_ranges(),edge_port_ranges()andport_in_ranges()turnport_rangestrings into numeric ranges;is_internet_source()is the single check for a source that stands for the internet (0.0.0.0/0,::/0, and the Azure tagsInternet,Anyand*in any letter case).- Network-flow traversal as public API:
ReachabilityAnalyzer.flow_hops(),flow_successors()andinternet_entry_points(), andcloudg.graph.reachability.network_flow_graph(), so other code walks the network the way the exposure analysis does.finding_id()andasset_key()give the id scheme of reachability findings. cloudg.inventory.dependencies.AssetIndex, the one asset matcher: asset id, ARN or alias, unique name, unique ARN tail, plus two opt-in tiers (arn_partsfor ARN resource parts such asdb:orders,casefoldfor names in any letter case).AssetIndex.add()takes extraaliases.DependencyGraph.find, the ontology and the RAG exporter use it.RelationType.CONTAINS, a generic containment relation in theCONTAINMENTgroup, for 64 relation types in all.
Changed:
aws.max_retriesandaws.retry_modeare honoured. Earlier releases accepted them and then hard-coded adaptive mode with 10 retries, so the defaults behave as before. botocore countsmax_retriesafter the first attempt: the default of 10 allows up to 11 attempts per call.- Every cloud API call is paced by default at or below the provider's published limits: AWS 20 rps per service and per EC2 action, Route 53 5, tagging API 5, Azure 20 rps per subscription and 3 rps per user for Resource Graph, GCP 1.5 rps for
ListAssets. Per-item fan-out is held to those rates, so maps of AMI-heavy or bucket-heavy accounts can take longer than in 0.5.x.ratelimit.enabled: falseturns the pacing off. - A service that stays throttled after its retries is recorded in coverage as FAILED or PARTIAL with an error starting
throttled:instead of failing the run. AWS collectors that swallowed per-item errors used to report SUCCESS with missing assets when they were throttled; they now report PARTIAL, telling calls that gave up after retries apart from calls an open circuit skipped. cloudg.retry.with_retrykeeps its signature and still retries everything it retried in 0.5.x. It now also retries provider throttling identified by error code, exception class or HTTP status (Azure 429s, GCPResourceExhausted), and never sleeps less than the server's Retry-After, capped at itsmax_delay. It does not retry cloudg's own resilience errors (an open circuit, a spent retry budget, a missed deadline), a plain HTTP 500 or an error whose message only mentions rate limiting.cloudg.retryre-exports the maincloudg.resiliencenames.- New optional extra
mcp(mcp>=1.30,<3).allnow includes it, and so doesdev. The core layer and its native server need no extra. - The wheel ships
docs/DOCUMENTATION.mdanddocs/INVENTORY_REFERENCE.mdascloudg/_docs/, so the MCPcloudg://docs/{topic}resources work from a plainpip install. The sdist includes everydocs/*.md. ReachabilityAnalyzer.find_internet_exposed()walks network-flow edges only, sogenerate_findings()no longer reports resources that are reachable only through an IAM grant. Before, the walk followed every edge type out of an internet-exposed node, includingGRANTS_ACCESS,ASSUMES_ROLE,IAM_TRUSTandIAM_POLICY_ATTACHMENT, and produced findings such as a CRITICAL "Internet-exposed RDS_INSTANCE" for a database whose only link to the internet was a role granted access to it. The rules, documented in the module docstring:INTERNET_EXPOSED,LOAD_BALANCER_TARGET,ROUTEandPEERINGare followed source to target.SECURITY_GROUP_RULEandNACL_RULEare followed source to target, except egress rules (direction: "egress").ATTACHED_TOis followed backwards when the target is a security group, NSG or NACL, or the edge declaresPROTECTED_BY_SG/PROTECTED_BY_NACL(traffic a group admits reaches the resources attached to it), and forwards when the source is aNETWORK_INTERFACEorELASTIC_IP. Other attachments are not followed.CONTAINSis followed only from aVPC,VNETorSUBNET. Organization, account, cluster and namespace containment is not.INVOKES,REFERENCES,USES_IMAGE,LOGS_TO,PROTECTS,MONITORS,MANAGESandGOVERNSare not followed. A Lambda function behind a public API Gateway (anINVOKESedge) is therefore no longer reported as internet-exposed by this analysis.compute_blast_radius()is unchanged and still follows every edge type.
- Internet entry points are nodes named
0.0.0.0/0or::/0and the source of any non-egress edge whose CIDR is0.0.0.0/0,::/0or an Azure service tag for the internet (Internet,Any,*). Before, every external placeholder node counted as an entry point, including private CIDRs such as10.0.0.0/8and unresolved references, and a security group with an egress rule to0.0.0.0/0became an entry point itself. - Security groups, NSGs, NACLs and target groups are no longer reported as "unexpected internet-exposed resources". The walk still passes through them and marks them
is_internet_exposed; a group's open rules are reported by the sensitive-port findings, and a target group's exposure by the findings on the targets behind it. - Reachability finding ids are deterministic: a UUID5 hash of the rule and a stable asset key (
cloudg.graph.reachability.finding_id(rule, asset_key)) instead of a randomuuid4. The key is the asset's ARN, or its asset id when it has no ARN, because collectors give assets a fresh random id on every collection. Exposure findings key on the asset, open-port findings on the keys of the rule's two endpoints and the port, so the same exposure gets the same id on every scan.generate_findings()also returns exposure findings in a stable order. - Ontology:
CONTAINSedges get a relation only when both endpoint types match it:VPC_CONTAINS_SUBNET(VPC or VNet to subnet),SUBNET_CONTAINS_INSTANCE(subnet to a resource placed in it, not a route table, NACL, gateway or other network construct),CLUSTER_CONTAINS_SERVICE(cluster or Kubernetes namespace to a workload, service, namespace, node group or add-on) andORG_CONTAINS_ACCOUNT(organization or OU to account). Every other containment (organization to OU, account to VPC, VNet to VM, resource group to resource, unresolved endpoints) uses the new genericRelationType.CONTAINS. Before, these fell back toVPC_CONTAINS_SUBNETand produced triples such assample-org VPC_CONTAINS_SUBNET Workloads. - Ontology: a relationship declared on an edge (
NetworkEdge.relationship) replaces the inferred relation onCONTAINS,ATTACHED_TOand the typed inventory edges (INVOKES,USES_IMAGE,ASSUMES_ROLE,GRANTS_ACCESS,LOGS_TO,PROTECTS,MONITORS,MANAGES,GOVERNS,REFERENCES). OnSECURITY_GROUP_RULE,IAM_TRUST,IAM_POLICY_ATTACHMENT,LOAD_BALANCER_TARGET,ROUTE,PEERINGandINTERNET_EXPOSEDedges the declared relation comes first and the inferred ones are added after it. - Ontology:
SECURITY_GROUP_RULEandNACL_RULEedges no longer yieldPROTECTED_BY_SG/PROTECTED_BY_NACL. Those edges point from the traffic source to the group, so they produced triples such as0.0.0.0/0 PROTECTED_BY_SG sg-web. The protection relations now come fromATTACHED_TOedges whose target is a security group or NSG (PROTECTED_BY_SG) or a NACL (PROTECTED_BY_NACL), so the triple readsweb-alb PROTECTED_BY_SG sg-web. OtherATTACHED_TOedges without a declared relationship stayDEPENDS_ON. - Ontology: the port relations of a security group rule are read from the parsed port ranges, as the sensitive-port findings are. A rule open on every port (
0-65535, protocolALL/-1/*, or an Azure*rule forTcp) isALL_TRAFFIC; otherwise each of ports 22, 80, 443 and 3389 inside the ranges givesONLY_SSH,ONLY_HTTP,ONLY_HTTPSorONLY_RDP(an AWS rule for1-1024gives the first three), and a rule that opens none of them isPORT_RESTRICTED. A rule from an AzureInternet,Anyor*source isINTERNET_REACHABLE, like one from0.0.0.0/0. - Ontology:
ENCRYPTED_BY_KMSnow links an encrypted asset to its key:orders-db ENCRYPTED_BY_KMS <key>. Before,infer_relations()added it to every edge whose target hadencryptionorstorage_encryptedplus akms_key_idin its metadata, soCloudOntologywrote triples with the edge's source as the subject and the encrypted asset as the object, such asec2-1 ENCRYPTED_BY_KMS rds-1orsubnet-private ENCRYPTED_BY_KMS orders-db. The relation now comes frominfer_asset_relations()for the encrypted asset itself. Itskms_key_idis matched against the collectedKMS_KEYassets by asset id, ARN, bare key id, alias name or alias ARN. The keys are indexed once per ontology build instead of being scanned for every encrypted asset. If no collected key matches, the object is the rawkms_key_idstring.infer_relations()no longer returnsENCRYPTED_BY_KMSunless the edge declares it as itsrelationship(the linker'sREFERENCESedges still do). - Ontology: containment inferred from
vpc_idmetadata points from the VPC to its members. Before,infer_asset_relations()returned(VPC_CONTAINS_SUBNET, vpc)for a subnet and(SUBNET_CONTAINS_INSTANCE, vpc)for anything else in the VPC, which gavesubnet VPC_CONTAINS_SUBNET vpcandinstance SUBNET_CONTAINS_INSTANCE vpc: the wrong direction, and a subnet relation whose object was a VPC. Now a VPC or VNet asset returns(VPC_CONTAINS_SUBNET, subnet)for each subnet and(CONTAINS, member)for every other asset with the samevpc_id, and member assets return no containment. The return shape ofinfer_asset_relations()is unchanged: the asset passed in is still the subject of every tuple. If several VPC or VNet assets share avpc_id, the first one in the list claims the members.CloudOntology.query_asset_neighbourhood()withhops > 1follows these relations forward, so a walk from a VPC now reaches its members, and a walk from an instance no longer reaches the VPC through them. - RAG export: relation-group chunks include the asset-level relations (
ENCRYPTED_BY_KMS,vpc_idcontainment, tag governance, key rotation, EC2 security group membership) next to the edge relations. The per-edgeENCRYPTED_BY_KMSentries described above are gone. GraphBuildermerges parallel rule edges (see Fixed), sograph.number_of_edges()and the edge lists into_d3_json(),to_cytoscape_json()and GraphML exports shrink by the number of merged rules, and a merged edge'sport_range,protocolanddescriptioncan hold comma- or semicolon-separated lists.
Fixed:
ReachabilityAnalyzerdecided whether a sensitive port was open with a substring test on the edge'sport_rangestring. A rule open on0-65535, which is how the AWS collector writes protocol-1(all traffic), produced no SSH or RDP finding, and neither did an Azure NSG rule with*or a comma list such as443,3380-3390. The same test reported port 22 for2200-2300and port 3389 for33890. The string is now parsed into numeric ranges bycloudg.graph.portsand each of the 11 ports inSENSITIVE_PORTSis tested against those ranges. The module readsa-b, single ports, comma lists and*. An emptyport_rangeon aSECURITY_GROUP_RULEorNACL_RULEedge means every port when the protocol isALL/-1/*/Any, or TCP, UDP or SCTP (a GCP firewall protocol with no ports listed). On other edge types an empty string still opens nothing, so a GCPINTERNET_EXPOSEDedge for a public bucket gets no port findings. ICMP, ICMPv6, ESP, AH, GRE and IPIP rules open no ports; for ICMP the numbers AWS stores in the port fields are a type and code. Expect more open-port findings on estates with all-traffic or wide-range ingress rules from the internet, and fewer on rules whose range only looked like a sensitive port. An edge still yields at most one finding per port, however many of its ranges overlap that port. The analyzer also reads aportslist from a graph edge when one is present, as before;GraphBuilderstill does not copyNetworkEdge.portsonto the graph, since the collectors cap that list at 100 entries and GraphML export cannot store lists.- Egress rules (
direction: "egress") no longer produce open-port findings. They point from a group to0.0.0.0/0and admit nothing inbound. Before this change they were checked like ingress rules and only escaped because the default AWS egress string0-65535contains none of the sensitive port numbers as a substring; with numeric matching every security group's default egress rule would otherwise report all 11 ports. - Azure NSG rules whose source is the
Internet,Anyor*service tag now produce open-port findings, and the finding title names the tag. Before, they marked resources internet-exposed but no port finding was ever raised for them. GraphBuilderlost rules that shared a source and target. The graph is annx.DiGraph, which holds one edge per pair, so the secondNetworkEdgefor a pair overwrote the first one's attributes. A security group with ingress rules from0.0.0.0/0for port 22 and port 443 ended up with a single edge for 443, andReachabilityAnalyzernever reported SSH open to the internet. An ICMP rule from0.0.0.0/0listed after the SSH rule hid it the same way. AWS security groups, Azure NSG rules and GCP firewall rules all produce such pairs.SECURITY_GROUP_RULE,NACL_RULEandINTERNET_EXPOSEDedges on the same pair are now merged into one graph edge when their CIDR and direction match:port_rangelists the ports of every merged rule, comma-separated and without repeats: rules for 22 and 443 give"22,443".- Rules for different protocols are merged too, and
protocollists each one once ("TCP,ICMP"). ICMP, ESP, AH, GRE and IPIP rules add no ports, because the numbers AWS stores for ICMP are a type and code, and Azure writes"*"for an ICMP rule. A TCP, UDP or all-protocol filter rule with no ports (GCP writes these) adds0-65535. With mixed protocols the ports are a union: the edge shows which ports are open from the source, not which protocol each port is open for. Sensitive-port findings never looked at the protocol, so they come out the same as if each rule had its own edge. descriptionjoins the rules' descriptions with"; ".- Rules whose CIDR or direction differ, and edges of any other type, still replace the earlier edge on the pair.
- The inventory mapper's deduplication dropped parallel security group, NACL and internet-exposure rules from
inventory-map.jsonwhenever it merged a duplicate asset. Rules that differ in ports, protocol, CIDR or direction are now kept. - The ontology and RAG exporters attached findings to assets by raw
Finding.resource_idonly. Findings whoseresource_idis an ARN or a display name, which is common in scanner output, left a dangling resource node (for examplecmr:arn:aws:rds:...) with noFINDING_AFFECTSedge to the real asset, and RAG entity and community chunks reported afinding_countof 0 for it. Both exporters now resolve a finding's asset withAssetIndex: by id, then ARN (fromresource_idorresource_arn), then unique name, then unique ARN tail, falling back to the rawresource_idonly when nothing matches. Ambiguous names never match.DependencyGraph.findbehaves as before apart from building its lookup tables once instead of scanning every asset on each call, and an empty reference now returnsNone. - AWS
SECURITY_GROUP_RULEedges fromcollect_edges()now use the security group asset'sidas the security group endpoint instead of the nativesg-...ID, as Azure NSG rule edges already did. The graph exports no longer contain a disconnectedEXTERNALnode per security group, and sensitive-port findings carry the security group's asset ID and ARN. The internet-exposure walk now continues from an open security group to the resources attached to it, so an instance behind a group that admits0.0.0.0/0is reported as internet-exposed; before, the walk stopped at the native-ID placeholder. The CIDR endpoint is unchanged. - The multi-account collector called STS (
AssumeRole,GetCallerIdentity) synchronously on the event loop, which stalled every other collection while it waited. Those calls now run in a worker thread.
Security:
- The MCP layer never echoes a real value that it restored from a pseudonym in the arguments. It records each restored value with its pseudonym, and error messages and error data, results, resource links, progress messages and log notifications are pseudonymised again with those pairs before they leave, for tools, resources, prompts and completions alike. Text payloads (JSON strings, GraphML, N-Triples) are pseudonymised consistently with structured results, using the values the vault already knows (
TokenVault.replace_known_values). - Progress messages, log notifications, resource-link names, titles and descriptions, and the URIs of embedded resources go through the caller's privacy policy.
- An unexpected exception in a resource or prompt handler reaches the client as a generic error on every flavor, never as the raw exception text.
- Percent-encoded and templated resource URIs are put in canonical form before the policy's allow and deny rules are checked, so
cloudg://graph/%64%33is treated ascloudg://graph/d3. - DNS rebinding: on a non-loopback bind without
--allowed-host, the native HTTP server no longer trusts anOriginjust because it equals theHostheader. Same-origin requests are accepted only when theHostheader is validated. cloudg mcp serverefuses an empty--auth-tokenorCLOUDG_MCP_AUTH_TOKENS.--registryaccepts only amodule:attributedotted name that resolves to aRegistry; it loads operator code and must never come from an untrusted source.- Session caps, connection caps and read timeouts on the native HTTP server, and session ids bound to the principal on the SDK and fastmcp HTTP transports (see Added).
- Policy allow and deny lists and rules also match the concrete resource URI that was requested, both as sent and percent-decoded.
preview_transformpseudonymises with a separate vault under a random key (Policy.preview_vault()), so a preview cannot confirm which real value is behind a pseudonym. Thestrictprofile hides the tool.privacy_audit_logreturns only the caller's own entries unless the caller has theadminorprivacy-adminrole, and its result says which (scope:ownorall).- Audit log lines escape CR, LF and other control characters, and a probe for an unknown resource is recorded with a keyed hash of the URI's variable part instead of the value.
- An invalid policy is reported without echoing the values in it, so a misplaced vault key does not end up in an error message.
- Vault files are written atomically with fsync, with mode 0600 in directories created with mode 0700.
- Regular expressions that could backtrack for a long time on hostile input were replaced with linear patterns or small parsers: the vault's token scanner, the detectors for GCP resource names, e-mail addresses, hostnames and AWS secret access keys, the prompt-injection pattern, the RAG member and triple parsers, fence stripping, the
explain_*metadata helpers and the literal stripping insparql_query(a 20,000-character query used to take 23 seconds there; 200,000 characters now take 0.06). - Over HTTP with OAuth, the scopes of the access token become the principal's roles as they are, so a token with the scope
adminorprivacy-admingets what the policy grants those roles, reveal included. Issue such scopes only to people who may see real identifiers, or pass aprincipal_resolverthat maps scopes onto roles.
Notes:
- Existing imports, signatures and config files keep working, and a config without
ratelimitgets the built-in limits. The release is not purely additive, though: the reachability, ontology, graph builder, mapper andwith_retrychanges under Changed and Fixed alter results for the same input. Reachability finding ids, open-port findings, ontology triples, graph edge counts and the rules kept ininventory-map.jsonall differ from 0.5.x. - Several large modules were split into packages or sibling modules (
cloudg.mcp.stateandcloudg.mcp.policyare packages, the transforms are spread over smaller modules,cloudg.apimoved its scanner runs tocloudg.api_scanners, the linker rules moved tocloudg.inventory.linker_rules), and long functions across the CLI, the collectors, the resilience layer and the MCP server were split up. The documented import paths, public signatures and CLI options are unchanged. The GCP default skip list is nowcloudg.config.GCP_DEFAULT_SKIP_ASSET_TYPES;cloudg.collectors.gcp.DEFAULT_SKIP_ASSET_TYPESstays as an alias. - The
sanitizetransform is about three times faster on large results, and the redactor no longer scans the same value twice. - The
mcpextra allowsmcp>=1.30,<3, verified with mcp 1.30.0 and 2.3.0. Mounting into an SDKMCPServer(mcp 2.x) orFastMCP(mcp 1.x) goes through private SDK attributes that a future release can rename, so applications using that path should pin the minor version they tested, for examplemcp>=2.3,<2.4. Mounting into the low-levelServerand the native server are unaffected. - Sessions on the legacy SSE transport of the SDK and fastmcp flavors are not bound to a principal.
Documentation:
- MCP guide: installation, client setup, CLI, transports and protocol versions, flavors, mounting, in-process use, workspace, errors, middleware, live tools, troubleshooting.
- MCP tool reference: every tool, resource, template and prompt.
- MCP privacy: profiles, policy schema, transforms, detectors, the token vault.
- MCP internals: package map, core contracts, the layer, the adapters, the native server.
- Rate limits and throttling: architecture, provider wiring, built-in limits with sources, every
ratelimitkey, a throttled run's output, the Python API, tuning, limitations. - The feature reference and the handbook have new MCP server and rate limit chapters, the
ratelimitconfiguration and the new Python API. The inventory reference, catalog and internals describe merged rule edges, the port and protocol formats of each provider and the new reachability rules.
Full Changelog: v0.5.3...v0.6.0