-
Notifications
You must be signed in to change notification settings - Fork 36
feat(spec): add Deployment official extension (resolves #64) #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marianogonzalez
wants to merge
1
commit into
Agent-Card:main
Choose a base branch
from
marianogonzalez:feat/deployment-metadata-extension
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## 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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
I think on the concept of a default URL or something like that today in the ai-catalog-cli we have logic which is:
So you gracefully degrade from:
to
to
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-euyou can alway re-discover the same catalog entry.There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.