[Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

Description

@eddy-curly

Before submitting

  • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
  • I included enough detail to reproduce or investigate the problem.

Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

Area

apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

Summary

Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

Observed user-facing output — the entire diagnostic signal:

Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.

The real error, which never reaches the user or any log:

ERROR: TF400813: The user '<guid>' is not authorized to access this resource.

The three defects:

  1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
  2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
  3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

Steps to reproduce

The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

Concretely, on the reporting machine:

tenantidentity
az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
  1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
  2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
  3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
$ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
  1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
  2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
$ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
$ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

Expected behavior

  • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
  • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
  • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

Actual behavior

  • Classification falls through to command-failed.
  • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
  • The TF400813 text is discarded and appears in no log.
  • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
  • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

Root cause (code references, at main @ 510393e8)

1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

Verified by running the shipped classifier verbatim against real stderr:

classifyNonZeroExit("az", stderr)stderr
command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

Suggested fix

  1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

"is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

  1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

  2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

Relationship to other issues

  • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

Note on scope

This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

      Description

      @eddy-curly

      Before submitting

      • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
      • I included enough detail to reproduce or investigate the problem.

      Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

      Area

      apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

      Summary

      Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

      Observed user-facing output — the entire diagnostic signal:

      Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.
      

      The real error, which never reaches the user or any log:

      ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
      

      The three defects:

      1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
      2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
      3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

      Steps to reproduce

      The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

      Concretely, on the reporting machine:

      tenantidentity
      az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
      ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
      1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
      2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
      3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
      $ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
      1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
      2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
      $ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
      $ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

      Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

      Expected behavior

      • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
      • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
      • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

      Actual behavior

      • Classification falls through to command-failed.
      • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
      • The TF400813 text is discarded and appears in no log.
      • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
      • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

      Root cause (code references, at main @ 510393e8)

      1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

      Verified by running the shipped classifier verbatim against real stderr:

      classifyNonZeroExit("az", stderr)stderr
      command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
      command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
      command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
      authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

      The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

      2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

      3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

      Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

      Suggested fix

      1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
      normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

      "is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

      1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

      2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

      Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

      A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

      Relationship to other issues

      • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

      Note on scope

      This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

          Description

          @eddy-curly

          Before submitting

          • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
          • I included enough detail to reproduce or investigate the problem.

          Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

          Area

          apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

          Summary

          Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

          Observed user-facing output — the entire diagnostic signal:

          Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.
          

          The real error, which never reaches the user or any log:

          ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
          

          The three defects:

          1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
          2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
          3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

          Steps to reproduce

          The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

          Concretely, on the reporting machine:

          tenantidentity
          az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
          ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
          1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
          2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
          3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
          $ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
          1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
          2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
          $ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
          $ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

          Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

          Expected behavior

          • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
          • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
          • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

          Actual behavior

          • Classification falls through to command-failed.
          • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
          • The TF400813 text is discarded and appears in no log.
          • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
          • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

          Root cause (code references, at main @ 510393e8)

          1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

          Verified by running the shipped classifier verbatim against real stderr:

          classifyNonZeroExit("az", stderr)stderr
          command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
          command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
          command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
          authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

          The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

          2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

          3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

          Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

          Suggested fix

          1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
          normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

          "is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

          1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

          2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

          Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

          A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

          Relationship to other issues

          • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

          Note on scope

          This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

              Description

              @eddy-curly

              Before submitting

              • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
              • I included enough detail to reproduce or investigate the problem.

              Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

              Area

              apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

              Summary

              Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

              Observed user-facing output — the entire diagnostic signal:

              Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.
              

              The real error, which never reaches the user or any log:

              ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
              

              The three defects:

              1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
              2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
              3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

              Steps to reproduce

              The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

              Concretely, on the reporting machine:

              tenantidentity
              az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
              ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
              1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
              2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
              3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
              $ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
              1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
              2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
              $ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
              $ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

              Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

              Expected behavior

              • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
              • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
              • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

              Actual behavior

              • Classification falls through to command-failed.
              • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
              • The TF400813 text is discarded and appears in no log.
              • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
              • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

              Root cause (code references, at main @ 510393e8)

              1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

              Verified by running the shipped classifier verbatim against real stderr:

              classifyNonZeroExit("az", stderr)stderr
              command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
              command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
              command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
              authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

              The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

              2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

              3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

              Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

              Suggested fix

              1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
              normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

              "is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

              1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

              2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

              Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

              A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

              Relationship to other issues

              • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

              Note on scope

              This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

                  Description

                  @eddy-curly

                  Before submitting

                  • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
                  • I included enough detail to reproduce or investigate the problem.

                  Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

                  Area

                  apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

                  Summary

                  Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

                  Observed user-facing output — the entire diagnostic signal:

                  Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.
                  

                  The real error, which never reaches the user or any log:

                  ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                  

                  The three defects:

                  1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
                  2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
                  3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

                  Steps to reproduce

                  The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

                  Concretely, on the reporting machine:

                  tenantidentity
                  az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
                  ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
                  1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
                  2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
                  3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
                  $ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                  1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
                  2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
                  $ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
                  $ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

                  Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

                  Expected behavior

                  • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
                  • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
                  • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

                  Actual behavior

                  • Classification falls through to command-failed.
                  • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
                  • The TF400813 text is discarded and appears in no log.
                  • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
                  • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

                  Root cause (code references, at main @ 510393e8)

                  1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

                  Verified by running the shipped classifier verbatim against real stderr:

                  classifyNonZeroExit("az", stderr)stderr
                  command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                  command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
                  command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
                  authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

                  The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

                  2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

                  3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

                  Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

                  Suggested fix

                  1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
                  normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

                  "is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

                  1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

                  2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

                  Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

                  A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

                  Relationship to other issues

                  • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

                  Note on scope

                  This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

                      Description

                      @eddy-curly

                      Before submitting

                      • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
                      • I included enough detail to reproduce or investigate the problem.

                      Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

                      Area

                      apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

                      Summary

                      Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

                      Observed user-facing output — the entire diagnostic signal:

                      Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.
                      

                      The real error, which never reaches the user or any log:

                      ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                      

                      The three defects:

                      1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
                      2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
                      3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

                      Steps to reproduce

                      The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

                      Concretely, on the reporting machine:

                      tenantidentity
                      az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
                      ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
                      1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
                      2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
                      3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
                      $ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                      1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
                      2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
                      $ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
                      $ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

                      Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

                      Expected behavior

                      • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
                      • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
                      • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

                      Actual behavior

                      • Classification falls through to command-failed.
                      • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
                      • The TF400813 text is discarded and appears in no log.
                      • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
                      • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

                      Root cause (code references, at main @ 510393e8)

                      1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

                      Verified by running the shipped classifier verbatim against real stderr:

                      classifyNonZeroExit("az", stderr)stderr
                      command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                      command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
                      command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
                      authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

                      The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

                      2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

                      3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

                      Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

                      Suggested fix

                      1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
                      normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

                      "is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

                      1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

                      2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

                      Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

                      A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

                      Relationship to other issues

                      • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

                      Note on scope

                      This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

                          Description

                          @eddy-curly

                          Before submitting

                          • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
                          • I included enough detail to reproduce or investigate the problem.

                          Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

                          Area

                          apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

                          Summary

                          Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

                          Observed user-facing output — the entire diagnostic signal:

                          Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.
                          

                          The real error, which never reaches the user or any log:

                          ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                          

                          The three defects:

                          1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
                          2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
                          3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

                          Steps to reproduce

                          The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

                          Concretely, on the reporting machine:

                          tenantidentity
                          az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
                          ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
                          1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
                          2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
                          3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
                          $ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                          1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
                          2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
                          $ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
                          $ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

                          Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

                          Expected behavior

                          • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
                          • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
                          • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

                          Actual behavior

                          • Classification falls through to command-failed.
                          • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
                          • The TF400813 text is discarded and appears in no log.
                          • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
                          • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

                          Root cause (code references, at main @ 510393e8)

                          1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

                          Verified by running the shipped classifier verbatim against real stderr:

                          classifyNonZeroExit("az", stderr)stderr
                          command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                          command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
                          command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
                          authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

                          The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

                          2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

                          3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

                          Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

                          Suggested fix

                          1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
                          normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

                          "is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

                          1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

                          2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

                          Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

                          A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

                          Relationship to other issues

                          • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

                          Note on scope

                          This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [Bug]: every Azure DevOps auth failure is reported as "Azure DevOps CLI command failed" - TF400813 is unclassified, stderr is discarded, and the auth probe checks ARM instead of ADO #122

                              Description

                              @eddy-curly

                              Before submitting

                              • I searched existing issues and did not find a duplicate. (Closest is this fork's #4 — same connector, a different root cause. See "Relationship to other issues".)
                              • I included enough detail to reproduce or investigate the problem.

                              Edited after filing: the original version of this issue mis-stated the precondition as "an Azure DevOps org not backed by an Entra tenant (MSA-backed)", citing X-VSS-ResourceTenant: 00000000-0000-0000-0000-000000000000. That header reading was taken from an HTTP 302 redirect to the sign-in page and is not authoritative — the org in question is Entra-backed (member descriptors carry the aad. prefix). The actual precondition is a multi-tenant identity mismatch, described below. The three code defects and all classifier evidence are unchanged; only the repro scenario is corrected.

                              Area

                              apps/servervcs/VcsProcess.ts (error classification), packages/contracts/src/vcs.ts (error construction), sourceControl/AzureDevOpsSourceControlProvider.ts (auth discovery probe).

                              Summary

                              Three defects combine so that every Azure DevOps authorization failure is reported as an opaque generic error, with the actionable message destroyed, while the Settings UI simultaneously claims the provider is authenticated.

                              Observed user-facing output — the entire diagnostic signal:

                              Source control provider azure-devops failed in listChangeRequests: Azure DevOps CLI command failed.
                              

                              The real error, which never reaches the user or any log:

                              ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                              

                              The three defects:

                              1. classifyNonZeroExit recognises no Azure DevOps auth error. It greps stderr for the literal substring "unauthorized"; Azure DevOps' wire format is "is not authorized", which does not contain it. TF400813 / VS30063 are not matched at all. Every ADO auth failure therefore classifies as command-failed.
                              2. VcsProcessExitError.fromProcessExit discards the stderr text. It records stderrLength and stderrTruncated but never the stderr body, and rebuilds detail purely from failureKind. So a misclassification is not merely a wrong label — it permanently destroys the only copy of the real error. Diagnosing this required unpacking resources/server.asar.
                              3. The auth discovery probe checks the wrong plane.discovery.authArgs runs az account show, which reports Azure Resource Manager auth for whatever tenant is currently active. ADO auth is a separate token audience (499b84ac-1321-427f-aa17-267ca6975798) and, critically, is resolved per-tenant. A user can pass the probe and fail every ADO call — and vice versa.

                              Steps to reproduce

                              The precondition is an identity that exists in two Entra tenants, where the tenant backing the ADO org is notaz's active tenant. This is ordinary for contractors and vendor-hosted repos: you are a member of your own tenant (which backs the ADO org) and a guest in a customer tenant (which owns the Azure subscriptions). az login selects the tenant with the subscriptions; the ADO org trusts the other one.

                              Concretely, on the reporting machine:

                              tenantidentity
                              az active context<customer-tenant> (owns all 5 subscriptions)guest: user_example.com#EXT#@customer.onmicrosoft.com
                              ADO org membership<home-tenant> (owns example.com)member, aad. descriptor
                              1. az login. Because the subscriptions live in the customer tenant, that becomes active. az account show returns a user, so the discovery probe is satisfied and Settings shows Azure DevOps as authenticated.
                              2. az devops configure -l shows a correct default org/project, and git ls-remote against the same repo succeeds (Git Credential Manager holds a working credential for the home-tenant identity).
                              3. Every ADO call fails, because az presents the guest identity from the wrong tenant:
                              $ az repos pr list --organization https://<org>.visualstudio.com/ --project <proj> --repository <repo>ERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                              1. Open the PR/change-request list in T3 Code. It fails with Azure DevOps CLI command failed. and nothing else.
                              2. Confirm the CLI and the account are both fine — the sameaz install, asked for a token in the home tenant, is authorized:
                              $ TOK=$(az account get-access-token --tenant <home-tenant> \ --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)
                              $ curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOK" \ "https://dev.azure.com/<org>/_apis/projects?api-version=7.0"200

                              Note that az devops / az repos expose no --tenant flag — they inherit the active CLI context — which is precisely why a probe on the ARM plane says nothing about ADO reachability.

                              Expected behavior

                              • TF400813 / VS30063 / "is not authorized" classify as authentication, so the existing AzureDevOpsCliAuthenticationError fires and the user sees its already-written, actionable hint: "Azure DevOps CLI is not authenticated. Run az devops login and retry."
                              • The underlying stderr (redacted/truncated as needed) survives into the error and/or the server log, so a command-failed fallback is still diagnosable without unpacking the app bundle.
                              • The auth probe reflects Azure DevOps reachability, not ARM. A PAT-only session (az devops login, no az login) should read as authenticated; an ARM-only session that cannot reach ADO should not.

                              Actual behavior

                              • Classification falls through to command-failed.
                              • detail becomes "Process exited with a non-zero status.", which AzureDevOpsCommandFailedError renders as "Azure DevOps CLI command failed.".
                              • The TF400813 text is discarded and appears in no log.
                              • Settings continues to show the provider as authenticated, because az account show still succeeds — against the wrong tenant.
                              • Net effect: the one error class that has a helpful remediation hint written for it is unreachable for the most common ADO auth failure.

                              Root cause (code references, at main @ 510393e8)

                              1. apps/server/src/vcs/VcsProcess.ts:53-86classifyNonZeroExit. The auth arm (lines 56-65) matches "unauthorized" at line 64 but never "is not authorized", TF400813, or VS30063. ADO auth failures reach return "command-failed" at line 86.

                              Verified by running the shipped classifier verbatim against real stderr:

                              classifyNonZeroExit("az", stderr)stderr
                              command-failedERROR: TF400813: The user '<guid>' is not authorized to access this resource.
                              command-failedERROR: VS30063: You are not authorized to access https://dev.azure.com.
                              command-failedERROR: TF400813: Resource not available for anonymous access. Client authentication required.
                              authenticationerror: not logged in to any GitHub hosts(control — gh is matched correctly)

                              The asymmetry is the bug: gh's auth vocabulary is covered, az's is not.

                              2. packages/contracts/src/vcs.ts:129-153fromProcessExit. detail is derived only from failureKind (line 143 for the fallback); the constructor persists stderrLength (line 150) and stderrTruncated (line 151) but not error.stderr itself. There is no field on VcsProcessExitError that can carry it (detail: Schema.String at line 119 is the derived string).

                              3. apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts:41-51discovery.authArgs at line 47 is ["account", "show", "--query", "user.name", "-o", "tsv"], and parseAzureAuth (lines 15-39) maps a successful az account show to status: "authenticated", host: "dev.azure.com". az account show never contacts dev.azure.com, and in the multi-tenant case above it succeeds while ADO is unreachable. Note the fallback detail at line 22 already says "Run az login", which is the wrong instruction for ADO — az devops login is what sets ADO credentials.

                              Environment on the reporting machine: T3 Coil (Alpha) 0.0.33-coil.122, Windows 11 Pro 26200, azure-cli 2.88.0, azure-devops extension 1.0.6, GCM 2.7.3. az is on PATH and az account show returns in ~1 s, so this is not #4.

                              Suggested fix

                              1. Add ADO auth vocabulary to the auth arm in classifyNonZeroExit:
                              normalized.includes("is not authorized")||normalized.includes("tf400813")||normalized.includes("vs30063")||normalized.includes("client authentication required")||

                              "is not authorized" also subsumes the VS30063 phrasing, but matching the codes explicitly keeps it robust to Microsoft's wording changes.

                              1. Preserve stderr. Add an optional redacted/truncated stderrExcerpt (or similar) to VcsProcessExitError and populate it in fromProcessExit, at minimum for the command-failed fallback where no derived detail is meaningful. Even logging it at debug level on the server would have made this self-diagnosable.

                              2. Probe the ADO plane. az devops project list --query "[0].name" -o tsv (or az devops configure -l plus one cheap ADO call) reflects actual ADO reachability. If keeping az account show as a cheap pre-check, do not report host: "dev.azure.com" on its result alone, and correct the remediation hint to az devops login.

                              Worth noting for whoever picks this up: fixing (1) alone makes this class of failure self-service, since AzureDevOpsCliAuthenticationError already carries the correct instruction. Fixing (2) is what prevents the next unclassified ADO error from being equally opaque. And (3) is what stops the Settings UI from actively asserting the opposite of the truth.

                              A further enhancement worth considering, since the multi-tenant case is common: surface the tenant mismatch specifically. When az account show reports tenant A and the ADO call fails with TF400813, the useful message is "your Azure CLI is signed in to tenant A, but this organization is backed by a different tenant — run az devops login with a PAT, or az login --tenant <B>."

                              Relationship to other issues

                              • This fork's #4 (closed): same connector, and also a misclassification, but there the version probe timed out and was collapsed into status: "missing". Here the CLI is fast and detected fine; the failure is in non-zero-exit classification and in the auth probe checking ARM instead of ADO. Independent root causes, adjacent theme: the ADO connector collapses distinguishable failure modes into a single wrong bucket and drops the evidence.

                              Note on scope

                              This is filed against the fork because that is where it was found. Defects (1) and (2) are provider-agnostic and live in shared code, so they likely apply upstream (pingdotgg/t3code) unchanged.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions