diff --git a/ext-common-sha.txt b/ext-common-sha.txt
index fe38af40..0154d725 100644
--- a/ext-common-sha.txt
+++ b/ext-common-sha.txt
@@ -1 +1 @@
-3f8c4dfdf46ce0c9f623fac82d5e822dea80f8bf
+9691bd89cc9df2641055fd7c55688fbd83ebd7f6
diff --git a/ext/Lidarr.Plugin.Common b/ext/Lidarr.Plugin.Common
index 3f8c4dfd..9691bd89 160000
--- a/ext/Lidarr.Plugin.Common
+++ b/ext/Lidarr.Plugin.Common
@@ -1 +1 @@
-Subproject commit 3f8c4dfdf46ce0c9f623fac82d5e822dea80f8bf
+Subproject commit 9691bd89cc9df2641055fd7c55688fbd83ebd7f6
diff --git a/tests/Tidalarr.Parity.Tests/CommonPinDriftTests.cs b/tests/Tidalarr.Parity.Tests/CommonPinDriftTests.cs
new file mode 100644
index 00000000..c6fd1559
--- /dev/null
+++ b/tests/Tidalarr.Parity.Tests/CommonPinDriftTests.cs
@@ -0,0 +1,86 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using Xunit;
+
+namespace Tidalarr.Parity.Tests;
+
+///
+/// LOOP-001 drift gate: the pinned Common version is recorded in ext-common-sha.txt (the greppable,
+/// reviewable sentinel), and the version actually compiled into the plugin is whatever the
+/// ext/Lidarr.Plugin.Common submodule has checked out. Those MUST agree. When they diverge (the apple
+/// failure mode: sentinel says one SHA, the submodule is another), reviews and the build disagree about which
+/// Common is in the plugin.
+///
+/// We compare the sentinel against the submodule's actual checked-out HEAD (git -C ext/... rev-parse HEAD)
+/// rather than the committed gitlink: that holds in a dirty local re-pin (sentinel + submodule updated together,
+/// pre-commit) AND in CI — a clean checkout materializes the submodule at the committed gitlink, so a commit
+/// that bumped the sentinel but forgot to stage the submodule still surfaces here.
+///
+[Trait("Category", "Parity")]
+public class CommonPinDriftTests
+{
+ private static string RepoRoot =>
+ Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
+
+ [Fact]
+ public void ExtCommonShaSentinel_IsAFortyHexSha()
+ {
+ var shaFile = Path.Combine(RepoRoot, "ext-common-sha.txt");
+ Assert.True(File.Exists(shaFile), $"ext-common-sha.txt sentinel missing at {shaFile}");
+
+ var declared = File.ReadAllText(shaFile).Trim();
+ Assert.Matches("^[0-9a-f]{40}$", declared);
+ }
+
+ [Fact]
+ public void ExtCommonSha_MatchesCheckedOutSubmodule()
+ {
+ var declared = File.ReadAllText(Path.Combine(RepoRoot, "ext-common-sha.txt")).Trim();
+
+ var submoduleHead = TryRevParseHead(Path.Combine(RepoRoot, "ext", "Lidarr.Plugin.Common"));
+ if (submoduleHead is null)
+ {
+ // git unavailable or submodule not initialized (e.g. an exported source snapshot) — inconclusive.
+ return;
+ }
+
+ Assert.True(
+ string.Equals(declared, submoduleHead, StringComparison.OrdinalIgnoreCase),
+ $"Common pin drift: ext-common-sha.txt = {declared} but the checked-out submodule = {submoduleHead}. " +
+ "Re-pin both together (update the submodule AND the sentinel) so reviews and the build agree.");
+ }
+
+ /// Returns the checked-out HEAD SHA of the git repo at , or null when git
+ /// is unavailable / the directory is not a git checkout.
+ private static string? TryRevParseHead(string repoDir)
+ {
+ try
+ {
+ var psi = new ProcessStartInfo("git", $"-C \"{repoDir}\" rev-parse HEAD")
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+ using var p = Process.Start(psi);
+ if (p is null)
+ {
+ return null;
+ }
+
+ var output = p.StandardOutput.ReadToEnd();
+ if (!p.WaitForExit(5000) || p.ExitCode != 0)
+ {
+ return null;
+ }
+
+ var m = Regex.Match(output, "([0-9a-f]{40})");
+ return m.Success ? m.Groups[1].Value : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/tests/Tidalarr.Tests/TidalModuleEndToEndDiFlowsTests.cs b/tests/Tidalarr.Tests/TidalModuleEndToEndDiFlowsTests.cs
index 235e267d..377faceb 100644
--- a/tests/Tidalarr.Tests/TidalModuleEndToEndDiFlowsTests.cs
+++ b/tests/Tidalarr.Tests/TidalModuleEndToEndDiFlowsTests.cs
@@ -107,7 +107,9 @@ public async Task DI_Flow_IndexerSearch_And_DownloadValidation_Work()
// Override seams for deterministic behavior
_ = services.AddScoped();
_ = services.AddScoped();
- _ = services.AddScoped(_ => new TidalChunkDownloader(new HttpClient(new OkHandler())));
+ // Inject the resolving SSRF policy so the stub chunk host (https://chunk) classifies as public under the
+ // Strict guard (Common 9691bd8 validates the URL before the first send). Matches the R2-02 test sweep.
+ _ = services.AddScoped(_ => new TidalChunkDownloader(new HttpClient(new OkHandler()), segmentPolicy: TidalTestPolicies.Resolving));
_ = services.AddScoped(sp => new TidalSearchService(sp.GetRequiredService(), new Domain.Quality.TidalQualityDetector()));
ServiceProvider provider = services.BuildServiceProvider();