Skip to content

Releases: affinidi-ai/agent-gateway

v1.0.5

Choose a tag to compare

@affinidi-semantic-release affinidi-semantic-release released this 03 Nov 17:51
325fe31

Changelog

Breaking

  • Refuse new http token endpoints when saving a credential provider — Creating a credential provider, changing its token endpoint, or turning an API key provider into an OAuth type now answers 400 "token_endpoint must be an https URL" unless the endpoint is https or listed exactly in credential_policy insecure_token_endpoints, and the Validate check refuses such an endpoint without contacting it. Saving a stored provider that keeps its http endpoint still works while credential_policy token_endpoint_https is warn. Upgrade note: provisioning that creates or changes a provider with an internal http:// token endpoint now gets 400; use https, or list the exact URL in credential_policy.insecure_token_endpoints.
  • Refuse new credential provider references to another tenant's secrets — Creating a credential provider, or changing a secret reference, now answers 400 "Secret reference is not accessible" unless the secret belongs to the provider's own tenant; an appliance-wide secret no longer counts for a tenant provider. Validate applies the same rule to a signing key or certificate. While credential_policy tenant_credential_isolation is warn, a stored reference is kept only in the field that holds it and while the provider's type, token and authorization endpoints, resource, additional parameters, default scopes and JWT issuer, subject and audience are unchanged, so a tenant cannot redirect an appliance-wide secret or signing key to its own endpoint. A provider with an issuer never keeps a secret outside its own tenant, even in warn mode. Upgrade note: provisioning that gives a tenant provider an appliance-wide or another tenant's secret now gets 400; create the secret in the tenant first.
  • Refuse new appliance-wide credentials on tenant surfaces when saving — Saving a tenant surface that newly names an appliance-wide secret for static secret Target authentication, or an appliance-wide API key, client credentials or JWT bearer provider, is refused with 400, and a tenant reassignment that would create such a reference answers 409. While credential_policy tenant_credential_isolation is warn, a stored appliance-wide reference is kept only at the destination that already received it: the endpoint of its Target or Transit Point, or the caller for response metadata. A reference to a secret that does not exist is refused even when stored, and a tenant provider that holds a secret outside its tenant is refused on a new or redirected leg. Upgrade note: provisioning that names an appliance-wide secret or provider on a tenant surface, or adds a tenant provider holding an outside secret to a new leg, now gets 400, and a tenant reassignment that would create such a reference gets 409; move the credential into the tenant first.
  • Refuse every save of a tenant surface that names a missing secret — A tenant surface whose stored custom metadata names a $SECRET: with no existing secret now answers 400 "Secret reference is not accessible" on every save, in warn mode too, even when the edit leaves the metadata alone. Upgrade note: delete or fix the reference, or create the secret in the surface's tenant, before saving such a surface.
  • Refuse tenant A2A Proxies whose Direct Line secret is outside their tenant — Creating a tenant A2A Proxy, or changing its secret or base URL, with an appliance-wide or another tenant's Direct Line secret now answers 400 "A2A proxy secret is not accessible", in warn mode too. Upgrade note: create the Direct Line secret in the proxy's tenant, or publish the proxy appliance-wide as an operator.
  • Refuse appliance-wide secrets in a tenant token's MCP tool discovery — POST /v1/mcp-proxies/discover-tools with a tenant token and an appliance-wide secret now answers 400 "Target auth secret is not accessible", whatever tenant_credential_isolation says. Upgrade note: run discovery with a secret of the token's own tenant, or with an operator session or appliance-wide token.
  • Fail closed when an inbound From API Key caller does not use an API key — An inbound From API Key Identity whose first Caller Context method is not API Key or API Key Provider gave callers the stored key's DID; it now returns 500 with a fixed body. Find such surfaces through agent_gateway_managed_identity_resolve_total with mode="from_api_key" and result api_key_unavailable. Saving one, on the surface or an enabled variant, is refused with 400, as is combining it with mcp_http.authorization, or a From API Key Identity on the protected, external or a Transit Point slot without a key id; a stored one answers a fixed 500 (result api_key_id_missing). A missing VC issuer or a failed DID issuance still resolves no caller identity and leaves the decision to policy.
  • Give each API key its own DID, so rules tied to the shared DID stop matching — On an API Key Provider surface, only callers presenting the stored key keep its DID; every other key gets its own. On a plain API Key surface, callers get the DID of the Caller Context secret id. That DID is new on every surface made in the dashboard, whose picker stored a provider key id, never a secret id. Policies, Trust Check entries and Transit Point OAuth grants naming an old DID stop matching, and the Transit token sub and Workload Binding userIdentity.id are per key. Re-grant or update rules for the affected keys.
  • Refuse From API Key on an MCP proxy:// surface when it is saved again — The MCP Proxy request path never resolves the caller's key, so saving an MCP surface with a proxy:// target and an inbound From API Key Identity is refused with 400. A stored surface keeps loading and serves its callers with no caller identity, and it cannot be saved until its inbound Identity changes.
  • Drop downgrade support for surfaces saved without an inbound key id — A surface saved with an inbound From API Key Identity has no stored key id, which older binaries require, so they skip it at load. Downgrading after re-saving such a surface is not supported.
  • Refuse variant Transit Points that change the surface's gating or policies — A variant's Transit Point is enforced with the mcp_tool_gating, policy and response_policy of the surface's Transit Point of the same alias. The admin API now refuses with 400 a variant Transit Point that sets any of them to something else, or sets one where the surface has no Transit Point of that alias. A stored variant whose Transit Point sets a different one has every request through it refused with 403, instead of silently running the surface Transit Point's. A variant Transit Point that leaves them out keeps the surface Transit Point's. This replaces the earlier handling of gating set only on a variant's Transit Point, which refused every tools/call and listed no tools. Move any variant-only gating or policy to the surface's Transit Point.
  • Refuse a tool call whose elicit consent is declined or cannot be asked — A tool call whose elicit binding was declined, cancelled or not answered within elicit_timeout_secs, and under elicit_fallback: fail one from a client without the elicitation capability or without an open SSE stream, was forwarded without the delegated credential. It is now refused as a credential that cannot be resolved: an Access Point answers 503 with a JSON-RPC error (-32603). A client must accept the prompt to have the call forwarded; use elicit_fallback: on_demand to answer a client that cannot be prompted with the consent link instead.
  • Reject a listener listener_type other than inbound or outbound — A listener whose listener_type was anything else, Outbound or egress included, was accepted and then served as an inbound listener on some paths and ignored on others. It now stops the gateway from loading its network configuration. Write inbound or outbound, in lower case, before upgrading; leaving the field out still means inbound.
  • Require a transit token for discovery documents below a Transit Point's root — A Transit Point skipped the transit token, and withheld the target_auth and delegated credentials, for any GET whose path ended like a discovery document, such as admin/discovery or tools/agent.json, and for spellings a target could resolve to another path. Only a GET or HEAD of .well-known/agent-card.json, .well-known/agent.json, .well-known/agent-card, .well-known/ucp, agent-card.json, agent.json or discovery directly below the Transit Point now counts. Any other path needs a transit token and carries the credentials, so a managed agent that fetched such a path without a token must now send one.

Security

  • Check a DID Auth session against the allow-list on every request — A DID Auth session authenticates only at the view it was minted on, the surface and the variant whose /authenticate minted it or the base surface, and every request it carries is checked again against that view's allowed_dids as they are then. A DID removed from the list, or left out of a list added after its session was minted, is refused at once rather than when the session expires, and a session minted on a variant is not taken at the base surface or another variant. session_ttl_seconds is capped at 86400, the default, and no session authenticates more than 86400 seconds after it was minted, whatever it was stored with. A save that sets a value above 86400 is refused; a stored surface above it still loads, with a startup warning, so an upgrade never stops the gateway, and can still be saved while it keeps that value.
  • Show dashboard log lines only to callers with the logs.view permission — The dashboard delta and stats endpoints and the dashboard WebSocket returned gateway log lines to every signed-in caller, including roles and access tokens without logs.view. The endpoints now leave log lines out, without reading the log file, unless the caller's role and access-token scopes grant logs.view,...
Read more

v1.0.4

Choose a tag to compare

@affinidi-semantic-release affinidi-semantic-release released this 02 Nov 18:01
5454241

Changelog

Breaking

  • Refuse to delete a credential provider that a surface still binds — Deleting a credential provider that a surface binds, in its base or a variant, through outbound_credentials or a Transit Point's transit_credentials, or uses for the credential_provider target authentication of its Target or a Transit Point, now answers 409 provider_in_use instead of 204, which left every later call through the binding refused; clients that delete providers must remove the bindings first. A provider delete is serialised with surface writes, so no write binds the provider while it is being deleted. The dashboard shows the reason and the surface. Only surfaces that may reference the provider are considered, and the answer names the surface when the caller may see it. Deleting a provider with an issuer deletes each of its grants and the provider, then revokes each grant at the authorization server's RFC 7009 revocation endpoint, the refresh token and then the access token, at most eight grants at a time, each within 30 seconds, through pinned Strict https egress, with the client authenticating as its token_endpoint_auth_method says. The revocations run after the delete answers and after the provider lock is released, so a slow vendor holds up no surface write. Revocation is best effort and audited as token_revoked with its outcome and the caller that deleted the provider; the grant is deleted whatever the outcome. Only that provider's grants are read: the provider is kept, with 503 naming the grant, while one of them cannot be read, for example because at-rest encryption is off, and another provider's unreadable grant never blocks the delete.
  • Connect the vendor calls of external consent directly, whatever proxy is set — Reading an external consent provider's authorization server metadata, revoking its grants, and forwarding a request that carries a grant by binding now ignore HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and the system proxy and connect directly, since a proxy would resolve the vendor's host again and connect to an address nobody vetted. On a network whose egress goes only through a proxy, allow direct egress to those vendors, or external consent fails to connect; the gateway warns at startup when a proxy variable is set. Every other outbound call uses a configured proxy as before.
  • Refuse to move a bound credential provider to another tenant — PUT /v1/tenant-ownership/credential-providers/{id} now answers 409 for a provider that any surface binds, of any tenant, in its base or a variant, through outbound_credentials or a Transit Point's transit_credentials, or uses for target authentication, instead of moving it: its bindings would name another tenant's provider, and moving it back would serve their grants again. Remove the bindings first. A move is serialised with surface writes, and a write that binds a provider moved to a tenant its surface may not reference since the write checked its references is refused, as one that binds a deleted provider is.
  • Require a verified caller for credentials on Fabric receive — A partner's legacy request to a Target with delegated credential bindings was keyed by the caller identity the partner named. Now a service-level binding (api_key and client credentials providers) refuses the request with 403 unless the receiving surface's own caller authentication verified a caller on it, such as the key the partner's target_auth sends, and a per-user (oauth2_authorization_code) binding always refuses it. Partners that relied on the identity they named, or on consent through the partner, stop getting credentials: give the receiving surface caller authentication the partner's target_auth satisfies for service-level bindings, and route per-user calls through a Transit Point that sets delegated_credentials_for_fabric_callers, on a surface whose caller authentication tells a partner's users apart. Agent card and discovery reads are not affected.
  • ⭐ Create surfaces with closed Access Points by default — The admin API and dashboard now create surfaces with access_point.public: false, so an Access Point no longer admits anonymous callers: a missing or invalid credential is refused with 401. A closed Access Point without source authentication (a caller_authentication method, or on MCP mcp_http.authorization) is saved, with access_point_status: not_ready and a Warning header, but answers every request, discovery reads included, with 503 until source authentication is added or public is set to true. The same applies after a PUT, PATCH, variant change or promote that leaves a closed surface, or one of its enabled variants, without source authentication. API and CLI clients that create surfaces must add source authentication or send access_point.public: true, and should check access_point_status or the Warning headers of the response. Surfaces stored before this release, built from DynamoDB, or restored without public keep their behaviour.
  • Older releases skip every surface created after this upgrade — A release before this one rejects access_point.public and caller_authentication.on_failure, so after a rollback it skips, with a logged error, every surface that stores either one: every surface created after the upgrade, and every surface closed or opened since. Their routes answer 404 and they leave the dashboard and Fabric discovery until the upgrade is reapplied; the records stay on disk. Removing the fields to load them on the older release opens them to callers without credentials. Back up _storage/agent_surfaces/ before upgrading.
  • ⭐ Refuse a Transit Point that authenticates no caller unless it is marked open — A Transit Point with require_transit_token: false and neither mcp_http.authorization nor source_auth served anyone who reached it, through this gateway and with its target_auth credential. Such a Transit Point must now be marked allow_unauthenticated_callers: true. The admin API refuses one that is not, on the surface or in a variant, with 400, and also refuses allow_unauthenticated_callers on a Transit Point that authenticates its callers, source_auth beside mcp_http.authorization, and a source_auth a Transit Point does not support (did_auth, a jwt_bearer that forwards the token or names no audience, or a key read from anything but a header). A stored surface with such a Transit Point still loads, with a warning in the log, but that Transit Point refuses every request with 503 until it is fixed. A save that leaves such a stored Transit Point exactly as it is is still accepted, so a dashboard save of the surface works; one that changes it, or adds another, is refused. After upgrading, set allow_unauthenticated_callers: true on every Transit Point that should stay open, or give it source_auth, with an admin API PUT of the surface or in the dashboard; do not set the flag before upgrading, because a release before this one drops it when it saves the surface.
  • ⭐ Mark surfaces whose Transit Points use source_auth so older releases skip them — A release before this one ignores a Transit Point's source_auth. After a rollback, or on an older node sharing storage during a rolling upgrade or in an Active/Standby pair, it would serve such a Transit Point to anyone who reaches it, with its target_auth credential, when the transit token is off. A surface with a Transit Point that uses source_auth, on the base or in a variant, is now stored with transit_point_source_auth: true, which a release before this one refuses, so it skips the whole surface instead of serving it open. Upgrade every node that shares storage before giving a Transit Point source_auth, and do not roll back below this release while one uses it: first remove source_auth from each such Transit Point and turn its transit token back on, or delete the surface, then check no surface carries transit_point_source_auth. A Surface Template's surface is marked the same way, so an older release's admin API refuses a surface created from it; the dashboard rebuilds a template's surface without the mark, so remove source_auth from such templates too before rolling back, or back them up before upgrading.
  • Key a caller by how it was identified, not by its name alone — The caller's identity hash, which keys delegated credentials, travels in the transit token as user_identity_hash and is in Transit Point policy input as input.transit.caller.user_identity_hash, was the SHA-256 of the bare principal, so an API key named admin and a JWT whose sub is admin, from any issuer, were one caller. It is now the SHA-256 of the authentication method, its issuer (the JWT iss, the API Key Provider agent, or the certificate an mTLS client certificate was verified against), the surface's tenant and the principal. The Workload Binding user_hash for caller_source: did follows the same rule. A JWT without an iss or a sub has no identity hash, so an Access Point that would give its caller a delegated credential answers it with 403. Every delegated credential stored before this release is keyed by the old hash and is no longer found, so users who connected an account connect it again, and policies that compare user_identity_hash need the new value. The old entries stay in the delegation vault, with their refresh tokens, until an operator revokes them; see "Removing grants keyed by the old identity hash" in docs/CREDENTIAL_DELEGATION.md.
  • Refuse transit_token_mode reference in the admin API — A surface or surface template that sets transit.transit_token_mode: reference, on the surface or in a variant's overrides, now gets a 400 on create, update and import, where it was accepted although the gateway always issued embedded tokens. Send embedded or leave the field out. A stored reference is rewritten to embedded on load.
  • **Ignore log redaction set i...
Read more

v1.0.3

Choose a tag to compare

@affinidi-semantic-release affinidi-semantic-release released this 29 Oct 16:22
a338d9c

Changelog

Breaking

  • Refuse transit tokens issued before the upgrade or by an unshared instance — Transit tokens issued before the upgrade are refused once, so a managed agent's Transit Point calls fail until its next inbound request brings a new token, at most five minutes later. Gateway instances that do not share [storage_paths] vc_keys no longer accept each other's transit tokens; route a managed agent's Transit Point calls to an instance sharing that directory with the one that served its inbound request. During a rolling upgrade, instances on the old and new versions refuse each other's tokens even when they share storage, so upgrade every instance sharing storage together.
  • Refuse a transit token 5 seconds after it expires instead of 60 — A Transit Point accepted a transit token for 60 seconds after its expiry. It now allows 5 seconds of clock difference, so a managed agent turn that runs past about 305 seconds has its later Transit Point calls refused until its next inbound request brings a new token.
  • Set Transit Point tool gating on the surface's Transit Point, not a variant's — MCP tool gating is compiled from a surface's own Transit Points only, so a variant's Transit Point is gated by the surface's Transit Point of the same alias. A variant Transit Point that sets mcp_tool_gating where the surface's Transit Point of that alias sets none, or that the surface does not have, now refuses every tools/call and answers tools/list with no tools, as a per-Transit-Point policy set only on a variant is already refused. Move such gating to the surface's Transit Point.
  • Remove transit.source_auth from surfaces to serve their Transit Points again — A surface that sets transit.source_auth has every request to its Transit Points refused, and the admin API refuses to save it with the field. Authenticate Transit Point callers with require_transit_token and, on MCP Transit Points, mcp_http.authorization.
  • Give Transit Points with credential bindings an agent identity — A Transit Point with outbound_credentials or transit_credentials now refuses a request with 403 when no agent identity is resolved, because neither the Transit Point's managed identity nor the transit token's caller DID is set, instead of keying the vault on a shared placeholder. Configure a managed identity on such Transit Points, or caller identity on the Access Point; a static key that does not depend on the agent can use target_auth instead. Grants consented under the placeholder agent are no longer released.
  • Refuse credentialed Transit Point calls whose transit token names no caller — A Transit Point with credential bindings now refuses with 403 a request whose transit token carries no caller identity, instead of forwarding it without the credential. That covers every request to such a Transit Point that sets require_transit_token: false, and every request from an anonymous caller of an Access Point whose caller authentication is optional. Authenticate the Access Point's callers, or move a static credential to target_auth.
  • Refuse Access Point and Fabric calls whose delegated credential is unavailable — An Access Point or Fabric receive with credential delegation now refuses with 503 a request whose provider is deleted or unreadable, whose API key secret cannot be read, whose client-credentials token request fails, whose vault read fails, or whose consent cannot be asked for, instead of forwarding it without the credential. The refusal is a JSON-RPC error (-32603) carrying the request's id on an MCP surface and an application/problem+json body otherwise, where the gateway used to answer 503 in plain text.
  • Forward Transit Point discovery reads without a delegated credential — A GET of a public discovery path, such as an agent card, through a Transit Point with credential bindings used to carry the delegated credential when its transit token named a caller. It is now always forwarded without one, as it is without the target_auth credential, because the gateway does not check a transit token on such a read.
  • Check inbound listener bind_address values before upgrading — An inbound listener whose bind_address is not 0.0.0.0 now accepts connections on that address only, so a listener set to 127.0.0.1 is no longer reachable from other hosts. Inbound listeners used to ignore their bind_address; one that is not an IP address now stops the gateway from loading its network configuration, and one the host does not own, such as an Elastic IP or a NAT address, now stops that port from serving, the management API and dashboard included when they are on it. The example configuration and Docker setup bind 0.0.0.0 and are unaffected.

Security

  • Reject JWT Bearer tokens used before their nbf — JWT Bearer caller tokens were accepted before the time in their nbf claim, because the JWT library checks nbf only when asked. A token whose nbf is still more than 60 seconds away is now refused like any other invalid token, on every surface with JWT Bearer caller authentication and on the gateway STS Resource Server.
  • Keep MCP consent prompts on the surface whose session asked for them — The MCP notification streams that carry credential-delegation consent prompts, and the pending prompts themselves, are now keyed by surface as well as Mcp-Session-Id. Session ids can now come from the upstream, so a client on another surface presenting the same id could otherwise receive another user's authorization URL or cancel their pending prompt. Sessions are not bound to the caller, so within one surface protection now rests on the upstream's session id generator rather than an id the gateway minted. When an upstream returns a session id already in use on the surface to a new initialize, consent prompts are turned off for that id, even after DELETE and until the gateway restarts, and the binding's elicit_fallback applies.

Added

  • Publish protected resource metadata for MCP surfaces with JWT callers — An MCP Access Point whose JWT Bearer source authentication accepts tokens from an external authorization server can set caller_authentication.protected_resource with its resource, authorization servers and scopes. The gateway then serves the RFC 9728 document at /.well-known/oauth-protected-resource followed by the route, for the base surface and each enabled variant, so an MCP client finds where to get a token. Each listener publishes only its own surfaces. The authorization servers must be exactly the issuer of the caller authentication's JWT verification strategy. A token is accepted only when its aud claim names the resource advertised for the route called, the base resource or the variant's own, as the MCP authorization specification requires, directly and over Fabric, so the JWT Bearer audiences list must be empty. The caller's token is never forwarded: forward_header is refused on save, and a stored surface with it answers 503. The setting is checked on save like mcp_http.authorization. A PUT of the surface or of a variant that omits it keeps it while the Access Point would still serve it, and is refused when it moves the route or listen address the setting names. A PUT or PATCH that sends null removes it, and surfaces without it behave as before.
  • Challenge MCP callers without a valid token to fetch one — A request to an MCP Access Point that publishes protected resource metadata, whose bearer token is missing or refused, is now answered before policy with 401 and WWW-Authenticate Bearer resource_metadata naming the absolute metadata URL, with error=invalid_token once a token was presented and the scopes when set. MCP clients follow it to the authorization server, get a token and retry. Over Fabric the 401 carries no resource_metadata, since the sending gateway strips the caller's own token. Without this setting a failed credential is still handed to policy.
  • Edit MCP authorization server discovery in the dashboard — The Caller Context editor of an MCP surface whose caller authentication is JWT Bearer can turn on authorization server discovery and set its resource, authorization servers and scopes, for the surface and each variant. It keeps token forwarding off and the accepted audiences empty while discovery is on, shows the resource each variant advertises, and removes a setting the operator switches off in the same request as the rest of the save, so a save that fails changes nothing.
  • Run MCP discovery end to end against Ory Hydra — make mcp-discovery starts Ory Hydra in Docker, a login and consent app, an MCP server and the gateway, then checks discovery, sign-in, the token audience, token stripping and the caller identity in policy with a scripted client and the official MCP client SDK. With ARGS=--keep it stays up for a check with MCP Inspector. The consent app shows how to grant the RFC 8707 resource as the token audience, which Hydra does not do by itself.

Changed

  • Sign transit tokens with a random key kept in storage — The gateway now signs transit tokens with a random 256-bit key it creates on its first start and keeps beside its own keys under [storage_paths] vc_keys, readable by the owner only, restricted to the owner again when a restore wrote it otherwise, and encrypted when encryption at rest is on, so instances sharing storage agree. Every token names the key in its kid header, and a token naming another key, or none, is refused. A key file that cannot be read stops the gateway from starting rather than being replaced, and a key directory on a filesystem without hard links stops the first start with an error saying so.
  • Check a transit token's surface and lifetime on every Transit Point request — Every Transit Point request now checks, alongside the transit token's signature, issuer and expiry, that the token's surface is the Transit Point's surface, that its issue time is not in the future, tha...
Read more

v1.0.2

Choose a tag to compare

@affinidi-semantic-release affinidi-semantic-release released this 28 Oct 16:30
98c68fd

Changelog

Security

  • Update zstd-sys to 2.1.2 and ignore the zstd advisories no release fixes yet — RUSTSEC-2026-0410 reports memory corruption in the zstd-sys WebAssembly allocator shim, fixed in 2.1.2, which Cargo.lock now selects. RUSTSEC-2026-0391, RUSTSEC-2026-0398 and RUSTSEC-2026-0403 affect zstd 0.13.3 and zstd-safe 7.2.4 and are fixed only in zstd 0.14 and zstd-safe 8, while pulsar and the Solana RPC client still require zstd 0.13. The gateway calls no zstd API itself, so they are ignored in .cargo/audit.toml with the reason until those dependencies move.
  • Stop reading task history for the managed agent's identity over Fabric — Over Fabric, both gateways read the managed agent's identity from the first entry of a task's history, which is normally the caller's own message. A caller could have its self-asserted identity resolved as the agent's and signed, and the agent's credential could be attached to the caller's message. Task history is no longer read for identity on the Fabric path, matching a direct call.
  • Keep Trust Checks on MCP and HTTP Transit Points until reviewed — The old surface-wide Trust Check list also ran on MCP and HTTP Transit Points, where an HTTP destination serving an A2A agent card could produce real denials. The conversion copies the list onto those Transit Points too and flags each copy for review, so no stored Transit Point silently stops being checked. Removing a copy stops its checks and its verified target identity, and a policy that denied on them then allows that Transit Point's traffic; a rule that only looks for failed checks also allows traffic when no checks ran, and docs/POLICY.md shows how to require that checks ran.

Added

  • Show the Trust Recorder status on identities and the recorder panel — The Identities page now shows a Recorded, Partly recorded, Failed, Skipped or Not attempted yet chip for each managed agent whose surface has a Trust Recorder, with a tooltip naming the registry, the reason and the record counts, and a Recorder filter. The Trust Recorder panel shows a Last outcome row. The status is the recorder's last known outcome, kept per surface and agent, saved only when it changes, cleared when the surface is deleted or its Trust Recorder entries change, and follows live updates. It carries codes, counts and times only, never registry error text.
  • Retry the first call to a directory-listed mediator while its DID is refused — For a mediator the mediator directory lists, which may admit a DID only after the directory's operator has registered it in response to the creation_started notification, when the first call of a new Connection Point to it is refused because it does not know the DID yet (an HTTP 403, an ACL denial, or the live connection not becoming active), the gateway repeats it with the same DID instead of failing at once. AG_MEDIATOR_DIRECTORY_FIRST_CALL_RETRY_ATTEMPTS (default 5, 0 to 20, 0 never repeats) and AG_MEDIATOR_DIRECTORY_FIRST_CALL_RETRY_DELAY_MS (default 2000) set the repeats and the wait, for directory-listed mediators only. A mediator the operator added, or one the directory no longer lists, is never retried and its refusal fails at once, and for an unlisted provided mediator the error says it is no longer offered by the directory. Only an HTTP 403, an ACL denial and the live connection failing to become active (which also covers an unreachable mediator) are repeated, other failures end creation immediately, cancellation stops the wait, the whole sequence is capped at 45 seconds, and after the last repeat creation fails as before. This lets a mediator that only admits known DIDs accept the DID once a receiver of the event hook has registered it. See docs/INTEGRATIONS.md.
  • List extra mediators from an operator-configured mediator directory — When AG_MEDIATOR_DIRECTORY_URL is set, the gateway reads a list of mediators from that endpoint with a signed GET (the same X-Webhook-Timestamp and X-Webhook-Signature-256 scheme and AG_EVENT_HOOK_SECRET as the event hook, https unless the host is loopback, redirects not followed) and shows them in GET /v1/mediators, GET /v1/mediators/compatible, the Create Connection Point wizard (which pre-selects the first one) and the Mediators page. The endpoint answers {"items":[{"did","name"}]}, other fields are ignored. An unavailable, slow or invalid directory leaves only the stored mediators listed; right after start the gateway asks again every 2 seconds for up to 30 seconds, allowing each of those requests 10 seconds, without making any request wait, and an expired list is served at once while one background task refreshes it. Creating a Connection Point on a directory entry stores a provided mediator record for it under an id fixed by the DID before the Connection Point's first event, so every event carries the final mediator id (the record stays and is reused if the creation then fails; the final check and save hold the record's lock, and a token scoped to directory ids can create on the entry repeatedly). A mediator the operator added for the same DID is a separate row and is never used for the entry, and the entry is always listed under its directory id. Creating on an entry needs no mediators.edit permission and does not count toward the mediator limit. Creating a Connection Point on a directory id before the directory has answered successfully answers 503 with a Retry-After header instead of 404. A stored provided record cannot be edited through the API (409) and can be deleted only once the directory has answered without its DID and no Connection Point or pending pairing uses it (the record is stored only after the request's authorization and scope checks pass), which refreshes it; it follows the caller's mediator resource scope by its own id. The first-call retry also applies while the directory has not answered successfully yet, and each attempt is bounded by the 45 second cap. Mediator responses gain provided only when a directory is configured or a record was created from an entry. List requests never wait for the directory, even while it is down, and a provided record the directory stopped offering, or any provided record when no directory is configured, is marked deletable in the list. The first-call retry needs a configured directory, its delay is at most 30 seconds (a larger or invalid value falls back to the 2 second default), and a repeat only starts when at least 10 seconds of the 45 second cap remain after its wait, so a long delay means fewer repeats. Starting a pairing with a directory-provided acceptor mediator holds that record's lock while the pairing is stored. Each acceptance of an invitation now has its own pairing (keyed by its temporary connection point), so concurrent acceptances never overwrite or remove each other's state, and the connection-accepted message is matched to the pairing and the pending gateway of the connection point it arrived on, never to another acceptance's. When accepting an invitation fails after the pairing is stored, the pairing is removed together with its temporary connection point, pending gateway and generated keys. Finalizing a pairing validates the acceptor mediator and this attempt's pending gateway before any state is written (a pending gateway removed by a restart is no longer recreated, so an acceptance in flight during a restart does not complete), holds a provided acceptor mediator's lock, with the expiry and the record checked again under it, until the permanent connection point is saved, and finishes removing the temporary connection point and its keys even when stopping its listener aborts the running task. Once its expiry (oob_connection.pending_expiry_hours) has passed, neither a pairing nor a temporary connection point that names the record blocks deleting it, and an expired pairing cannot be finalized, and a pairing rebuilt from its temporary connection point keeps that connection point's recorded expiry (or its creation time plus the configured lifetime) instead of getting a new one. See docs/INTEGRATIONS.md.
  • Announce the DIDs of gateway pairing and Trust Registries to the event hook — The gateway now sends the event hook the DID it generates for each flow that logs in to a mediator, not only for Connection Points created through the API. The acceptor's temporary and permanent DIDs, the inviter's permanent DID after approval, and the DID of a stored Trust Registry are announced before the gateway first contacts the mediator, so a receiver that registers DIDs on a mediator that admits only known DIDs, such as the shared public mediator, covers them. Their records now carry mediator_did, so deleting a pairing, expiring a temporary Connection Point or deleting a Trust Registry sends deleted. The acceptor's handshake DID and a new Trust Registry's DID also repeat their first mediator call while a mediator the directory lists refuses a DID it has not admitted yet. The ephemeral DID for querying an unstored public Trust Registry and connections rebuilt after a restart are not announced. See docs/INTEGRATIONS.md.
  • Stamp every audit record with the id of the gateway request that produced it — Audit records now carry a request_id (16 lowercase hex characters) minted when an Access Point, Transit Point or fabric-received request starts. All records of one request share it, so a sink such as Agent Watch can count requests rather than policy evaluations; trace_id still links the requests of one chain.

Changed

  • Refuse to finalize a pairing whose own pending gateway is missing — Finalizing a pairing, for every gateway and not only with a directory mediator, now needs the pending gateway of that acceptance and refuses, writing nothing, when it is missing. Earlier versions recreated it with no tenant, which could attach a gateway with no tenant to a tenant's mediator or pick up another acceptance's gateway by the shared DID. An invitation accepted just before the gatewa...
Read more

v1.0.1

Choose a tag to compare

@affinidi-semantic-release affinidi-semantic-release released this 26 Oct 14:51
84895e8

Changelog

Breaking

  • Migrate existing integrations to endpoints and event sets on upgrade — On its first start the Active node turns each integration into an endpoint, an event set of the events it received and an integration, with the same id, writes _storage/integrations/migration-report-.json and renames the old directories to .migrated-, deleted 30 days later as they may hold plain-text credentials. Upgrade both nodes of a pair before the Active node starts; a Standby migrates when promoted. Attachments to some gateways or connection points now receive every resource's events, every tenant's included. Tenant-owned integrations become appliance-wide and are written disabled, since enabled they would receive every tenant's events; review each before enabling it. Custom _ variables and unfilled placeholders leave the templates, and endpoints failing the current checks are written disabled; the report names each change. The old paths resolve against the config file's directory, and while storage_paths.integrations is set, the new integration paths a config leaves unset go beside it, so a config written for the previous version needs no change; set them before removing the old keys. See the INTEGRATIONS upgrade section.
  • ⭐ Manage integrations as an endpoint plus an event set — /v1/integrations now keeps integrations that join one endpoint (/v1/integrations/endpoints: transport, destination, credentials) and one event set (/v1/integrations/event-sets: events picked from the whole catalog), with the template on the integration. Categories and tenant owners are gone.

Security

  • Upgrade rustls-webpki and webpki roots to clear three distrust advisories — rustls-webpki 0.103.15 could accept certificates through distrusted intermediates (RUSTSEC-2026-0382), and webpki-roots and webpki-root-certs 1.0.8 still trusted distrusted emSign intermediates (RUSTSEC-2026-0381, RUSTSEC-2026-0383). They are now 0.103.16 and 1.0.10, and the dependency audit passes again.
  • Keep secret values and webhook URLs out of integration errors — Saving an endpoint, test sends, the startup definition check and the migration report no longer show the value of a referenced secret or a URL, such as a Slack webhook URL. A failing $SECRET: reference is reported by field and secret id only, URL validation messages name the host but never the URL, and Redis stream errors no longer repeat part of the Redis URL.
  • Render dashboard notifications as text, not HTML — Dashboard toasts built their message and action label as HTML, so a name or server error message containing markup, such as an integration endpoint name, could run script in the browser of whoever saw the toast. Toasts now show all text literally.
  • Document that integrations.edit can use and read every appliance secret — An endpoint's $SECRET: reference is resolved by the gateway when it sends, without a secrets permission, so whoever holds integrations.edit can point an endpoint at a host of their choosing, send a test and receive the secret's value. Grant integrations.edit only to roles that may read secrets; the defaults keep it and secrets.* administrator only. Tenant-owned secrets never resolve for endpoints. RBAC.md and INTEGRATIONS.md say so.
  • Escape event values in HTML integration emails — Values filled into an HTML email body have &, <, >, " and ' escaped, so caller-supplied text such as a user name cannot add markup or links to the message. The template itself is not escaped.
  • Never return integration endpoint credentials from the API — SMTP passwords, Slack webhook URLs, webhook signing secrets and headers, and stream SASL passwords, Redis URLs and Pulsar auth tokens are write-only: responses name the secrets set but never their values, so the user role's integrations.view no longer reveals credentials.
  • Refuse tenant and resource-scoped tokens on the integrations API — Integrations, endpoints and event sets belong to the whole appliance, so every /v1/integrations route answers 403 to a tenant or resource-scoped access token.
  • Apply the response policy and identity credential to A2A streams — An A2A event stream from a direct Target used to be returned whole, and the response checks found no JSON in it. A response policy rule on the content could not block a stream, the response extension rules were skipped, and the caller received the agent's raw, unverifiable identity instead of the signed agent-identity-credential. Each event is now checked before it is sent and its identity claim replaced by the credential. On a surface that reads identity from the response, a stream that never claims one ends with an identity error. fabric:// surfaces and Transit Points still return the stream whole without these checks.
  • Update ctutils to 0.4.5 so its comparisons run in constant time — ctutils, which the gateway's elliptic-curve signing and DIDComm libraries use, could compile its mask generation and 128-bit comparisons to code that branches on the values compared, which can leak them through timing. The RustCrypto advisory is fixed in 0.4.4; the lockfile now pins 0.4.5.
  • Keep payment configuration errors out of 500 responses — When MPP challenges cannot be generated, or x402 payment requirements cannot be serialized, the 500 response says only "Payment configuration error" and the detail is logged. Before, the response carried the internal error text.
  • Build the dashboard with Node.js 22.23.4 to fix CVE-2026-111497 — The Docker images and the CI frontend job built the dashboard with Node.js releases affected by CVE-2026-111497, an out-of-bounds write in the Brotli decoder. They now use Node.js 22.23.4, the patched 22.x release; the CI job moves off Node.js 20, which no longer receives fixes.
  • Update dependencies with published security fixes — Updates anyhow to 1.0.104 and event-listener to 5.4.2 to fix soundness advisories RUSTSEC-2026-0190 and RUSTSEC-2026-0221, and applies non-breaking dashboard dependency fixes, including the critical websocket-driver, shell-quote and proxy-addr advisories in the build toolchain. Overrides pin postcss-selector-parser to 7.1.6 or newer and @tootallnate/once to 2.0.1 or newer, clearing GHSA-rj75-hqrm-r3gf and GHSA-vpq2-c234-7xj6 from the build.
  • Upgrade the dashboard to React Router 7.18.4 — React Router 6 is affected by GHSA-wrjc-x8rr-h8h6, an open redirect through a backslash in Link and useNavigate targets, and GHSA-337j-9hxr-rhxg, with no 6.x fix. The dashboard now uses React Router 7.18.4, leaving no known advisories in its production dependencies.
  • Upgrade Alloy to 2.5 to drop the unsound lru 0.16 dependency — Alloy 1.x and 2.1 depend on lru 0.16, which is affected by RUSTSEC-2026-0253, a potential use-after-free in LruCache::pop. The gateway and its x402 payment dependencies now share Alloy 2.5, which uses the patched lru 0.18.
  • Build the dashboard on Node.js 22.23.4 — The Docker images now build the www dashboard on Node.js 22.23.4 instead of 22.21.1, and the MCP conformance CI job pins the same version. 22.23.4 is the October 2026 Node.js security release, which fixes an out-of-bounds write in the Brotli decoder (CVE-2026-111497) and several TLS certificate hostname-check bypasses. Node only runs while the image is built; the runtime image ships no Node.
  • Update TLS certificate dependencies in Cargo.lock — Bumps rustls-webpki to 0.103.16 (could accept distrusted intermediates, rustls/webpki#546), and webpki-roots and webpki-root-certs to 1.0.10 (bundled roots trusted distrusted emSign intermediates, rustls/webpki-roots#133). Fixes the failing Rust dependency audit.
  • Update rustls-webpki and webpki roots to reject distrusted emSign intermediates — Bumps the transitive rustls-webpki dependency from 0.103.15 to 0.103.16 and webpki-roots and webpki-root-certs from 1.0.8 to 1.0.10. The older versions could accept certificate paths through distrusted emSign intermediates whose private keys are now publicly known (RUSTSEC-2026-0381, RUSTSEC-2026-0382, RUSTSEC-2026-0383). Restores a passing cargo audit.
  • Update ctutils to 0.4.5 for constant-time mask and comparison fixes — Bump the transitive ctutils dependency from 0.4.2 to 0.4.5, which fixes non-constant-time mask generation and u128 comparisons reported by cargo audit (RustCrypto/utils#1569). The crate is pulled in by the elliptic-curve and digest crates used for DID and DIDComm cryptography.
  • Update crypto-bigint to 0.7.10 for constant-time operation fixes — Bump the transitive crypto-bigint 0.7 dependency from 0.7.5 to 0.7.10 (and ctutils to 0.4.6), which fixes timing variability in operations intended to be constant-time (RUSTSEC-2026-0387). The older 0.3 and 0.5 copies pulled in by elliptic-curve 0.13 and der 0.5 have no fixed release, so the advisory is recorded as accepted for them until those upstream crates move to 0.7.

Added

  • Check that pull request titles follow Conventional Commits — Squash merges use the pull request title as the commit message on main, so a new pr-title workflow checks the title with the Validate PR title job and fails when it does not follow Conventional Commits, for example feat: or fix(ci):. The description after the type must be at least 5 characters, the same minimum as the organization commit message rule. The job runs again when the title is edited, so fixing a title clears the failure without a new push.
  • ⭐ Manage integrations, endpoints and event sets from one Integrations page — The Integrations page has three tabs. Endpoints show the fields of their type, keep stored secrets hidden and can point a secret field at a stored secret. Event sets pick events from the whole catalog with search. The integration editor joins an endpoint and an event set, writes the template with the event set's variables, sets the audit delivery limits when audit events are picked, and sends a test as it stands. Endpoin...
Read more

v1.0.0

Choose a tag to compare

@affinidi-semantic-release affinidi-semantic-release released this 20 Oct 09:42
f62f2de

Changelog

Breaking

  • ⭐ Stop checking the A2A request shape by default — The A2A request-shape check (messageId, role, parts, params.id) used to run on every managed-agent A2A surface by default. A surface stored without its own settings now gets envelope validation (or off when [a2a] validate_messages is false), never the request-shape check, so malformed A2A messages are forwarded for the agent to decide. To keep refusing them, set Message validation to Envelope + A2A fields (access_point.a2a.validation = full) on those surfaces. On the first start after the upgrade, a warning names the surfaces that lost the check.
  • ⭐ Record only the audit events each destination selects — The VP Audit Log and each Governance Audit integration now record only the audit events selected for them, and nothing is selected by default. After upgrade, the VP Audit Log keeps policy decisions, trust checks and VP injections only if they were enabled before; credential delegation, payment and trace-termination records, which were always written, stop until selected in Settings › VP Audit. Existing Governance Audit integrations receive nothing until events are listed in their audit_events.
  • Deliver governance audit records at least once from a durable journal — Audit records are journalled to disk before any destination receives them, and each destination reads the journal from its own position, so a slow or unreachable integration falls behind instead of losing records, and delivery resumes after a restart. A record may be delivered more than once; deduplicate on event_id. Integrations no longer depend on the VP Audit Log keeping a record. The journal lives in the audit log directory under journal/, and a crash loses at most the last batch not yet synced to disk. While the journal cannot be written up to 10,000 records wait in memory; beyond that the oldest are discarded and counted in agent_gateway_audit_records_lost_total with reason journal_write. The journal size is reported in agent_gateway_audit_journal_bytes and logged as a warning above 256 MiB; above 1 GiB the destinations still holding back its oldest part are moved past it, and what each skips is counted in agent_gateway_audit_sink_skipped_total with reason backlog_cap. The VP Audit Log never writes the same record twice. A record no destination selects is not built at all, so unselected events cost nothing, including the signed VP a consent-granted record may carry.
  • Store VP Audit selections as audit_events instead of audit_categories — The settings API takes and returns audit_events, a list of event names such as policy_decision, and rejects names that are not audit events. A request that still sends audit_categories gets 200 and the field is ignored; send audit_events instead. Settings stored with audit_categories are read as the matching events (policies as policy_decision, trust_checks as trust_check, identity as vp_injected), so existing selections are kept.
  • Remove the dropped result from agent_gateway_audit_forward_total — Records are no longer dropped from a full forwarding queue, so agent_gateway_audit_forward_total no longer reports result dropped, and integration_id is never empty. Alert rules on result dropped never fire; alert on agent_gateway_audit_sink_skipped_total and agent_gateway_audit_records_lost_total instead.
  • Answer full Fabric stream caps on the receiving gateway with capacity_reached — A full framed-stream cap, Open replay table or table share, a full admission pool, or a peer past its Open rate, is now refused with capacity_reached instead of unavailable, so the sending gateway's caller gets 429 with Retry-After instead of 502. A 1.0.0 sender cannot parse capacity_reached, so while a pair runs mixed versions its callers wait for their response deadline, holding its stream slots, whenever the upgraded receiver is out of room. Normal traffic is unaffected. Upgrade both gateways of a pair together, or the sending gateway first, as an upgraded sender understands both codes.
  • ⭐ Refuse did:web and did:webvh on private hosts unless allow_private_hosts is on — DID resolution now refuses a host that is, or resolves to, a loopback, private-network, carrier-grade NAT or link-local address, and does not follow redirects; each method also refuses some local names outright. This closes an SSRF path through attacker-supplied DIDs and applies to every resolver the gateway builds. A stored mediator document is served from cache only until the resolver's TTL expires, so a stack whose mediator or peer DIDs live on such hosts must set the startup-only [did_cache] allow_private_hosts = true, which logs a warning at startup and also reopens cloud metadata. scripts/_prepare.sh turns it on for local and Docker dev stacks, and the BDD configs turn it on too.
  • ⭐ Newly paired Remote gateways reach no surface until one is exposed — Every pairing path, including the inbound handshake, now creates the peer with exposure mode none, so it can neither forward to nor list any local surface until an operator exposes one. An exposure update without exposure_mode and with an empty list now means none, not every surface. The capability query now filters on the authenticated sender's exposure and returns nothing when that lookup fails, instead of every surface.
  • ⭐ Never apply tool-scoped credential bindings on Transit Points — A legacy MCP request through a Transit Point matched required_for.tools against the JSON-RPC method, so a binding that listed a tool never applied there, while one that listed a method such as tools/call or message/send did. A 2026-07-28 tools/call matched on the tool name. Transit Points now apply only all bindings. Bindings carry no destination, and a partner upstream names its own tools, so a tool name that collided with one of the Target's tools would hand the partner a credential meant for the Target. all bindings still reach every Transit Point of the surface, including the transit_credentials that a surface without outbound_credentials reads as all. Access Points are unchanged. Upgrade note: a binding that lists a JSON-RPC method, or a tool called through a Transit Point with 2026-07-28, no longer sends its credential to the Transit upstream. Use an all binding only if every Transit Point's upstream should receive it.
  • Refuse MCP requests that skip POST admission by method — MCP admission, including the batch and single-object checks, ran only for POST. A PUT or PATCH, or a GET or DELETE with a body, reached the upstream without them, so an upstream that routes by path whatever the method could run a batch or a gated tool. Access Points, Transit Points and Fabric receive now answer 405 with Allow: POST, GET, DELETE, HEAD, OPTIONS to any method but those five, and 400 with JSON-RPC -32600 to any non-POST request with a body. A standalone MCP Proxy answers DELETE with 405 and Allow: POST, GET, HEAD. GET and DELETE without a body behave as before. Upgrade note: an MCP client that sends PUT, PATCH or a body on GET or DELETE is now refused, as is an MCP POST whose body is not valid JSON or is JSON but not an object, on Access Points, Transit Points, Fabric receive and the standalone MCP Proxy.
  • Stop sending delegated credentials over Fabric unless the route opts in — After upgrade, fabric:// Targets and Transit Points on surfaces with outbound_credentials stop sending delegated access tokens to the peer, because the new fabric_delegated_credentials setting defaults to off and no migration turns it on. Turn the setting on for each Target or Transit Point whose peer relies on receiving the token, including fabric:// Transit Points that rely only on their own transit_credentials. This includes A2A surfaces whose fabric:// Transit Points sent the token as a header; A2A Access Point traffic over Fabric never carried it and is unchanged. Transit Points on surfaces without outbound_credentials also stop receiving other Transit Points' transit_credentials; give each one its own binding if it needs a token. The surface's Target on such a surface also stops receiving Transit Points' transit_credentials; add the binding to outbound_credentials if the Target needs it.
  • Apply mcp_http Origin and size limits to every MCP endpoint — mcp_http Origin allowlist, header budget and max_request_bytes (default 1 MiB) now apply to every MCP endpoint and to 2024-11-05 traffic as well, so legacy browser clients whose Origin is not allowed get 403, and oversized requests get 413/431. Before upgrading, add browser origins to mcp_http.allowed_origins and raise max_request_bytes / max_header_bytes on endpoints that need them. Public endpoints given as bare listen addresses are skipped by the Origin check; any other public endpoint that is not an HTTP(S) origin is skipped with a warning.
  • ⭐ Admit MCP 2026-07-28 on every MCP endpoint and retire mcp_protocol_mode — Access Points, Transit Points, MCP Proxies and Fabric framed streams now admit MCP 2026-07-28 alongside 2024-11-05 without configuration, instead of rejecting it with 400 / -32022 unless the endpoint was set to dual. mcp_protocol_mode is retired: stored records and API payloads that carry it still load, the value is ignored, it is dropped on the next save and no longer returned. There is no per-endpoint opt-out, so a modern request to a legacy-only upstream gets 502. Buffered Fabric ForwardRequest stays 2024-11-05-only.
  • Stop sending target credentials for failed callers over MCP proxy and Fabric — Backends behind proxy:// MCP, a2a-proxy:// or Fabric surfaces with source authentication now receive requests from failed callers without the target credential, and will usually reject them. Two Fabric gateways that both require source authentication on the same header now reach the backend without the target credential, because the sending gateway strips t...
Read more

v0.0.1-rc8

v0.0.1-rc8 Pre-release
Pre-release

Choose a tag to compare

@robert-affinidi robert-affinidi released this 19 Oct 14:29
665dab4
docs: add self-hosting deployment requirements (#72)

## What

Adds `docs/DEPLOYMENT_REQUIREMENTS.md`: what your infrastructure needs
to self-host Agent Gateway — build, host sizing, storage and backups,
network and ports, outbound access, secrets, time sync, and the Fabric
mediator.

- Links it from `README.md`, `docs/README.md`, `docs/CAPABILITIES.md`
and `AGENTS.md`.
- Corrects `docs/CONFIGURATION_REFERENCE.md`: some `[storage_paths]`
keys resolve against the working directory, not the `config.toml`
folder.
- Adds a changelog fragment.

Docs only; no code changed.

## Verification

Links and anchors checked; config keys, ports, limits and release assets
checked against the source.

---------

Co-authored-by: Anton <anton.i@affinidi.com>