Metadata-Version: 2.4
Name: maf-sandbox-azure-pricing
Version: 0.1.1
Summary: Azure retail pricing in disposable allowlisted sandboxes.
Keywords: azure,pricing,sandbox,agent-framework
Author: SOKOLAI BV
Author-email: SOKOLAI BV <info@sokolai.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Requires-Dist: maf-sandbox>=0.52.0,<0.53
Requires-Dist: agent-framework-core>=1.23.0,<1.24
Requires-Python: >=3.12, <3.15
Project-URL: Homepage, https://www.sokol.ai
Project-URL: Source, https://github.com/sokolaidev/maf-extensions
Project-URL: Issues, https://github.com/sokolaidev/maf-extensions/issues
Description-Content-Type: text/markdown

# maf-sandbox-azure-pricing

[![PyPI](https://img.shields.io/pypi/v/maf-sandbox-azure-pricing)](https://pypi.org/project/maf-sandbox-azure-pricing/) [![Python](https://img.shields.io/pypi/pyversions/maf-sandbox-azure-pricing)](https://pypi.org/project/maf-sandbox-azure-pricing/) [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/sokolaidev/maf-extensions/blob/main/LICENSE)

> **Experimental.** Importing this package emits `MafSandboxAzurePricingExperimentalWarning`. Its API may change or be removed without notice. This package is not yet released.

Look up Azure public retail prices with one fixed `azmcp pricing get` execution in a fresh sandbox per call. The kind accepts typed filters, builds argv without a shell, validates the JSON envelope and price fields, and disposes the sandbox. It reads and lands no files and receives no Azure credentials.

## Host setup

Call `make_azure_pricing_tools(router, agent_id, context, image=...)` with a configured sandbox router and caller context. It returns the `azure_pricing_get` tool, or an empty list when the router is disabled or absent. The image must provide `/usr/local/bin/azmcp` with the Azure MCP `4.0.0-beta.4` pricing contract. See the [image recipe and live check](https://github.com/sokolaidev/maf-extensions/blob/main/images/azure-pricing-sandbox/README.md).

The spec requires POSIX, at least container isolation, `EXEC`, per-call isolation and disposal. Egress is fixed to GET requests to `prices.azure.com`; the policy derives the `EGRESS_METHODS` requirement. Backends unable to enforce it refuse attachment. Bounded execution is also mandatory: a backend without the optional `BoundedExec` surface is refused before executing the CLI. Docker needs the current packaged egress proxy. Its injected `SSL_CERT_FILE` must be honored; certificate verification is never disabled.

Azure MCP 4.0.0-beta.4 does not accept `--read-only` on `pricing get`. The kind fixes the subcommand and enforces GET-only egress instead. The host cannot supply command names, endpoint overrides or a wider allowlist. Enable network use by attaching this tool only where the host permits that public destination. Host confidentiality policy still governs outgoing query arguments; GET-only access does not prevent data disclosure in a query.

For public-only queries, pass `outbound_max_confidentiality="public"` and run the framework's label-tracking middleware followed by `PolicyEnforcementFunctionMiddleware(block_on_violation=True)`. The policy blocks private context or arguments before sandbox acquisition, including when the host trusts the pricing source's integrity. The cap defaults to `None`, following core's opt-in host classification contract; without enforcing middleware, the declaration alone does not block egress. A direct `tool.func` call bypasses framework middleware and is the host's responsibility.

## Arguments and results

Arguments are `sku`, `service`, `region`, `service_family`, `price_type`, `currency` (default `USD`), `include_savings_plan` (default false), and `filter` (raw OData). At least one filter is required; `currency` and `include_savings_plan` do not count. A `service` query also requires `sku`. `PriceType` accepts `Consumption`, `Reservation` and `DevTestConsumption`. Raw filters are parenthesized before the CLI combines them with the typed filters. Empty, oversized and control-bearing arguments are refused before sandbox acquisition.

The tool's parameter schema describes the price-list fields and their exact, case-sensitive values:

| Parameter | Price-list field | Example |
|---|---|---|
| `sku` | `armSkuName` | `Standard_D4s_v5` |
| `service` | `serviceName` | `Virtual Machines` |
| `region` | `armRegionName` | `westeurope` |
| `service_family` | `serviceFamily` | `Compute` |
| `price_type` | `priceType` | `Consumption` |

Many network and platform meters have an empty `armSkuName`; do not substitute a product name or invent a SKU. The pinned CLI rejects its named `service` argument without `sku`, even when a raw filter is also supplied. The tool refuses that combination before acquiring a sandbox and explains the alternative: omit both `service` and `sku`, and use `filter` on `serviceName`, `productName` or `meterName` instead. For Private Endpoint pricing, use `region="Global"` with `filter="serviceName eq 'Virtual Network' and productName eq 'Virtual Network Private Link' and meterName eq 'Standard Private Endpoint'"`; those rows have an empty ARM SKU.

For raw OData, [Microsoft's API filter reference](https://learn.microsoft.com/en-us/rest/api/cost-management/retail-prices/azure-retail-prices#api-filters) lists `armRegionName`, `location`, `meterId`, `meterName`, `productId`, `skuId`, `productName`, `skuName`, `serviceName`, `serviceId`, `serviceFamily`, `priceType` and `armSkuName`; `reservationTerm` also works. `contains(meterName, 'Spot')` supports substring queries. There is no operating-system field such as `osType`: Windows VM meters have `Windows` at the end of `productName`. `skuName` is a price-list value, not a portal display name. Unknown fields fail the whole lookup. Recognizable upstream HTTP 400 failures return fixed filter-correction guidance without echoing CLI text or arguments; raw predicates are not parsed or field-validated on the host.

Some meters use a billing zone (`Zone 1` through `Zone 8`, `US Gov Zone 1` or `US Gov Zone 2`), `Global`, or an empty `armRegionName`. An empty regional query does not establish that no price exists. Retry without `region`, or use a raw predicate such as `armRegionName eq 'Zone 1'`, `armRegionName eq 'Global'` or `armRegionName eq ''` while retaining another narrowing filter. The `region` argument itself cannot be an empty string.

The host selects `exec_timeout_seconds` (default 60), `max_output_bytes` (default 8 MiB for combined stdout/stderr) and `max_rows` (default 100). Responses must be successful, clean UTF-8 JSON with valid pricing values; duplicate keys, non-finite prices, malformed rows, transport errors and stderr fail closed. An empty price array is a completed lookup. Errors do not echo CLI text or query arguments.

Projected JSON includes `prices`, `returned_rows`, `received_rows`, `truncated` and `upstream_limit_reached`. The CLI retrieves at most 5,000 rows before the host applies its row cap. Reaching that ceiling marks the result potentially truncated, even when all received rows fit. Narrow the query; the tool has no continuation token and does not compute totals over incomplete results.

Keep reservation terms with their prices: `retailPrice` covers the whole reservation term even when `unitOfMeasure` says `1 Hour`. Savings plans retain their nested term and prices. These are public retail prices, not negotiated account prices. Azure MCP omits `tierMinimumUnits`, so the result cannot establish usage-tier thresholds.

## Integrity

Price data is returned through `SandboxResult.output` with an untrusted workload declaration. The host may pass `egress_integrity=SourceIntegrity.TRUSTED` to vouch for the fixed Microsoft pricing source. Core promotes the data only when the framework also establishes trusted conversation and argument labels. Without either piece of evidence it stays untrusted. The host's result confidentiality is preserved.

That host trust decision covers every retained field, including product, service and meter names; it is not proof that external text is free of instructions. Leave egress integrity unset when the host cannot vouch for those fields. Fixed standing guidance tells consumers to treat all report fields as data, never instructions or authority to call tools or change policy. This guidance does not replace the host's trust decision or enforce confidentiality.

Only fixed completion, failure and truncation messages and standing pricing guidance use the trusted part of the result. Parsing JSON alone does not establish trust. See the [information-flow contract](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/information-flow.md).
