Agent identities downstream

Give each agent its own account in ServiceNow, Jira, M365 or SAP, publish agents into your Entra or Okta directory, and use Cross-App Access (ID-JAG) in both directions.

After reading this page you will be able to:
  • Give an agent its own account on a connected system
  • Provision that account through the system's API
  • Require agents to call as themselves
  • Publish an agent to Entra Agent ID or Okta
  • Trust an IdP's ID-JAGs for Liyagent's MCP endpoint
  • Reach another app through Cross-App Access

By default, an agent running as itself calls a connected system with the server's shared login. The far end then sees one integration user for every agent. On this page you give each agent an identity of its own, both in the systems it calls and in your identity provider.

Nothing here changes an existing flow until you set it up. An agent with no account of its own keeps using the shared login. An agent running for a person still sends that person's token.

An agent's own account on a system

An account belongs to one agent on one MCP server (PUT /api/agent-accounts/{agent}/{server}, needs access.write):

{"system": "servicenow", "external_user": "liyagent.triage",
 "auth": {"mode": "static_bearer", "secret_value": "..."}}
  • system is one of servicenow, jira, m365, sap or other.
  • auth takes the same modes as a server's own credential: static_bearer, static_header or client_credentials. A pasted secret_value is filed in the Secrets Store, and the account keeps only its secret:// reference.
  • The server must be a remote (HTTP) server with a shared credential. A server bound to each person's own connection never sends an agent account.

When the agent runs as itself, its calls to that server carry its own credential. Its tool listing is cached separately from the shared login's. Each save, rotation and delete is audited as agent.account.create, agent.account.rotate and agent.account.delete.

Provision the account through the system's API

POST /api/agent-accounts/{agent}/{server}/provision creates the agent's user in the system, using an administrator credential you name by secret:// reference:

kindWhat is createdCredential
servicenowA web-service-only sys_user (Table API)A generated password, filed in the Secrets Store and used at once
jiraA Jira Cloud user (/rest/api/3/user)None: the account waits until you save an API token
m365A Microsoft Graph user (/v1.0/users), with a licence when you give license_skuNone: use Entra Agent ID below, or save one
scimA SCIM 2.0 user (/Users): SAP Cloud Identity and any SCIM directoryNone: the account waits until you save one

An account waiting for its credential is never sent, so the agent keeps the shared login until you save one. Each provisioning is audited as agent.account.provision, and a refused one as agent.account.provision_failed.

Require agents to call as themselves

PUT /api/agent-accounts/settings with {"require_own_account": true} refuses an agent's call to any server where it holds no enabled account of its own, instead of sending the shared login. Each refusal is audited as agent.account.refused. It is off by default, because turning it on stops every agent that has no account yet.

Publish agents to your identity provider

Configure the directory once (PUT /api/agent-idp/settings), then publish an agent (POST /api/agent-idp/{agent}/publish) or withdraw it (DELETE /api/agent-idp/{agent}).

  • Microsoft Entra Agent ID. Set provider: "entra", your tenant_id, and your agent identity blueprint's blueprint_client_id and blueprint_secret_ref. Optionally add sponsor_ids, the Entra object ids of the people who answer for the agents. Publishing creates an agent identity under the blueprint (POST /beta/servicePrincipals/Microsoft.Graph.AgentIdentity).
  • Okta. Set provider: "okta", your org_url and an api_token_ref. Publishing creates an OIDC service application for the agent: client credentials only, authenticated with private_key_jwt against Liyagent's published keys, so no client secret is ever minted.

Publishing is off (provider: "none") until you configure it, and only an operator's call publishes. Each publish, withdrawal and failure is audited as agent.idp.publish, agent.idp.withdraw and agent.idp.publish_failed.

Cross-App Access (ID-JAG)

Cross-App Access lets the enterprise IdP, not each app's consent screen, decide whether one app may reach another for a person. It is the Identity Assertion JWT Authorization Grant (ID-JAG), and Liyagent supports both sides of it.

Liyagent reaching another app

Add a connection provider in mode obo with profile: "id_jag":

{"subject_source": "sso:okta", "profile": "id_jag",
 "audience": "https://wiki-as.example.com",
 "resource": "https://wiki.example.com/mcp",
 "resource_as_url": "https://wiki-as.example.com/oauth/token",
 "resource_client_id": "liyagent", "resource_client_secret": "secret://wiki-client"}

On each call, Liyagent first trades the person's sign-in ID token at their IdP for an ID-JAG addressed to the app's authorization server (RFC 8693, requested_token_type urn:ietf:params:oauth:token-type:id-jag). It then redeems the ID-JAG there for an access token (RFC 7523 JWT bearer). When every provider that uses a sign-in source is an id_jag one, single sign-on keeps the ID token for it rather than the access token. A person signed in before that is asked to sign in again.

Other apps reaching Liyagent

Trust an IdP's ID-JAGs with PUT /api/xaa/issuers/{name}:

{"issuer": "https://acme.okta.com", "jwks_uri": "https://acme.okta.com/oauth2/v1/keys",
 "clients": ["claude-enterprise"], "user_claim": "email"}

A confidential client named in clients can then post grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer with the ID-JAG as assertion to /oauth/token. Liyagent checks the following before it issues a /mcp token for the person:

  • typ is oauth-id-jag+jwt.
  • The signature verifies against the issuer's keys.
  • aud is Liyagent's authorization server.
  • client_id is the authenticated client.
  • The assertion is unexpired, lives at most an hour, and has not been used before.
  • user_claim names an enabled account.

With no trusted issuer, there is no grant. Each token issued and each refusal is audited as oauth.server.token or oauth.server.token.refused.