feat: give agents a Microsoft Entra identity across register, trace and evidence - #413
Merged
Merged
Conversation
…nd evidence Closes #353, #354, #355. A trace could say what an agent did but not which registered agent did it. Nothing in the release evidence pack tied runtime behaviour back to an accountable owner in the tenant, so "who owns this agent" was answerable only out of band. These three issues asked for the same loop from different ends, and they only make sense implemented together. The loop has three points. Register. `agentops agent register --sponsor <upn>` creates or adopts a Microsoft Entra agent identity blueprint and writes the resolved id to `.agentops/identity/agent-identity.json`. It is idempotent: an existing blueprint with the same display name is adopted rather than duplicated. A sponsor is mandatory, because an identity with no named owner is not governable, which is the whole point of registering it. `--dry-run` resolves and prints the inputs without contacting Microsoft Graph. Stamp. `_resource_attributes()` in the telemetry bootstrap resolves the id and sets it as the OpenTelemetry resource attribute `gen_ai.agent.id` on every span. When no identity is registered the attribute is omitted entirely rather than set to an empty string, so its presence is itself a usable filter in a trace query. Publish. The release evidence pack gains an `agent_identity` section reporting the id and whether it came from the local record or the environment. A missing identity produces a warning, not a blocker, because adoption has to be incremental. A read-only Doctor check reports registration posture under the Security category. It resolves the local record first, at zero cost, and only contacts Microsoft Graph when `identity.verify` is set in `agentops.yaml`. Four design decisions deviate from the literal issue text, each forced by the existing code rather than chosen: - The posture check is standalone rather than a `RULE_REGISTRY` rule. Every rule in that registry takes `(payload: AzureResourcesPayload, source_name)`, and `run_posture_check` returns early when `resources.account is None`, so a registry rule would silently never run for a workspace with no Azure account resolved. It follows the `governance.py` standalone pattern and is called from `analyzer.py`. - Registration is `agentops agent register`, not a step inside `agentops ship`. There is no `ship` command. Ship is a pillar, documented in `docs/ship.md` and materialised as workflow templates. The templates now carry the step, gated on the `AGENTOPS_IDENTITY_SPONSOR` repository variable so it stays opt-in: it writes to a tenant, which should never be the default behaviour of a generated workflow. - Findings use `Category.SECURITY`. There is no `governance` member in the `Category` enum, and adding one would reshape every consumer of the catalogue for a single check. - `agentops.yaml` needed a typed `AgentIdentityConfig` submodel rather than a free-form dict. `AgentOpsConfig` sets `extra="forbid"`, so the `identity` key was rejected outright until it was declared. A permissive `Dict[str, Any]` would have accepted `sponser:` silently and failed much later with a misleading "sponsor is required", so the submodel also forbids extras. The Agent 365 ingestion API is not public, so nothing here depends on it. The handshake is documented instead, in `docs/observe.md`, so the id can be correlated by whoever consumes the traces. 65 unit tests added across six files. Full suite: 1271 passed, 6 skipped. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bcb9c0b6-d506-46dc-90d2-8120413166ee
The resource attribute assembly moved into _resource_attributes(), which imports agentops itself. The import at the init_tracing call site became dead and ruff F401 flagged it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bcb9c0b6-d506-46dc-90d2-8120413166ee
This was referenced Aug 9, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #353, #354, #355.
The gap
A trace could say what an agent did, but not which registered agent did it.
Nothing in the release evidence pack tied runtime behaviour back to an
accountable owner in the tenant, so "who owns this agent" was answerable only
out of band. The three issues approach the same loop from different ends, which
is why they are implemented together rather than as three separate PRs.
The loop
Register.
agentops agent register --sponsor <upn>creates or adopts aMicrosoft Entra agent identity blueprint and writes the resolved id to
.agentops/identity/agent-identity.json. It is idempotent: an existingblueprint with the same display name is adopted, not duplicated. A sponsor is
mandatory, because an identity with no named owner is not governable, which is
the entire point of registering one.
--dry-runresolves and prints the inputswithout contacting Microsoft Graph.
Stamp.
_resource_attributes()in the telemetry bootstrap resolves the idand sets it as the OpenTelemetry resource attribute
gen_ai.agent.idon everyspan. When no identity is registered the attribute is omitted entirely rather
than set to an empty string, so its presence is itself a usable filter in a
trace query.
Publish. The release evidence pack gains an
agent_identitysectionreporting the id and whether it came from the local record or the environment. A
missing identity produces a warning, not a blocker, because adoption has to be
incremental.
A read-only Doctor check reports registration posture under Security. It
resolves the local record first, at zero cost, and contacts Microsoft Graph only
when
identity.verifyis set inagentops.yaml.Four deviations from the literal issue text
Each was forced by the existing code, not chosen for convenience.
governancepillaranalyzer.pyRULE_REGISTRYrule takes(payload: AzureResourcesPayload, source_name), andrun_posture_checkreturns early whenresources.account is None. A registry rule would silently never run for a workspace with no Azure account resolved. Follows the existinggovernance.pystandalone pattern.agentops shipagentops agent register, plus an opt-in step in the prod workflow templatesshipcommand. Ship is a pillar, documented indocs/ship.mdand materialised as workflow templates.governanceCategory.SECURITYgovernancemember exists in theCategoryenum, and adding one would reshape every consumer of the catalogue for a single check.AgentIdentityConfigsubmodel inagentops.yamlAgentOpsConfigsetsextra="forbid", so theidentitykey was rejected outright until declared. A permissiveDict[str, Any]would acceptsponser:silently and fail later with a misleading "sponsor is required", so the submodel forbids extras too.Why the workflow step is off by default
The generated production workflows carry the registration step gated on the
AGENTOPS_IDENTITY_SPONSORrepository variable. It writes to a tenant, so itshould never be the default behaviour of a workflow that AgentOps generates for
someone. Setting the variable to the sponsor's UPN turns it on.
Agent 365
The Agent 365 ingestion API is not public, so nothing here depends on it. The
handshake is documented instead, in
docs/observe.md, so the id can becorrelated by whoever consumes the traces.
Tests
65 new unit tests across six files:
test_agent_identity_service.pytest_agent_checks_agent_identity.pytest_cli_agent_register.pytest_evidence_pack_agent_identity.pytest_telemetry_agent_id.pytest_agentops_config_identity.pyFull suite: 1271 passed, 6 skipped. Both generated prod workflow templates
(placeholder and azd modes) were regenerated and verified to parse as valid
YAML with the identity steps correctly gated.
Docs
docs/observe.md— new "Agent identity on traces" section with thethree-step handshake, the
identityconfig block, and a KQL query.docs/doctor-checks.md—graphadded to the data sources table, threeagent_identity.*rows in the Security catalogue, plus a note that the checksare read-only.
docs/ship.md— "Giving the agent its own identity" under Identity andaccess, explaining the opt-in workflow variable.