Skip to content

Use S2S-only OBS export and app-only hosting token resolvers - #265

Merged
Krishnadheeraj (DheerajPannala) merged 9 commits into
mainfrom
users/DheerajPannala/obs-s2s-only-20260929
Sep 30, 2026
Merged

Krishnadheeraj (DheerajPannala) merged 9 commits into
mainfrom
users/DheerajPannala/obs-s2s-only-20260929

Conversation

@DheerajPannala

@DheerajPannala Krishnadheeraj (DheerajPannala) commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

.NET port of microsoft/Agent365-nodejs#290. Agent 365 observability (OBS) export now uses only the app-only S2S route, so customers no longer need to admin-consent the delegated Agent365.Observability.OtelWrite permission for telemetry.

  • The exporter always uses S2S. The sync and async exporters always post to /observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1.
    • Agent365ExporterOptions.UseS2SEndpoint and Agent365ExporterCore.BuildEndpointPath(string, string, bool) are [Obsolete] and ignored, even when false.
    • There is no fallback to /observability. HTTP 401, 403 and 404 fail the batch with no retry and no delegated route.
  • Export authenticates only with the configured app-only resolver. TokenResolver / ContextualTokenResolver must return an app-only OBS token for the exporting agent and tenant.
    • The resolver is called once per tenant/agent identity group in each export batch, so it should cache tokens per agent and tenant.
    • A null or empty token, or a resolver exception, fails the batch before any HTTP request is sent.
  • Hosting cache: an app-only resolver replaces delegated OBO.
    • AgenticTokenCache now implements IExporterTokenCache<ObservabilityTokenResolver>.
    • New RefreshObservabilityToken(agentId, tenantId, resolver[, scopes]):
      • returns the cached token while it is still valid; otherwise it calls the resolver;
      • tokens without an exp claim expire 1 hour after they are acquired;
      • on failure it clears the cached token and rethrows.
    • Entries are keyed by the (agentId, tenantId) pair, so IDs that contain a colon can't collide.
    • RegisterObservability keeps its first-registration-wins behavior.
    • Concurrent refreshes for the same agent and tenant are serialized, so callers share one acquisition.
    • Cleanup (the timer and RemoveExpiredTokens) clears expired token values but keeps resolver registrations. A resolver registered once therefore keeps working after an idle period longer than the token lifetime.
    • The delegated RegisterObservability(..., AgenticTokenStruct, ...) overload and the AgenticTokenStruct constructor are [Obsolete(error: true)]. A call that bypasses the compiler logs once and never calls ExchangeTokenAsync.
  • The default OBS scope is /.default. EnvironmentUtils.GetObservabilityAuthenticationScope() now returns api://9b975845-388f-4429-889e-eab1ef63949c/.default, which is what client credentials requires. This matches PROD_OBSERVABILITY_SCOPE in the Node SDK.
  • Other changes: docs, READMEs and CHANGELOG updated; version 1.1-preview → 2.0-preview.
  • Unchanged: workload MCP/Graph/OBO authentication and ServiceTokenCache.

Compatibility

This is a breaking change, hence the major version bump:

  • Delegated/OBO OBS tokens are no longer an export path; the S2S route rejects scp tokens.
  • Callers of the delegated AgenticTokenStruct registration get a compile error that points them to RefreshObservabilityToken.
  • Code that sets UseS2SEndpoint still compiles; it gets a CS0618 warning.

The SDK does not validate token claims itself. Resolvers should check that the token is app-only:

  • accept idtyp=app; when idtyp is absent, accept a non-empty roles array or oid == sub;
  • reject any scp claim;
  • check aud and expiry.

This is documented in the Runtime and Hosting READMEs.

Coordinated export configuration (CLAUDE.md)

This PR changes two of the three constants that must stay in sync. All three have been reviewed together:

Constant Value Change
ProdObservabilityScope api://9b975845-388f-4429-889e-eab1ef63949c/.default Was the delegated .../Agent365.Observability.OtelWrite scope
DefaultEndpointHost agent365.svc.cloud.microsoft Unchanged
BuildEndpointPath() /observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces Previously /observability/... unless UseS2SEndpoint was set

ExportConfigConsistencyTests is updated to the new values. The live check below uses a token for this scope, sent to this host and path.

Validation

  • Full suite: dotnet test src/Microsoft.Agents.A365.Sdk.sln gives 818 passed / 9 skipped / 827 total. Main's baseline is 791 passed / 9 skipped / 800 total, and no tests were removed.
  • Build: dotnet build finishes with 0 warnings and 0 errors.
  • Mutation checks: each check temporarily reverted one behavior and confirmed that a test fails:
    • S2S route
    • empty token fails before sending
    • resolver called once per batch
    • 401/403/404 cause no retry or fallback
    • cache early return
    • cache cleared on resolver failure
    • 1-hour boundary for tokens without exp
    • first registration wins
    • IDs that contain a colon don't share a cache entry
    • concurrent refreshes share one acquisition, and a failed refresh doesn't clear another caller's token
    • cleanup keeps resolver registrations and skips a refresh in flight
    • a cache-driven refresh doesn't restore a resolver that an explicit refresh replaced
    • default /.default scope
    • removed delegated DI contract (compile error)
  • Live check against the production Agent 365 endpoint, using a registered agent instance in a test tenant:
    • Token acquisition: blueprint FMI assertion, then the agent's client_credentials grant for the OBS /.default scope. The token has idtyp=app, no app roles and no scp, so no OtelWrite grant is involved.
    • One invoke_agent activity was exported through Agent365Exporter in three configurations:
Configuration Result
Default options POST /observabilityService/tenants/{t}/otlp/agents/{a}/traces?api-version=1 → 200, ExportResult.Success
UseS2SEndpoint = false (deprecated) Same S2S route → 200, ExportResult.Success
Resolver returns an empty token ExportResult.Failure, and no HTTP request is sent
TokenResolver backed by AgenticTokenCache.RefreshObservabilityToken(agentId, tenantId, resolver), two exports Both exports → 200. The resolver was called once and received the default scope api://9b975845-388f-4429-889e-eab1ef63949c/.default

Merge gate

Ship together with the other PRs for this change: microsoft/Agent365-nodejs#290 (Node SDK), microsoft/Agent365-python#275 (Python SDK), microsoft/Agent365-devTools#501 (CLI stops requesting OtelWrite admin consent), microsoft/Agent365-Samples#339 (samples), and microsoft/agent365-skills#84 (skills). Merge the CLI PR last.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Copilot AI balanced review requested due to automatic review settings September 29, 2026 14:09
@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Cache cleanup and concurrent refreshes can discard resolver registrations or newly acquired valid tokens.

Review effort: Balanced
Findings: 1 High severity · 1 Low severity

Open (2)
What changed in this PR

Ports observability export to the S2S-only, app-token authentication model.

Changes:

  • Always exports through /observabilityService using the /.default scope.
  • Replaces delegated hosting authentication with resolver-backed token caching.
  • Adds migration documentation, tests, and a 2.0 preview version bump.
File Description
CHANGELOG.md Documents breaking authentication changes.
src/​version.json Bumps version to 2.0 preview.
.../​ExportConfigConsistencyTests.cs Verifies coordinated S2S configuration.
.../​Agent365ExporterTests.cs Tests routing and token failures.
.../​EnvironmentUtilsTests.cs Verifies the app-only scope.
.../​BuilderTests.cs Accommodates the obsolete endpoint flag.
.../​Agent365ExporterE2ETests.cs Updates legacy-flag test handling.
.../​Agent365ExporterAsyncE2ETests.cs Updates async integration coverage.
.../​ObservabilityBuilderExtensionsTests.cs Updates hosting builder tests.
.../​ObservabilityServiceCollectionExtensionsTests.cs Verifies new cache registrations.
.../​AgenticTokenCacheTests.cs Adds resolver-cache tests.
.../​TokenResolverContext.cs Clarifies app-only behavior.
.../​AgentIdentity.cs Clarifies identity metadata semantics.
.../​Agent365ExporterOptions.cs Documents and deprecates endpoint selection.
.../​Agent365ExporterCore.cs Forces the S2S endpoint.
.../​Agent365ExporterAsync.cs Improves missing-resolver diagnostics.
.../​Agent365Exporter.cs Improves missing-resolver diagnostics.
src/​Observability/​Runtime/​README.md Documents exporter authentication.
src/​Observability/​Runtime/​docs/​design.md Updates exporter design.
.../​EnvironmentUtils.cs Changes the default OBS scope.
src/​Observability/​README.md Documents S2S-only export.
src/​Observability/​Hosting/​README.md Documents app-only hosting integration.
.../​ObservabilityServiceCollectionExtensions.cs Registers the resolver-backed cache.
...Observability.Hosting.csproj Exposes internals to tests.
src/​Observability/​Hosting/​docs/​design.md Updates token-cache architecture.
src/​Observability/​Hosting/​Caching/​README.md Adds migration and cache guidance.
.../​ObservabilityTokenResolver.cs Defines the app-only resolver delegate.
.../​IExporterTokenCache.cs Updates the cache contract documentation.
.../​AgenticTokenStruct.cs Obsoletes delegated construction.
.../​AgenticTokenCache.cs Implements resolver-based caching.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Observability/Hosting/Caching/AgenticTokenCache.cs Outdated
Comment thread src/Observability/Hosting/Caching/IExporterTokenCache.cs Outdated
… cleanup

- Serialize AgenticTokenCache.RefreshObservabilityToken per agent and tenant
  so concurrent callers share one acquisition and a failed refresh cannot
  clear a token another caller cached while it waited.
- RemoveExpiredTokens (and the cleanup timer) now clears expired token values
  instead of removing entries, so a resolver registered once keeps working
  after an idle period longer than the token lifetime. Entries with a refresh
  in flight are skipped.
- IExporterTokenCache.RegisterObservability: whether a repeated registration
  replaces the existing one is implementation-specific.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Token parsing and resolver-refresh concurrency can cause failed exports or restore stale authentication behavior.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Stale resolver snapshot can overwrite explicit refresh updates

src/​Observability/​Hosting/​Caching/​AgenticTokenCache.cs:150

GetObservabilityToken snapshots entry.TokenResolver and entry.Scopes before acquiring the per-entry refresh lock. If a concurrent explicit refresh installs a new resolver and token after that snapshot, this stale call later enters RefreshObservabilityToken, writes the old resolver back at lines 190–191, and returns the newly cached token. The next expiry then uses the old resolver, contradicting the documented guarantee that an explicit refresh replaces the resolver used by future refreshes. Have the cache-driven path acquire the entry lock and read the current resolver/scopes under that lock without overwriting them.

Medium severity Malformed tokens can bypass opaque-token fallback

src/​Observability/​Hosting/​Caching/​AgenticTokenCache.cs:328

Malformed or opaque tokens containing a period reach ReadJwtToken, which can throw SecurityTokenMalformedException; that exception is not derived from ArgumentException, so the refresh fails instead of treating the token as having no readable exp and applying the documented one-hour fallback. Check CanReadToken and catch token-parsing exceptions so all non-JWT token shapes follow the opaque-token path.

…n refresh

- GetObservabilityToken no longer snapshots the resolver before taking the
  per-entry refresh lock. Cache-driven refreshes read the current resolver
  under the lock and never write it, so a refresh queued behind an explicit
  RefreshObservabilityToken cannot restore the resolver it replaced.
- Add regression tests pinning the opaque one-hour fallback for tokens the
  JWT handler cannot parse (two segments, invalid base64url, non-JSON,
  non-numeric exp).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Copilot AI balanced review requested due to automatic review settings September 29, 2026 17:22
@DheerajPannala

Copy link
Copy Markdown
Contributor Author

Follow-up on the two "previously missed" findings in Copilot's second review (on d2b9ec8):

  1. Stale resolver snapshot (medium): fixed in 03e642d. A new test queues a cache-driven GetObservabilityToken behind an explicit RefreshObservabilityToken, and it reproduced the problem: the queued refresh wrote the replaced resolver back. Cache-driven refreshes now read the current resolver under the per-entry lock and never overwrite it, so only an explicit refresh replaces it. The test is GetObservabilityToken_QueuedBehindExplicitRefresh_DoesNotRestoreReplacedResolver, and reverting the fix makes it fail.
  2. Malformed tokens bypass the opaque fallback (medium): doesn't reproduce with the library version we ship. With System.IdentityModel.Tokens.Jwt 8.15.0, SecurityTokenMalformedException derives from SecurityTokenArgumentException, which derives from System.ArgumentException. The existing catch (ArgumentException) therefore handles it, and those tokens get the one-hour opaque fallback. I added regression tests that keep that behavior fixed if a future library version changes it. They cover a two-segment token, invalid base64url, non-JSON segments and a non-numeric exp.

Full suite: 817 passed / 9 skipped, with 0 build warnings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The breaking authentication and concurrent token-cache changes warrant final human review despite extensive automated coverage.

Review effort: Balanced
Findings: None

Previously missed (2)

In code that hasn't changed since last review

Low severity Add blank line between license header and using directive

src/​Observability/​Hosting/​Caching/​AgenticTokenCache.cs:3

The repository's required C# header format includes a blank line between the license header and the first using; this modified file currently places the using immediately after the header.

Low severity Declare scopes or use the documented three-argument overload

src/​Observability/​Hosting/​Caching/​README.md:70

This standalone example passes scopes, but the variable is never declared in the snippet, so copying it does not compile. Use the three-argument overload here (which also demonstrates the documented default /.default scope), or define the array in the example.

The resolver-cache and migration snippets passed an undeclared `scopes`
variable. Use the three-argument RefreshObservabilityToken overload, which
passes the default app-only OBS scope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Copilot AI balanced review requested due to automatic review settings September 29, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Malformed opaque tokens can throw instead of using the documented fallback lifetime, and resolver invocation cardinality is documented inaccurately.

Review effort: Balanced
Findings: None

Previously missed (4)

In code that hasn't changed since last review

Medium severity Catch malformed JWT exceptions for opaque-token fallback

src/​Observability/​Hosting/​Caching/​AgenticTokenCache.cs:347

ReadJwtToken reports malformed compact tokens with SecurityTokenMalformedException, which does not derive from ArgumentException. As a result, the new opaque-token fallback throws for cases such as the added opaque.token and not.a.jwt rows instead of applying the one-hour lifetime. Catch SecurityTokenException as well (or preflight with CanReadToken) so malformed tokens take the documented opaque-token path.

Low severity Clarify resolver runs once per identity partition

src/​Observability/​Runtime/​README.md:16

A single OpenTelemetry export batch can contain several tenant/agent identities, and the implementation invokes the resolver once for each partition, not once for the whole batch. Update this guidance to avoid underestimating resolver calls.

Low severity Document resolver calls per identity group, not per batch

src/​Observability/​Runtime/​Tracing/​Exporters/​Agent365ExporterOptions.cs:62

This invocation guarantee is inaccurate for a batch containing multiple tenant/agent identities. Agent365Exporter.Export partitions the batch and ExportBatchCoreAsync invokes the resolver inside the loop over those identity groups, so the resolver may run multiple times for one export batch. Document the actual per-identity-group behavior so consumers can size and cache their resolver correctly.

Low severity Correct resolver invocation cardinality documentation

src/​Observability/​Runtime/​docs/​design.md:101

The resolver is called inside the loop over tenant/agent partitions, so one export batch can invoke it more than once. This design documentation should describe the per-identity-group cardinality rather than promising one call for the entire batch.

@Jason-R-Lien Jason-R-Lien left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review — Agent365-dotnet #265

Verdict: Needs work · Risk: Medium — this is a breaking public authentication/export contract with one inaccurate resolver-invocation guarantee still open.

Should fix

  • src/Observability/Runtime/Tracing/Exporters/Agent365ExporterOptions.cs:62 — The public contract says the resolver runs once per export batch, but ExportBatchCoreAsync invokes it inside the tenant/agent partition loop. A mixed-identity batch can therefore invoke the resolver multiple times. Please describe this as once per tenant/agent identity group (and align src/Observability/Runtime/README.md, src/Observability/Runtime/docs/design.md, the CHANGELOG, and PR description). This matters because SDK consumers use this guarantee to size and cache token acquisition. (Customer Service, Performance, Senior Engineer · SemVer/public-contract accuracy, Azure WAF Performance Efficiency)

This exact-head issue was already raised by the structured Copilot review, so I did not add duplicate inline comments.

Persona roll-up

  • Security: No new finding. S2S-only routing is fail-closed on missing/empty token, plaintext HTTP remains rejected, no secret is logged, and CodeQL is green.
  • Privacy: No concerns; the change adds no data field, sink, retention, or residency path.
  • Performance: The per-key refresh lock and post-lock usability check correctly coalesce concurrent acquisition; the remaining documentation must state the real per-partition call cardinality.
  • Customer Service: Major-version migration, obsolete compatibility members, README/design guidance, and changelog are present; the resolver-call guarantee above remains inaccurate.
  • Business / COGS: No material storage, egress, cardinality, or CI-cost concern.
  • Senior Engineer: The prior cache-cleanup, concurrent-failure, stale-resolver, and opaque-token cases have exact-head regression coverage; no additional correctness finding survived source verification.
  • Architect: Scope, endpoint host, and S2S path are coordinated and snapshot-tested; the Node companion has merged and the remaining companion PRs are open under the documented coordinated-release plan.

Feedback-ledger and review reconciliation

  • No repository ledger entry required suppression.
  • Resolved prior cache findings were verified at a863f2bdfb902189ac8652cf8f0ceec52dc8335c.
  • The repeated malformed-token claim was suppressed: with the pinned IdentityModel version the thrown type derives from ArgumentException, and the exact-head malformed-token regressions pass in CI.
  • Low-severity header-spacing and standalone-snippet findings remain advisory and do not affect the approval gate.

Approval gate

Not approved: one consequential should-fix public-contract finding remains. All completed substantive checks are green, all review threads are resolved, and no structured review is still in progress. Merge remains subject to branch protection and the documented cross-SDK release coordination.

…ache keys

- The exporter invokes the OBS token resolver once per tenant/agent identity
  group in each export batch, not once per batch. Correct the options XML doc,
  Runtime README, design doc and CHANGELOG.
- Key AgenticTokenCache entries by an (agentId, tenantId) tuple instead of
  "agentId:tenantId", so IDs that contain a colon cannot alias another
  identity's cached token.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5cbf5f6b-cc40-4b7e-a591-65848db73a12
Copilot AI balanced review requested due to automatic review settings September 29, 2026 18:01
@DheerajPannala

Copy link
Copy Markdown
Contributor Author

Jason-R-Lien thanks, both addressed in 90d4d10:

  • Resolver call count (your should-fix): the resolver is called inside the loop over tenant/agent groups in ExportBatchCoreAsync. The docs now say it runs once per tenant/agent identity group in each export batch, so a batch with several identities calls it several times. I updated the TokenResolver XML doc, Runtime/README.md, Runtime/docs/design.md, the CHANGELOG and the PR description.
  • Same key fix as your Python finding: AgenticTokenCache now keys entries by an (agentId, tenantId) tuple instead of "agentId:tenantId". The colon form predates this PR, but it had the same aliasing problem you found in Python (Use S2S-only OBS export and app-only hosting token resolvers Agent365-python#275). The regression test RefreshObservabilityToken_SeparatorBearingIds_DoNotShareCacheEntry fails if the string key is restored.

Full suite: 818 passed / 9 skipped, 0 build warnings. On the repeated malformed-token claim: I agree with your suppression, and the regression tests from 03e642d cover it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The authentication and concurrent token-cache rewrite is security-sensitive and requires final human validation alongside the coordinated SDK rollout.

Review effort: Balanced
Findings: None

@Jason-R-Lien Jason-R-Lien left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review complete - approved

The approval gate passes at 90d4d100860f96d201f27415cacff6a193c2ca37.

Author-claimed fixes

  • Accepted - resolver invocation cardinality (90d4d10). Agent365ExporterOptions.TokenResolver, Runtime/README.md, Runtime/docs/design.md, the changelog, and the live PR summary now consistently state that the resolver runs once per tenant/agent identity group in each export batch. This matches ExportBatchCoreAsync, which resolves inside the identity-group loop. The prior should-fix is closed.
  • Accepted - separator-bearing cache identities (90d4d10). AgenticTokenCache now uses the tuple (AgentId, TenantId) as its dictionary key instead of concatenating IDs with :, and RefreshObservabilityToken_SeparatorBearingIds_DoNotShareCacheEntry pins the formerly-colliding pairs ("a:b", "c") and ("a", "b:c").
  • Accepted from the prior response - stale resolver snapshot (03e642d). Cache-driven refresh reads the current resolver under the per-entry lock and no longer restores an older resolver over an explicit replacement; the queued-get regression covers the ordering.
  • Accepted rebuttal - malformed-token opaque fallback (03e642d). With the pinned IdentityModel version, the malformed-token exception derives from ArgumentException; the exact-head regressions cover two-segment, invalid-base64url, non-JSON, and non-numeric-exp tokens. This is not an open finding.
  • Accepted from the earlier thread replies - concurrent refresh and cleanup (d2b9ec8). Per-entry locking, the post-lock usability check, and cleanup's preservation of resolver registrations are present with concurrency/lifecycle regressions.

Review evidence

  • Security: S2S-only authentication remains fail-closed; no new secret, delegated fallback, or isolation issue was introduced by this delta.
  • Privacy: No new data field, sink, retention, residency, or personal-data flow.
  • Performance: Tuple lookup remains O(1), and per-key refresh coalescing is unchanged.
  • Customer Service: The breaking resolver contract and migration guidance now describe the implementation accurately.
  • Business / COGS: No material storage, egress, cardinality, or paid-dependency change.
  • Senior Engineer: The delta is limited to contract alignment plus collision-safe cache identity and a focused regression; no new correctness or edge-case finding survived review.
  • Architect: The coordinated S2S host/scope/path contract is unchanged by this delta. The PR's cross-SDK merge order remains an operator merge gate, not an unresolved defect in this exact head.

All review threads are resolved. The exact-head Copilot structured review reports no findings. All completed substantive checks, including .NET SDK, both C# analyses, CodeQL, dependency review, and CLA, are green. A local targeted rerun could not restore from NuGet because the TLS handshake failed; exact-head CI is therefore the authoritative test result. Merge remains subject to branch protection and the documented coordinated-release order.

@DheerajPannala
Krishnadheeraj (DheerajPannala) merged commit 7c05574 into main Sep 30, 2026
8 checks passed
@DheerajPannala
Krishnadheeraj (DheerajPannala) deleted the users/DheerajPannala/obs-s2s-only-20260929 branch September 30, 2026 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants