# 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.

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`):

```json
{"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:

| `kind` | What is created | Credential |
| --- | --- | --- |
| `servicenow` | A web-service-only `sys_user` (Table API) | A generated password, filed in the Secrets Store and used at once |
| `jira` | A Jira Cloud user (`/rest/api/3/user`) | None: the account waits until you save an API token |
| `m365` | A Microsoft Graph user (`/v1.0/users`), with a licence when you give `license_sku` | None: use Entra Agent ID below, or save one |
| `scim` | A SCIM 2.0 user (`/Users`): SAP Cloud Identity and any SCIM directory | None: 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"`:

```json
{"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}`:

```json
{"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`.
