-
Notifications
You must be signed in to change notification settings - Fork 3.1k
fix(onboard): probe registered extra providers exactly #6596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
b8204d2
fix(onboard): probe registered extra providers exactly
apurvvkumaria c66486b
test(onboard): keep provider probe fixtures linear
apurvvkumaria a55ce25
test(onboard): remove obsolete provider-list mock
apurvvkumaria 7ebcab1
fix(onboard): recognize wrapped provider errors
apurvvkumaria a0ea6e6
fix(onboard): handle real probe output safely
apurvvkumaria cb7977f
fix(onboard): surface indeterminate provider probes
apurvvkumaria b23bfea
test(onboard): cover stale extra providers on resume repair
prekshivyas c0b2d16
Merge remote-tracking branch 'origin/main' into codex/6501-strict-ext…
prekshivyas f2516d3
test(onboard): avoid repair conditional growth
prekshivyas d6b1248
merge(main): sync v0.0.80 branch
cv ca48d51
Merge branch 'main' into codex/6501-strict-extra-provider-reconciliation
prekshivyas 5bc41db
Merge remote branch 'codex/6501-strict-extra-provider-reconciliation'
prekshivyas 043cfcd
test(onboard): cover resume stale provider reconciliation
prekshivyas e32a26c
fix(onboard): parse wrapped provider diagnostics
prekshivyas 4fd6a33
fix(onboard): harden extra provider reconciliation
prekshivyas d458c26
fix(onboard): prune stale extra providers
prekshivyas 14d1fc8
fix(onboard): honor resume sandbox recreation
prekshivyas 78fa183
chore(onboard): keep entrypoint size neutral
prekshivyas d6adbfd
test(onboard): split resume recreation coverage
prekshivyas 85d092d
fix(onboard): parse issue-form provider diagnostics
prekshivyas 81668f2
fix(onboard): parse provider get stale diagnostics
prekshivyas f4f7be4
fix(onboard): parse structured provider not-found diagnostics
prekshivyas a52b5ac
fix(onboard): reconcile extra providers in machine create
prekshivyas 04e17df
chore(onboard): keep create wiring net-neutral
prekshivyas d1b7085
Merge remote-tracking branch 'origin/main' into codex/6501-strict-ext…
prekshivyas 7e07ab3
test(e2e): record onboard mock parity
prekshivyas 7ea6dcd
fix(onboard): preserve empty reconciled extra providers
prekshivyas 5671f09
test(e2e): prove extra provider resume filtering
prekshivyas 974ac86
fix(onboard): accept wrapped provider not-found diagnostics
prekshivyas ed92e4c
Merge remote-tracking branch 'origin/main' into codex/6501-strict-ext…
prekshivyas d0fbf2d
fix(onboard): prune targeted missing extra providers
prekshivyas e69a6a0
test(sandbox): budget MCP status subprocess suite
prekshivyas 1a6885b
test(onboard): cover extra provider retry stability
prekshivyas cb0a4c8
test(onboard): isolate gateway recovery coredns patch
prekshivyas bc4a194
test(policy): isolate policy list docker guidance
prekshivyas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,237 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| const DIAGNOSTIC_PREFIXES = ["error:", "rpc error:", "status:"]; | ||
| const NOT_FOUND_SUFFIXES = new Set([ | ||
| "not found", | ||
| "notfound", | ||
| "is not found", | ||
| "is notfound", | ||
| "was not found", | ||
| "was notfound", | ||
| ]); | ||
|
|
||
| // Parser helpers deliberately default to null/false for malformed or unknown | ||
| // diagnostics. The caller treats that as `ambiguous-diagnostic`, preserving the | ||
| // provider and emitting one redacted aggregate warning for observability. | ||
| function stripAnsi(value: string): string { | ||
| return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/gu, ""); | ||
| } | ||
|
|
||
| function stripIssueMarker(text: string): string { | ||
| const trimmed = text.trimStart(); | ||
| return trimmed.startsWith("×") ? trimmed.slice(1).trimStart() : trimmed; | ||
| } | ||
|
|
||
| function stripIssueDecoration(line: string): string { | ||
| const trimmed = stripAnsi(line).trim(); | ||
| const withoutPipe = trimmed.startsWith("│") ? trimmed.slice(1).trimStart() : trimmed; | ||
| return stripIssueMarker(withoutPipe); | ||
| } | ||
|
|
||
| function joinDiagnosticLines(lines: string[]): string { | ||
| return lines | ||
| .reduce((message, line) => { | ||
| const part = line.trim(); | ||
| if (!part) return message; | ||
| return message.endsWith("-") ? `${message}${part}` : `${message} ${part}`; | ||
| }, "") | ||
| .trim(); | ||
| } | ||
|
|
||
| function stripDiagnosticPrefixes(line: string): string { | ||
| let text = stripIssueDecoration(line); | ||
| for (let attempt = 0; attempt < 10; attempt += 1) { | ||
| const lower = text.toLowerCase(); | ||
| const prefix = DIAGNOSTIC_PREFIXES.find((candidate) => lower.startsWith(candidate)); | ||
| if (!prefix) return text; | ||
| text = stripIssueMarker(text.slice(prefix.length)); | ||
| } | ||
| return text; | ||
| } | ||
|
|
||
| function readQuotedValue(text: string, searchStart = 0): { value: string; end: number } | null { | ||
| const quoteIndex = ["'", '"', "`"] | ||
| .map((quote) => ({ quote, index: text.indexOf(quote, searchStart) })) | ||
| .filter(({ index }) => index >= 0) | ||
| .sort((left, right) => left.index - right.index)[0]; | ||
| if (!quoteIndex) return null; | ||
| const end = text.indexOf(quoteIndex.quote, quoteIndex.index + 1); | ||
| return end >= 0 ? { value: text.slice(quoteIndex.index + 1, end), end: end + 1 } : null; | ||
| } | ||
|
|
||
| function lineReportsMissingGateway(line: string): boolean { | ||
| const lower = line.replace(/'[^']*'|"[^"]*"|`[^`]*`/gu, "").toLowerCase(); | ||
| return ( | ||
| lower.includes("unknown gateway") || | ||
| lower.includes("no such gateway") || | ||
| lower.includes("notfound: gateway") || | ||
| (lower.includes("gateway") && | ||
| (lower.includes("does not exist") || | ||
| lower.includes("not found") || | ||
| lower.includes("notfound"))) | ||
| ); | ||
| } | ||
|
|
||
| function structuredStatusValue(line: string): string | null { | ||
| const lower = line.toLowerCase(); | ||
| for (const key of ["status", "code"]) { | ||
| const keyIndex = lower.indexOf(key); | ||
| if (keyIndex < 0) continue; | ||
| let cursor = keyIndex + key.length; | ||
| while (/\s/u.test(line[cursor] ?? "")) cursor += 1; | ||
| if (line[cursor] !== ":" && line[cursor] !== "=") continue; | ||
| cursor += 1; | ||
| while (/[\s"']/u.test(line[cursor] ?? "")) cursor += 1; | ||
| if (line.slice(cursor).toLowerCase().startsWith("some requested entity was not found")) { | ||
| return "notfound"; | ||
| } | ||
| const start = cursor; | ||
| while (/[a-z_-]/iu.test(line[cursor] ?? "")) cursor += 1; | ||
| return line.slice(start, cursor); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| function normalizeStatus(value: string): string { | ||
| return value.replaceAll("_", "").replaceAll("-", "").toLowerCase(); | ||
| } | ||
|
|
||
| function normalizedNotFoundSuffix(value: string): string { | ||
| return value | ||
| .replace(/[.!]+$/u, "") | ||
| .trim() | ||
| .toLowerCase(); | ||
| } | ||
|
|
||
| function providerNameFromNotFoundLine(line: string): string | null { | ||
| return ( | ||
| providerNameFromNotFoundText(stripDiagnosticPrefixes(line)) ?? providerNameFromMessage(line) | ||
| ); | ||
| } | ||
|
|
||
| function providerNameFromNotFoundText(text: string): string | null { | ||
| let hasNotFoundStatusPrefix = false; | ||
| if (text.toLowerCase().startsWith("notfound:")) { | ||
| text = text.slice("notfound:".length).trimStart(); | ||
| hasNotFoundStatusPrefix = true; | ||
| } | ||
| const providerPrefix = "provider "; | ||
| if (!text.toLowerCase().startsWith(providerPrefix)) return null; | ||
| const quoted = readQuotedValue(text, providerPrefix.length); | ||
| if (!quoted) return null; | ||
| const suffix = normalizedNotFoundSuffix(text.slice(quoted.end)); | ||
| return suffix === "" && hasNotFoundStatusPrefix | ||
| ? quoted.value | ||
| : NOT_FOUND_SUFFIXES.has(suffix) | ||
| ? quoted.value | ||
| : null; | ||
| } | ||
|
|
||
| function providerNameFromMessage(line: string): string | null { | ||
| const text = stripDiagnosticPrefixes(line); | ||
| const markerIndex = text.toLowerCase().indexOf("message:"); | ||
| if (markerIndex < 0) return null; | ||
| let cursor = markerIndex + "message:".length; | ||
| while (/\s/u.test(text[cursor] ?? "")) cursor += 1; | ||
| const quote = text[cursor]; | ||
| if (quote === "'" || quote === '"' || quote === "`") { | ||
| const end = text.indexOf(quote, cursor + 1); | ||
| if (end < 0) return null; | ||
| return providerNameFromNotFoundText(text.slice(cursor + 1, end)); | ||
| } | ||
| return providerNameFromNotFoundText(text.slice(cursor)); | ||
| } | ||
|
|
||
| function readMessageValue(line: string): string | null { | ||
| const text = stripDiagnosticPrefixes(line); | ||
| const markerIndex = text.toLowerCase().indexOf("message:"); | ||
| if (markerIndex < 0) return null; | ||
| const quoted = readQuotedValue(text, markerIndex + "message:".length); | ||
| if (quoted) return quoted.value; | ||
| return text.slice(markerIndex + "message:".length).trim(); | ||
| } | ||
|
|
||
| function lineReportsTargetedProviderGetNotFound(line: string): boolean { | ||
| const text = stripDiagnosticPrefixes(line); | ||
| if (normalizedNotFoundSuffix(text) === "provider not found") return true; | ||
| const status = structuredStatusValue(line); | ||
| if (!status || normalizeStatus(status) !== "notfound") return false; | ||
| return normalizedNotFoundSuffix(readMessageValue(line) ?? "") === "provider not found"; | ||
| } | ||
|
|
||
| function commandNameAfterMarker(text: string, marker: string): string | null { | ||
| const markerIndex = text.toLowerCase().indexOf(marker.toLowerCase()); | ||
| if (markerIndex < 0) return null; | ||
| let cursor = markerIndex + marker.length; | ||
| while (/\s/u.test(text[cursor] ?? "")) cursor += 1; | ||
| const start = cursor; | ||
| while (cursor < text.length && !/[\s`]/u.test(text[cursor] ?? "")) cursor += 1; | ||
| return cursor > start ? text.slice(start, cursor) : null; | ||
| } | ||
|
|
||
| function wrappedIssueDiagnosticMatches( | ||
| issueDiagnostic: string, | ||
| providerName: string, | ||
| ): boolean | null { | ||
| const text = stripDiagnosticPrefixes(issueDiagnostic).replace(/^\s*×\s*/u, ""); | ||
| const lower = text.toLowerCase(); | ||
| const providerIndex = lower.indexOf("provider "); | ||
| const hasWrappedIssueShape = | ||
| providerIndex >= 0 && | ||
| lower.includes(" not found and ") && | ||
| lower.includes(" is not a recognized provider type"); | ||
| if (!hasWrappedIssueShape) return null; | ||
|
|
||
| const firstProvider = readQuotedValue(text, providerIndex); | ||
| const secondProvider = firstProvider | ||
| ? readQuotedValue(text, lower.indexOf(" and ", firstProvider.end)) | ||
| : null; | ||
| const commandProvider = commandNameAfterMarker(text, "--name "); | ||
| return ( | ||
| firstProvider?.value === providerName && | ||
| secondProvider?.value === providerName && | ||
| (commandProvider === null || commandProvider === providerName) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Accept only diagnostics that bind "not found" to this exact quoted provider. | ||
| * | ||
| * OpenShell currently renders both `provider 'name' not found` and the gRPC | ||
| * ordering `NotFound: provider "name"`. Keeping these shapes narrow matters: | ||
| * gateway failures can mention the provider being queried, but must remain | ||
| * indeterminate so onboarding does not silently drop a healthy attachment. | ||
| */ | ||
| export function reportsExactProviderNotFound( | ||
| output: string, | ||
| providerName: string, | ||
| diagnosticLimit: number, | ||
| ): boolean { | ||
| const lines = output.slice(0, diagnosticLimit).split(/\r?\n/); | ||
| const diagnosticLines = lines.map(stripIssueDecoration).filter(Boolean); | ||
| if (diagnosticLines.length === 0) return false; | ||
| if (diagnosticLines.some(lineReportsMissingGateway)) return false; | ||
| if ( | ||
| diagnosticLines.some((line) => { | ||
| const status = structuredStatusValue(line); | ||
| return Boolean(status && normalizeStatus(status) !== "notfound"); | ||
| }) | ||
| ) { | ||
| return false; | ||
| } | ||
|
|
||
| const wrappedIssueMatch = wrappedIssueDiagnosticMatches( | ||
| joinDiagnosticLines(diagnosticLines), | ||
| providerName, | ||
| ); | ||
| if (wrappedIssueMatch !== null) return wrappedIssueMatch; | ||
| if ( | ||
| diagnosticLines.length === 1 && | ||
| lineReportsTargetedProviderGetNotFound(diagnosticLines[0] ?? "") | ||
| ) { | ||
| return true; | ||
| } | ||
|
|
||
| return diagnosticLines.every((line) => providerNameFromNotFoundLine(line) === providerName); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.