Repository: iflytek/skillhub (2026-04-08T02:02:51Z)

Issue:
[Bug] False positive in pre-publish validator: function-call assignment to token variable is treated as leaked secret
### Summary

The pre-publish validator is producing a false positive for normal Python code that assigns a function return value to a variable named `token`. 

### Steps To Reproduce

  ### Current behavior

  Publishing fails with an error like:

  ```text
  Pre-publish validation failed:
  scripts/f2e_mock.py line 1842 contains a value that looks like a secret or token. Replace real credentials with placeholders before publishing.
```

  A representative line that triggers the error is:

  token = extract_group_token_value(response, group_choice.group_id)

  This line does not contain a hardcoded credential. It only assigns the return value of a function call.

  ### Minimal example

  def maybe_generate_group_token(...):
      requested_token = secrets.token_hex(20)
      ...
      token = extract_group_token_value(response, group_choice.group_id)
      if token:
          return token
      ...

  ### Why this looks like a false positive

  The pre-publish validator currently uses a regex-based heuristic in:

  - server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java

  Relevant rule:

  (?i)(api[_-]?key|access[_-]?key|secret|password|token)\s*[:=]\s*['\"]?([A-Za-z0-9_\-]{12,})

  This regex scans text line-by-line and does not parse Python syntax. As a result:

  - the left-hand side matches token
  - the right-hand side matches the identifier prefix extract_group_token_value
  - the validator treats that identifier as a “secret-like value”

  So function calls / identifiers can be mistaken for leaked secrets.

  ### Related code path

  The validator is invoked during publish here:

  - server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java

  ### Expected behavior

  The validator should block obvious hardcoded credentials, but should not reject:

  - variable assignments
  - function-call return values
  - ordinary identifiers that happen to contain words like token, secret, etc.

  Example that should be allowed:

  token = extract_group_token_value(response, group_choice.group_id)


### Expected Behavior

  ### Suggested regression test

  A test case similar to this should pass:

  token = extract_group_token_value(response, group_choice.group_id)

  while real hardcoded secrets such as:

  token = "ghp_xxxxxxxxxxxxxxxxxxxx"
  api_key = "sk-xxxxxxxxxxxxxxxxxxxx"

  should still fail.

### Environment

_No response_

### API Contract Impact

_No response_

### Logs Or Screenshots

_No response_


Discussion:
## Issue Triage

- Route: `triage/core`
- Priority: `priority/p1` (4.0/5)
- Effort: `effort/m` (3/5)
- Confidence: `5/5`
- Risk: `high`
- Analysis Mode: `llm-shadow`

### Why
- Touches core SkillHub workflows (publish, publishing, token).
- Contains urgency signals (fails, regression).
- Text suggests a bounded change (text, placeholder).

### LLM Assist
- Provider: `openai-compatible`
- Model: `astron-code-latest`
- Mode: `shadow`
- Status: fresh assessment
- Summary: The pre-publish validator produces false positives when Python code assigns function return values to variables named 'token', due to an overly broad regex that treats identifiers as leaked secrets.

### High-Risk Signals
- Mentions sensitive areas (token, credential, secret).

### Maintainer Brief
Summary: The pre-publish validator produces false positives when Python code assigns function return values to variables named 'token', due to an overly broad regex that treats identifiers as leaked secrets.
Why core:
- Touches a higher-risk area where automated fixes should not be trusted without maintainer review.
- Mentions sensitive areas (token, credential, secret).
Reproduction or operator path:
- ### Current behavior
- Publishing fails with an error like:
- ```text
- Pre-publish validation failed:
- scripts/f2e_mock.py line 1842 contains a value that looks like a secret or token. Replace real credentials with placeholders before publishing.
Suspected areas:
- CLI publish command option parsing and namespace-aware publish flow
- Authentication, session, and identity integration
Risks to watch:
- Mentions sensitive areas (token, credential, secret).
- Namespace-scoped behavior can regress default publish/install flows if fallback routing is not preserved.
Validation checklist:
- Replay the exact reproduction steps from the issue and confirm the reported failure disappears.
- Confirm the final behavior matches the issue's expected outcome: ### Suggested regression test A test case similar to this should pass: token = extract_group_token_value(response, group_choice.group_id) while real hardcoded secrets such as: token = "ghp_xxxxxxxxxxxxxxxxxxxx" api_key = "sk-xxxxxxxxxxxxxxxxxxxx" should still fail.
- Verify both namespace-scoped behavior and the default non-namespace flow.

### Next Action
- Hand the issue to a core maintainer and use a local programming agent for reproduction, scoping, and validation.

---

## 问题分流结果

- 路由: `triage/core`
- 优先级: `priority/p1` (4.0/5)
- 修复投入: `effort/m` (3/5)
- 信息完整度: `5/5`
- 风险: `高`
- 分析模式: `llm-shadow`

### 原因
- 涉及 SkillHub 核心流程（publish, publishing, token）
- 包含紧急信号（fails, regression）
- 文本显示改动范围较可控（text, placeholder）

### AI 辅助
- 服务商: `openai-compatible`
- 模型: `astron-code-latest`
- 模式: `shadow`
- 状态: 新鲜评估
- 摘要: 预发布校验器在检测 Python 代码时产生误报：将函数返回值赋给名为 `token` 的变量时，被错误地识别为泄露的密钥。原因是 `BasicPrePublishValidator.java` 中的正则表达式过于宽泛，将标识符误判为密钥值。

### 高风险信号
- 提到敏感区域（token, credential, secret）

### 维护者交接摘要
概要: 预发布校验器在检测 Python 代码时产生误报：将函数返回值赋给名为 `token` 的变量时，被错误地识别为泄露的密钥。原因是 `BasicPrePublishValidator.java` 中的正则表达式过于宽泛，将标识符误判为密钥值。
为何进入 core:
- 触及高风险区域，未经 maintainer 审查不应直接信任自动修复
- 提到敏感区域（token, credential, secret）
复现路径或操作路径:
- ### Current behavior
- Publishing fails with an error like:
- ```text
- Pre-publish validation failed:
- scripts/f2e_mock.py line 1842 contains a value that looks like a secret or token. Replace real credentials with placeholders before publishing.
怀疑影响区域:
- CLI 发布命令参数解析与 namespace 感知发布流程
- 认证、会话与身份集成
重点风险:
- 提到敏感区域（token, credential, secret）
- namespace 范围行为如果没有保留 fallback routing，可能回归默认 publish/install 流程
验证清单:
- 按 issue 中的复现步骤逐条回放，确认报告的问题已消失
- 确认最终行为符合 issue 期望
- 同时验证 namespace 范围行为与默认非 namespace 流程

### 下一步
- 交给 core maintainer，并结合本地编程Agent协助完成复现、收敛范围与验证闭环

<!-- skillhub-issue-triage-state:
{
  "version": 2,
  "issue": 234,
  "inputHash": "c513d299c4d2e9c3721627680ec60348bb9007e5dd5d6b288c2e46aa096d80d6",
  "mode": "llm-shadow",
  "route": "core",
  "priority": 4,
  "openDays": 5,
  "requiresCoreMaintainer": false,
  "impact": 5,
  "urgency": 5,
  "effort": 3,
  "confidence": 5,
  "riskLevel": "high",
  "ageBoost": 0,
  "priorityFloor": 0,
  "engagementBoost": 0,
  "missingFields": [],
  "updatedAt": "2026-04-09T05:57:40.679Z",
  "llm": {
    "provider": "openai-compatible",
    "model": "astron-code-latest",
    "mode": "shadow",
    "inputHash": "c513d299c4d2e9c3721627680ec60348bb9007e5dd5d6b288c2e46aa096d80d6",
    "summary": "预发布校验器在检测 Python 代码时产生误报：将函数返回值赋给名为 `token` 的变量时，被错误地识别为泄露的密钥。原因是 `BasicPrePublishValidator.java` 中的正则表达式过于宽泛，将标识符误判为密钥值。",
    "summaryEn": "The pre-publish validator produces false positives when Python code assigns function return values to variables named 'token', due to an overly broad regex that treats identifiers as leaked secrets.",
    "summaryZh": "预发布校验器在检测 Python 代码时产生误报：将函数返回值赋给名为 `token` 的变量时，被错误地识别为泄露的密钥。原因是 `BasicPrePublishValidator.java` 中的正则表达式过于宽泛，将标识符误判为密钥值。",
    "impact": 4,
    "urgency": 4,
    "effort": 3,
    "confidence": 5,
    "riskFlags": [
      "token",
      "security"
    ],
    "missingInfo": [],
    "suggestedQuestions": [],
    "recommendedRoute": "core",
    "rationale": [
      "这是一个高质量的 Bug 报告：提供了清晰的复现步骤、根因分析（正则表达式 `(?i)(api[_-]?key|access[_-]?key|secret|password|token)\\s*[:=]\\s*['\\\"]?([A-Za-z0-9_\\-]{12,})` 的问题）、受影响的代码路径以及预期的修复行为。问题涉及核心发布流程，会阻止用户正常发布代码，需要核心团队修复正则逻辑以区分函数调用/标识符与真正的硬编码密钥。"
    ],
    "reused": false,
    "failed": false
  }
}
-->



Patch:
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java
index c61e420b..3709bece 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java
@@ -16,6 +16,12 @@
 @Component
 public class BasicPrePublishValidator implements PrePublishValidator {
 
+    private static final Pattern ASSIGNMENT_WITH_SENSITIVE_KEY = Pattern.compile(
+            "(?i)(api[_-]?key|access[_-]?key|secret|password|token)\\s*[:=]\\s*(.+)$"
+    );
+    private static final Pattern QUOTED_LITERAL = Pattern.compile("^(['\"])(.*)\\1$");
+    private static final Pattern IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
+    private static final Pattern BARE_LITERAL = Pattern.compile("[A-Za-z0-9_\\-]{12,}");
     private static final Pattern PLACEHOLDER_VALUE = Pattern.compile(
             "(?i).*(your|example|sample|placeholder|changeme|replace|dummy|mock|test|fake|todo|xxx|redacted).*"
     );
@@ -23,10 +29,7 @@ public class BasicPrePublishValidator implements PrePublishValidator {
             new SecretRule(Pattern.compile("(AKIA[0-9A-Z]{16})"), 1, "cloud access key"),
             new SecretRule(Pattern.compile("(ghp_[A-Za-z0-9]{20,})"), 1, "GitHub token"),
             new SecretRule(Pattern.compile("(sk-[A-Za-z0-9]{20,})"), 1, "API key"),
-            new SecretRule(
-                    Pattern.compile("(?i)(api[_-]?key|access[_-]?key|secret|password|token)\\s*[:=]\\s*['\\\"]?([A-Za-z0-9_\\-]{12,})"),
-                    2,
-                    "secret or token")
+            new SecretRule(ASSIGNMENT_WITH_SENSITIVE_KEY, 0, "secret or token")
     );
 
     @Override
@@ -46,7 +49,10 @@ public ValidationResult validate(SkillPackageContext context) {
                     if (!matcher.find()) {
                         continue;
                     }
-                    String matchedValue = matcher.group(rule.valueGroup());
+                    String matchedValue = extractMatchedValue(line, matcher, rule);
+                    if (matchedValue == null) {
+                        continue;
+                    }
                     if (isPlaceholderValue(matchedValue)) {
                         continue;
                     }
@@ -87,5 +93,64 @@ private boolean isPlaceholderValue(String value) {
                 || value.chars().allMatch(ch -> ch == 'x' || ch == 'X' || ch == '*' || ch == '-');
     }
 
+    private String extractMatchedValue(String line, Matcher matcher, SecretRule rule) {
+        if (rule.valueGroup() > 0) {
+            return matcher.group(rule.valueGroup());
+        }
+
+        Matcher assignmentMatcher = ASSIGNMENT_WITH_SENSITIVE_KEY.matcher(line);
+        if (!assignmentMatcher.find()) {
+            return null;
+        }
+
+        String rawValue = assignmentMatcher.group(2).trim();
+        if (rawValue.isBlank()) {
+            return null;
+        }
+
+        Matcher quotedLiteralMatcher = QUOTED_LITERAL.matcher(rawValue);
+        if (quotedLiteralMatcher.matches()) {
+            return quotedLiteralMatcher.group(2);
+        }
+
+        rawValue = stripInlineComment(rawValue);
+        if (rawValue.isBlank()) {
+            return null;
+        }
+
+        quotedLiteralMatcher = QUOTED_LITERAL.matcher(rawValue);
+        if (quotedLiteralMatcher.matches()) {
+            return quotedLiteralMatcher.group(2);
+        }
+
+        if (looksLikeExpression(rawValue) || IDENTIFIER.matcher(rawValue).matches()) {
+            return null;
+        }
+
+        return BARE_LITERAL.matcher(rawValue).matches() ? rawValue : null;
+    }
+
+    private String stripInlineComment(String rawValue) {
+        int hashIndex = rawValue.indexOf('#');
+        if (hashIndex >= 0) {
+            return rawValue.substring(0, hashIndex).trim();
+        }
+        return rawValue;
+    }
+
+    private boolean looksLikeExpression(String rawValue) {
+        return rawValue.contains("(")
+                || rawValue.contains(")")
+                || rawValue.contains(".")
+                || rawValue.contains("[")
+                || rawValue.contains("]")
+                || rawValue.contains("{")
+                || rawValue.contains("}")
+                || rawValue.contains(",")
+                || rawValue.contains(" ")
+                || rawValue.contains("+")
+                || rawValue.contains("/");
+    }
+
     private record SecretRule(Pattern pattern, int valueGroup, String label) {}
 }


Test patch:
diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java
index a40eb5c3..f5f2ad00 100644
--- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java
+++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java
@@ -98,4 +98,71 @@ void shouldIgnoreObviousPlaceholderSecrets() {
 
         assertTrue(result.passed());
     }
+
+    @Test
+    void shouldAllowFunctionCallAssignedToTokenVariable() {
+        PackageEntry script = new PackageEntry(
+                "scripts/f2e_mock.py",
+                """
+                token = extract_group_token_value(response, group_choice.group_id)
+                if token:
+                    return token
+                """.getBytes(StandardCharsets.UTF_8),
+                97,
+                "text/x-python"
+        );
+
+        ValidationResult result = validator.validate(new PrePublishValidator.SkillPackageContext(
+                List.of(script),
+                new SkillMetadata("Safe Skill", "desc", "1.0.0", "body", Map.of()),
+                "user-1",
+                1L
+        ));
+
+        assertTrue(result.passed());
+    }
+
+    @Test
+    void shouldAllowIdentifierAssignedToSecretNamedVariable() {
+        PackageEntry envTemplate = new PackageEntry(
+                "config.env",
+                """
+                token=generated_token_value
+                api_key=current_api_key
+                """.getBytes(StandardCharsets.UTF_8),
+                46,
+                "text/plain"
+        );
+
+        ValidationResult result = validator.validate(new PrePublishValidator.SkillPackageContext(
+                List.of(envTemplate),
+                new SkillMetadata("Safe Skill", "desc", "1.0.0", "body", Map.of()),
+                "user-1",
+                1L
+        ));
+
+        assertTrue(result.passed());
+    }
+
+    @Test
+    void shouldRejectQuotedSecretWithTrailingComment() {
+        PackageEntry script = new PackageEntry(
+                "scripts/publish.py",
+                """
+                token = "ghp_abcdefghijklmnopqrstuvwxyz1234" # do not commit real token
+                """.getBytes(StandardCharsets.UTF_8),
+                76,
+                "text/x-python"
+        );
+
+        ValidationResult result = validator.validate(new PrePublishValidator.SkillPackageContext(
+                List.of(script),
+                new SkillMetadata("Secret Skill", "desc", "1.0.0", "body", Map.of()),
+                "user-1",
+                1L
+        ));
+
+        assertFalse(result.passed());
+        assertTrue(result.errors().stream().anyMatch(error -> error.contains("scripts/publish.py")));
+    }
 }