weiqingy commented on code in PR #1005:
URL: https://github.com/apache/flink-agents/pull/1005#discussion_r3837507296


##########
runtime/src/main/java/org/apache/flink/agents/runtime/skill/repository/SkillMaterializer.java:
##########
@@ -254,14 +255,47 @@ private static void copyJarEntries(URL jarUrl, String 
prefix, Path extractDir)
      * @throws IOException on connect / read failures or HTTP error responses.
      */
     public static Path downloadToTempFile(String url, int timeoutMs) throws 
IOException {
+        return downloadToTempFile(url, timeoutMs, false);
+    }
+
+    /**
+     * Download {@code url}, optionally permitting plain HTTP transport.
+     *
+     * @throws IOException on connect / read failures or HTTP error responses.
+     */
+    public static Path downloadToTempFile(String url, int timeoutMs, boolean 
allowInsecureHttp)
+            throws IOException {
         URL u = new URL(url);
+        String initialProtocol = u.getProtocol();
+        if (!("https".equalsIgnoreCase(initialProtocol)
+                || (allowInsecureHttp && 
"http".equalsIgnoreCase(initialProtocol)))) {
+            throw new IOException("Skill URL uses a disallowed transport: " + 
url);
+        }
         HttpURLConnection conn = (HttpURLConnection) u.openConnection();
         conn.setConnectTimeout(timeoutMs);
         conn.setReadTimeout(timeoutMs);
         conn.setRequestMethod("GET");
+        // HttpURLConnection follows same-protocol redirects but leaves 
cross-protocol redirects
+        // unfollowed. Any future HTTP client must preserve that restriction.
+        conn.setInstanceFollowRedirects(true);

Review Comment:
   The comment is accurate, the JDK really does decline a scheme-changing 
redirect: `followRedirect()` compares the two protocols and bails before taking 
it.
   
   The call itself isn't quite a no-op though. `instanceFollowRedirects` is 
initialised from the static `followRedirects`, so if anything in the JVM has 
called `HttpURLConnection.setFollowRedirects(false)`, which some deployments do 
as a hardening step, this line overrides that and re-enables redirect following 
for skill downloads specifically. I checked on temurin-11: after 
`setFollowRedirects(false)` the instance default reads back `false`, and this 
line flips it to `true`. Without it the 3xx would surface and `:284` would fail 
closed with a clear message.
   
   Was pinning it to `true` deliberate, or is the comment doing the real work 
here?



##########
api/src/test/java/org/apache/flink/agents/api/yaml/YamlLoaderBuildAgentsTest.java:
##########
@@ -133,6 +133,34 @@ void skillsPerAgentAndShared() {
                         new SkillSourceSpec("local", Map.of("path", 
"./more")));
     }
 
+    @Test
+    void rejectsPlainHttpSkillUrlDuringLoading(@TempDir Path tmp) throws 
Exception {

Review Comment:
   nit: this covers the `urls:` list, and the new four-way test in 
`YamlLoaderBuildersTest` covers the `url_sources` shapes that succeed. The case 
#1003 is actually about, `url_sources: [{url: http://…}]` with 
`allow_insecure_http` absent, isn't asserted on either side, and the issue asks 
for "focused Java and Python tests for HTTP rejection or opt-in behavior".
   
   The positive cases do pin the factory choice indirectly, so this is coverage 
rather than a hole. Worth one line per language?



##########
python/flink_agents/api/skills.py:
##########
@@ -107,22 +109,96 @@ def from_local_dir(cls, *paths: str) -> Skills:
         a zip, its top-level entries are the skill subdirectories.
         """
         return cls(
-            sources=[
-                SkillSourceSpec(scheme="local", params={"path": p}) for p in 
paths
-            ]
+            sources=[SkillSourceSpec(scheme="local", params={"path": p}) for p 
in paths]
         )
 
     @classmethod
     def from_url(cls, *urls: str) -> Skills:
-        """Create a Skills resource from one or more http(s) URLs.
+        """Create a Skills resource from one or more HTTPS URLs.
 
         Each URL must point to a ``.zip`` whose top level is the baseDir
         (i.e. skill subdirectories sit at the top of the zip).
         """
+        for url in urls:
+            cls._require_url(url, allow_insecure_http=False)
         return cls(
             sources=[SkillSourceSpec(scheme="url", params={"url": u}) for u in 
urls]
         )
 
+    @classmethod
+    def from_url_with_sha256(cls, url: str, sha256: str) -> Skills:
+        """Create an HTTPS URL source pinned to a SHA-256 archive digest."""
+        cls._require_url(url, allow_insecure_http=False)
+        cls._require_sha256(sha256)
+        return cls(
+            sources=[
+                SkillSourceSpec(scheme="url", params={"url": url, "sha256": 
sha256})
+            ]
+        )
+
+    @classmethod
+    def from_url_unsafe(cls, *urls: str) -> Skills:
+        """Create URL sources that explicitly permit plain HTTP transport.
+
+        This compatibility escape hatch should be used only on trusted 
networks.
+        Prefer :meth:`from_url` with HTTPS.
+        """
+        for url in urls:
+            cls._require_url(url, allow_insecure_http=True)
+        return cls(
+            sources=[
+                SkillSourceSpec(
+                    scheme="url",
+                    params={"url": url, "allow_insecure_http": "true"},
+                )
+                for url in urls
+            ]
+        )
+
+    @classmethod
+    def from_url_unsafe_with_sha256(cls, url: str, sha256: str) -> Skills:
+        """Create a digest-pinned source that explicitly permits plain HTTP."""
+        cls._require_url(url, allow_insecure_http=True)
+        cls._require_sha256(sha256)
+        return cls(
+            sources=[
+                SkillSourceSpec(
+                    scheme="url",
+                    params={
+                        "url": url,
+                        "sha256": sha256,
+                        "allow_insecure_http": "true",
+                    },
+                )
+            ]
+        )
+
+    @staticmethod
+    def _require_url(url: str, *, allow_insecure_http: bool) -> None:
+        if not isinstance(url, str):
+            msg = "skill URL must be a string"
+            raise TypeError(msg)
+        parsed = urlparse(url)

Review Comment:
   `urlparse` doesn't reject a malformed URL, so `https://exa mple.com/x.zip` 
is accepted here and only fails later at download time, while 
`Skills.java:163-168` runs the same string through `URI.create` and rethrows it 
as `Invalid skill URL:` at call time.
   
   Before these commits neither YAML loader reached either validator. Now both 
do, so the split is exercised at load time on both runtimes rather than only 
through the code API.
   
   Is the later failure good enough here, or would you rather Python rejected 
it at the same point Java does?



##########
api/src/main/java/org/apache/flink/agents/api/skills/Skills.java:
##########
@@ -92,17 +94,94 @@ public static Skills fromLocalDir(String... paths) {
     }
 
     /**
-     * Create a {@link Skills} resource from one or more http(s) URLs.
+     * Create a {@link Skills} resource from one or more HTTPS URLs.
      *
      * <p>Each URL must point to a {@code .zip} whose top level is the baseDir.
      */
     public static Skills fromUrl(String... urls) {
         return new Skills(
                 Arrays.stream(urls)
-                        .map(u -> new SkillSourceSpec("url", Map.of("url", u)))
+                        .map(
+                                u -> {
+                                    requireUrl(u, false);
+                                    return new SkillSourceSpec("url", 
Map.of("url", u));
+                                })
                         .collect(Collectors.toList()));
     }
 
+    /**
+     * Create a {@link Skills} resource from an HTTPS URL pinned to a SHA-256 
digest.
+     *
+     * <p>The digest is verified against the downloaded archive before 
extraction.
+     */
+    public static Skills fromUrlWithSha256(String url, String sha256) {
+        return urlSource(url, sha256, false);
+    }
+
+    /**
+     * Create a {@link Skills} resource that explicitly permits plain HTTP 
transport.
+     *
+     * <p>This compatibility escape hatch should be used only on trusted 
networks. Prefer {@link
+     * #fromUrl(String...)} with HTTPS.
+     */
+    public static Skills fromUrlUnsafe(String... urls) {
+        return new Skills(
+                Arrays.stream(urls)
+                        .map(
+                                u -> {
+                                    requireUrl(u, true);
+                                    return new SkillSourceSpec(
+                                            "url", Map.of("url", u, 
"allow_insecure_http", "true"));
+                                })
+                        .collect(Collectors.toList()));
+    }
+
+    /**
+     * Create a digest-pinned {@link Skills} resource that explicitly permits 
plain HTTP transport.
+     */
+    public static Skills fromUrlUnsafeWithSha256(String url, String sha256) {
+        return urlSource(url, sha256, true);
+    }
+
+    private static Skills urlSource(String url, String sha256, boolean 
allowInsecureHttp) {
+        requireUrl(url, allowInsecureHttp);
+        if (sha256 == null || !sha256.matches("[0-9a-fA-F]{64}")) {

Review Comment:
   nit: this accepts either case and `URLSkillRepository.java:90` lowercases 
before comparing, which matches what `skills.md:144` promises ("lowercase or 
uppercase SHA-256 digest"). Every digest literal in the suite is lowercase 
though (`"a".repeat(64)` and friends), so nothing walks the uppercase path.
   
   Would flipping one existing digest literal to uppercase cover it?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to