Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions adr/0022-deployment-metadata-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# ADR-0022: Deployment Metadata as an Official Extension

## Status
Accepted

## Date
2026-08-05

## Context
[Issue #64](https://github.com/Agent-Card/ai-catalog/issues/64)
observes that a Catalog Entry conflates *identity* ("what the agent is")
with *instance* ("where it lives"). The entry's `identifier` names a
single logical artifact, and its `url` names a single entry point — yet
the same logical agent is routinely deployed many times: across
environments (development, staging, production), across release channels
(stable, beta, LTS, edge), and across regions (us-east, eu-west) that
carry differing data-residency and compliance constraints.

Enterprise consumers need to discover and select among these deployments
deterministically: an EU client must reach an EU instance subject to
GDPR, a test harness must reach staging, and shadow-testing a pre-release
release channel must be possible — all while the artifact keeps a single
logical identity. The
core entry schema, which is intentionally closed (see
[ADR-0012](0012-extensibility-via-metadata.md)), has no place to express
this, and there is no interoperable convention for it today.

## Decision
Deployment metadata is defined as an **official extension**, registered
at the extension key
`https://ai-catalog.org/extensions/deployment`, and carried in the
entry's `extensions` map. It is **not** added as a native `instances`
field on the entry.

The extension value is a JSON object with a REQUIRED, non-empty
`instances` array. Each Instance object carries:

- `instanceId` (REQUIRED, string) — stable identifier, unique within the
entry.
- `url` (REQUIRED, string) — entry-point URL for this instance.
- `environment` (OPTIONAL, string) — e.g. `production`, `staging`,
`development`.
- `releaseChannel` (OPTIONAL, string) — release maturity track, orthogonal
to `environment`; e.g. `stable`, `beta`, `LTS`, `EDGE`.
- `region` (OPTIONAL, string) — e.g. `us-east-1`, `eu-west-1`.
- `dataResidency` (OPTIONAL, array of strings) — jurisdictions where data
is stored or processed.
- `compliance` (OPTIONAL, array of strings) — compliance regimes the
instance conforms to.
- `description` (OPTIONAL, string) — human-readable label.

The entry's top-level `url` remains the DEFAULT entry point for the
artifact, and one instance `url` SHOULD equal it. Each instance is an
alternative endpoint for the SAME logical artifact (same `identifier`);
instances are deployments, not distinct artifacts. A consumer that does
not understand the key MUST ignore it and fall back to the entry `url`; a
consumer that does understand it MAY select an instance matching its
policy and, if none matches, SHOULD NOT fall back to a non-conforming
instance. All identifiers use the `urn:air` format mandated by
[ADR-0015](0015-agent-identifier-naming.md).

## Rationale
- Keeps the core entry schema closed and stable, consistent with the
closed-core philosophy of
[ADR-0012](0012-extensibility-via-metadata.md); a not-yet-universal
need does not force a change to the core.
- Consumers that do not recognize the key ignore it safely and fall back
to the entry `url`, guaranteed by the existing `extensions` rule.
- The extension can evolve independently without requiring a major
version bump of the specification.
- Aligns with the precedent already set by the Metadata official
extension (`https://ai-catalog.org/extensions/metadata`).

## Alternatives Considered
- **Native `instances` field on the entry** — Rejected. It bloats the
closed core for a need that is not universal and reopens the schema
evolution problem ADR-0012 was written to avoid.
- **Separate curated catalogs per compliance regime** — Rejected as
inflexible (per the issue's own analysis): it multiplies catalogs,
duplicates identity, and cannot express a single logical artifact with
many deployments.
- **Freeform vendor metadata** — Rejected. Ad-hoc, per-vendor keys give
no interoperability, so no consumer could select instances
deterministically across producers.

## Consequences
- Instance selection becomes deterministic and interoperable across
producers and consumers.
- The metadata is advisory. It is unsigned unless carried in a Trust
Manifest's `extensions`, and MUST NOT be treated as a security control
on its own; `dataResidency` and `compliance` are producer assertions
that a consumer needing assurance verifies via the Trust Manifest
(attestations), not via this extension.
- The official extensions registry gains one entry:
`https://ai-catalog.org/extensions/deployment`.
145 changes: 145 additions & 0 deletions docs/examples/deployment-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Example: Deployment Metadata

A single logical agent often runs in more than one place: production and
staging, `us-east-1` and `eu-west-1`, with different data-residency and
compliance constraints in each region. AI Catalog keeps **identity** ("what
the agent is") separate from **instance** ("where it lives"): the entry has one
stable `identifier`, and the [Deployment official extension](../specification.md)
enumerates the concrete deployment instances behind it.

This lets a consumer deterministically pick an instance that satisfies its
policy — an EU client selecting the GDPR instance in `eu-west-1`, a test harness
targeting `staging`, or a client opting into a `beta` release channel — while
everything stays under one logical identity.

## The extension

The extension key (namespace) is:

```
https://ai-catalog.org/extensions/deployment
```

It lives inside an entry's `extensions` map. Its value is an object with a
required, non-empty `instances` array. Each instance describes one deployment of
the **same** logical artifact — instances are deployments, not distinct
artifacts.

!!! note "The entry `url` stays the default"
The entry's top-level `url` remains the default entry point. Each instance's
`url` is an alternative endpoint for the same `identifier`. One instance
`url` should equal the entry `url` so the default is represented among the
instances.

## Full example

```json
{
"identifier": "urn:air:acme-corp.com:agent:invoice-processor",
"type": "application/a2a-agent-card+json",
"url": "https://api.acme-corp.com/agents/invoice",
"tags": ["finance", "a2a"],
"extensions": {
"https://ai-catalog.org/extensions/deployment": {
"instances": [
{
"instanceId": "invoice-prod-us",
"environment": "production",
"url": "https://api.acme-corp.com/agents/invoice",
"region": "us-east-1"
},
{
"instanceId": "invoice-prod-eu",
"environment": "production",
"url": "https://eu-api.acme-corp.com/agents/invoice",
"region": "eu-west-1",
"dataResidency": ["EU"],
"compliance": ["GDPR"]
},
{
"instanceId": "invoice-staging",
"environment": "staging",
"url": "https://staging-api.acme-corp.com/agents/invoice",
"region": "us-east-1",
"releaseChannel": "beta"
}
]
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider using the ai-catalog spec itself to solve this problem for example:

{
  "identifier": "urn:air:acme-corp.com:agent:invoice-processor",
  "type": "application/ai-catalog+json",
  "tags": ["finance", "a2a"],
  "data": {
    "entries": [
      {
        "id": "urn:air:acme-corp.com:agent:invoice-processor-us",
        "type": "application/a2a-agent-card+json",
        "url": "https://api.acme-corp.com/agents/invoice",
        "extensions": {
          "https://ai-catalog.org/extensions/deployment": {
              "environment": "production",
              "region": "us-east-1"
          }
      },
      {
        "id": "urn:air:acme-corp.com:agent:invoice-processor-prod-eu",
        "type": "application/a2a-agent-card+json",
        "url": "https://eu-api.acme-corp.com/agents/invoice",
        "extensions": {
            "https://ai-catalog.org/extensions/deployment": {
                "region": "eu-west-1",
                "dataResidency": ["EU"],
                "compliance": ["GDPR"],
                "instanceId": "invoice-prod-eu",
                "environment": "production"
            }
        }
      },
      {
        "id": "urn:air:acme-corp.com:agent:invoice-processor-staging-eu",
        "type": "application/a2a-agent-card+json",
        "url": "https://staging-api.acme-corp.com/agents/invoice",
        "extensions": {
            "https://ai-catalog.org/extensions/deployment": {
                "instanceId": "invoice-staging",
                "environment": "staging",
                "region": "us-east-1",
                "releaseChannel": "beta"
            }
         }
      }
    ]
  }
}

I think on the concept of a default URL or something like that today in the ai-catalog-cli we have logic which is:

If ID resolves to type ai-catalog+json and the user hasn't requested a specific type:
   prompt the user to provide `--type` to give their preference 

if catalog contains more than one entry of --type:
	prompt the user with the list of available entries to pick from

So you gracefully degrade from:

ai-catalog-cli pull urn:air:acme-corp.com:agent:invoice-processor

to

ai-catalog-cli pull --type application/a2a-agent-card+json  urn:air:acme-corp.com:agent:invoice-processor

to

ai-catalog-cli pull urn:air:acme-corp.com:agent:invoice-processor-staging-eu

This removes any implict "default" and the client must pick explicitly.

If you want a "generic" or default then you could go for either adding a default entry to the catalog, or you could have a top level catalog which includes the "default" entries for different types and add a nested catalog for when you want to go to more specific endpoints.

The key point is that once a client has selected urn:air:acme-corp.com:agent:invoice-processor-staging-eu you can alway re-discover the same catalog entry.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your feedback @Tehsmash . In my original proposal, this was indeed part of the entry rather than an extension. At the time, I received feedback that it should be instead be modeled as an extension in order to keep the specification simple.

More than happy to switch back if that's the consensus. We must be careful not to introduce any backwards compatibility issues in the process though

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I described is still an extension, its just that it uses the catalog entries as the list, then added deployment metadata on each specific entry.

```

## Instance fields

Each object in the `instances` array supports these fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `instanceId` | string | Required | Stable identifier for this instance, unique within the entry |
| `url` | string | Required | Entry-point URL for this instance |
| `environment` | string | Optional | Deployment environment, e.g. `production`, `staging`, `development` |
| `releaseChannel` | string | Optional | Release maturity track, orthogonal to `environment`, e.g. `stable`, `beta`, `LTS`, `EDGE` |
| `region` | string | Optional | Deployment region, e.g. `us-east-1`, `eu-west-1` |
| `dataResidency` | string[] | Optional | Jurisdictions where data is stored or processed, e.g. `["EU"]`, `["US","CA"]` |
| `compliance` | string[] | Optional | Compliance regimes the instance conforms to, e.g. `["GDPR"]`, `["SOC2","HIPAA"]` |
| `description` | string | Optional | Human-readable label for the instance |

## Selecting an instance

A consumer that understands the extension may select an instance whose
`environment`, `region`, `dataResidency`, or `compliance` satisfies its policy.
If no instance matches, it **should not** fall back to a non-conforming instance.

```python
DEPLOYMENT_KEY = "https://ai-catalog.org/extensions/deployment"

def select_instance(entry, environment=None, release_channel=None,
region=None, compliance=None):
"""Pick a deployment instance matching the consumer's policy.

Returns None when no instance conforms — don't fall back
to a non-conforming instance (e.g. the entry url) in that case.
"""
ext = entry.get("extensions", {}).get(DEPLOYMENT_KEY)
if not ext:
# Extension unknown/absent: fall back to the entry's default url.
return {"url": entry["url"]}

for inst in ext["instances"]:
if environment and inst.get("environment") != environment:
continue
if release_channel and inst.get("releaseChannel") != release_channel:
continue
if region and inst.get("region") != region:
continue
if compliance and compliance not in inst.get("compliance", []):
continue
return inst

return None # no conforming instance — do not fall back
```

```python
entry = ... # the invoice-processor entry above

# EU client bound by GDPR: resolves to the eu-west-1 instance.
eu = select_instance(entry, region="eu-west-1", compliance="GDPR")

# Test harness: resolves to the staging instance.
staging = select_instance(entry, environment="staging")

# Client opting into the beta release channel: resolves to the beta instance.
beta = select_instance(entry, release_channel="beta")
```

!!! warning "Deployment metadata is not a trust control"
`dataResidency` and `compliance` are producer assertions, not signed
claims. A consumer that needs assurance verifies them via the entry's Trust
Manifest (attestations), not via this extension. A consumer that does not
understand the extension key ignores it and falls back to the entry `url`.

## Related

- [Full Specification](../specification.md)
- [Creating a Catalog](../guides/creating-a-catalog.md#the-deployment-extension)
- [Consuming Catalogs](../guides/consuming-catalogs.md#selecting-a-deployment-instance)
36 changes: 36 additions & 0 deletions docs/guides/consuming-catalogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,42 @@ def resolve_artifact(entry):

When fetching from `url`, the server should respond with the content type declared in the entry's `type` field.

## Selecting a deployment instance

An entry may carry the [Deployment extension](../examples/deployment-metadata.md)
in its `extensions` map, listing the concrete instances of one logical artifact
across environments, release channels, and regions. A consumer that understands
the extension may pick the instance matching its
environment/releaseChannel/region/compliance policy instead of the default entry
`url`:

```python
DEPLOYMENT_KEY = "https://ai-catalog.org/extensions/deployment"

def resolve_url(entry, region=None, compliance=None, release_channel=None):
ext = entry.get("extensions", {}).get(DEPLOYMENT_KEY)
if not ext:
return entry["url"] # extension unknown/absent — use default url

for inst in ext["instances"]:
if region and inst.get("region") != region:
continue
if compliance and compliance not in inst.get("compliance", []):
continue
if release_channel and inst.get("releaseChannel") != release_channel:
continue
return inst["url"]

return None # policy unmet — do not fall back to a non-conforming instance
```

- If you **don't** understand the extension key, ignore it and fall back to the
entry's `url`.
- If you **do** understand it but no instance satisfies your policy, do not
fall back to a non-conforming instance.
- `dataResidency` and `compliance` are producer assertions, not signed claims.
Verify them via the entry's `trustManifest` when you need assurance.

## Handling nested catalogs

An entry with `type: "application/ai-catalog+json"` is itself a catalog. To get all artifacts, recurse into it:
Expand Down
54 changes: 54 additions & 0 deletions docs/guides/creating-a-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,60 @@ Both the top-level catalog object and individual entries support a `metadata` fi

Metadata keys should use reverse-DNS prefixes for vendor-specific keys (`com.acme.*`), or short unqualified names for broadly useful keys (`license`, `homepage`). Avoid shadowing standard fields like `displayName` or `tags`. Clients that don't recognize a key should ignore it.

## The Deployment extension {#the-deployment-extension}

When one logical artifact runs in several places — production and staging,
`us-east-1` and `eu-west-1`, with differing data-residency and compliance
constraints — describe those deployments with the **Deployment** official
extension. The entry keeps a single stable `identifier`; the extension
enumerates the concrete instances behind it.

Official extensions live in the entry's `extensions` map, keyed by their
namespace URL. The Deployment extension uses:

```
https://ai-catalog.org/extensions/deployment
```

Its value has a required, non-empty `instances` array. Each instance requires an
`instanceId` (unique within the entry) and a `url`, plus optional `environment`,
`releaseChannel`, `region`, `dataResidency`, `compliance`, and `description`:

```json
{
"identifier": "urn:air:acme-corp.com:agent:invoice-processor",
"type": "application/a2a-agent-card+json",
"url": "https://api.acme-corp.com/agents/invoice",
"extensions": {
"https://ai-catalog.org/extensions/deployment": {
"instances": [
{
"instanceId": "invoice-prod-us",
"environment": "production",
"url": "https://api.acme-corp.com/agents/invoice",
"region": "us-east-1"
},
{
"instanceId": "invoice-prod-eu",
"environment": "production",
"url": "https://eu-api.acme-corp.com/agents/invoice",
"region": "eu-west-1",
"dataResidency": ["EU"],
"compliance": ["GDPR"]
}
]
}
}
}
```

The entry's top-level `url` stays the default entry point; each instance `url` is
an alternative endpoint for the same artifact. Represent that default among the
instances so one instance `url` equals the entry `url`.

See [Deployment Metadata](../examples/deployment-metadata.md) for the full
example and instance field reference.

## Complete example

```json
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ nav:
- Minimal Catalog: examples/minimal-catalog.md
- Multi-Protocol Agent: examples/multi-protocol-agent.md
- Nested Catalogs: examples/nested-catalogs.md
- Deployment Metadata: examples/deployment-metadata.md
- Implementations: implementations.md


Loading