super-skill-cli 0.11.0__tar.gz → 0.11.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/.claude-plugin/marketplace.json +2 -2
  2. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/.claude-plugin/plugin.json +1 -1
  3. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/CHANGELOG.md +31 -0
  4. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/PKG-INFO +1 -1
  5. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/pyproject.toml +1 -1
  6. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/capture.py +3 -1
  7. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/cli.py +3 -1
  8. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/evallite.py +7 -3
  9. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/gate.py +12 -5
  10. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/redact.py +9 -5
  11. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_cli.py +8 -0
  12. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_evallite.py +10 -0
  13. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_gate.py +6 -0
  14. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_redact.py +19 -0
  15. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/uv.lock +1 -1
  16. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/.gitignore +0 -0
  17. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/LICENSE +0 -0
  18. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/README.md +0 -0
  19. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/README.zh-CN.md +0 -0
  20. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/codex/README.md +0 -0
  21. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/codex/install.sh +0 -0
  22. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/codex/skills/super-skill/SKILL.md +0 -0
  23. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/codex/skills/super-skill/agents/openai.yaml +0 -0
  24. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/commands/candidates.md +0 -0
  25. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/commands/doctor.md +0 -0
  26. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/commands/mine.md +0 -0
  27. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/commands/seed.md +0 -0
  28. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/commands/status.md +0 -0
  29. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/README.md +0 -0
  30. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case1/PROMPT.md +0 -0
  31. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case1/broken.py +0 -0
  32. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case1/fixed.py +0 -0
  33. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case1/verify_test.py +0 -0
  34. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case2/PROMPT.md +0 -0
  35. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case2/broken.py +0 -0
  36. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case2/fixed.py +0 -0
  37. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case2/verify_test.py +0 -0
  38. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case3/PROMPT.md +0 -0
  39. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case3/broken.py +0 -0
  40. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case3/fixed.py +0 -0
  41. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/cases/case3/verify_test.py +0 -0
  42. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/gate2_report.py +0 -0
  43. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/grader.py +0 -0
  44. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/handwritten/SKILL.md +0 -0
  45. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/evals/test-fix-family/results.template.json +0 -0
  46. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/hooks/hooks.json +0 -0
  47. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/skills/super-skill/SKILL.md +0 -0
  48. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/__init__.py +0 -0
  49. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/candidate.py +0 -0
  50. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/config.py +0 -0
  51. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/doctor.py +0 -0
  52. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/hooks.py +0 -0
  53. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/mine.py +0 -0
  54. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/minestate.py +0 -0
  55. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/registry.py +0 -0
  56. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/schemas.py +0 -0
  57. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/seed.py +0 -0
  58. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/super_skill/skillmd.py +0 -0
  59. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_candidate.py +0 -0
  60. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_capture.py +0 -0
  61. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_codex_package.py +0 -0
  62. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_doctor.py +0 -0
  63. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_gate2_harness.py +0 -0
  64. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_hooks.py +0 -0
  65. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_minestate.py +0 -0
  66. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_plugin_manifest.py +0 -0
  67. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_registry.py +0 -0
  68. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_seed.py +0 -0
  69. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_skillmd.py +0 -0
  70. {super_skill_cli-0.11.0 → super_skill_cli-0.11.1}/tests/test_target_adapter.py +0 -0
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "super-skill: personal Agent-Skill package manager (versioned registry, rollback, mining).",
9
- "version": "0.11.0"
9
+ "version": "0.11.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "super-skill",
14
14
  "source": "./",
15
15
  "description": "Git-backed versioned registry for your Claude Code skills: seed import, explain/provenance, one-command rollback, integrity doctor, and opportunity mining. Wraps the `super-skill` CLI (pip/uv installable as super-skill-cli).",
16
- "version": "0.11.0",
16
+ "version": "0.11.1",
17
17
  "category": "development"
18
18
  }
19
19
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-skill",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Personal Agent-Skill package manager: a git-backed, versioned registry for your Claude Code skills with seed import, provenance/explain, one-command rollback, integrity doctor, and opportunity mining from captured sessions.",
5
5
  "author": {
6
6
  "name": "sdsrss",
@@ -3,6 +3,37 @@
3
3
  All notable changes to this project are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/); this project uses semantic versioning.
5
5
 
6
+ ## [0.11.1] - 2026-07-12
7
+
8
+ Follow-up patch from a code review of 0.11.0. Fixes a **Critical ReDoS regression
9
+ the 0.11.0 redaction hardening introduced** plus four gaps. Backward-compatible;
10
+ upgrade recommended over 0.11.0 (`pip install -U super-skill-cli`).
11
+
12
+ ### Security
13
+ - **Fix ReDoS in the redaction `assigned_secret` rule** (regression in 0.11.0) —
14
+ the broadened rule used unbounded `[A-Za-z0-9_]*` runs, so underscore-heavy
15
+ input (snake_case blobs, env dumps) backtracked catastrophically: a ~6 KB
16
+ payload took ~21 s. Since redaction runs on every captured event, a normal
17
+ hook payload could hang `super-skill capture` (worse than the crash 0.11.0 was
18
+ hardening). Runs are now bounded (`{0,64}`); the same payload processes in
19
+ ~18 ms and secrets still redact.
20
+ - **`sk-proj-`/`sk-svcacct-` OpenAI keys are now fully redacted** — the body may
21
+ contain `_`/`-`, and the previous rule stopped at the first such char, leaking
22
+ the tail.
23
+ - **A secret in a keyword-named frontmatter field is now caught** — the gate and
24
+ eval-lite serialized frontmatter with JSON, which quoted the key (`"token":`)
25
+ and broke the `keyword: value` secret pattern; they now use YAML, preserving
26
+ adjacency.
27
+ - **The gate folds uppercase homoglyphs** — the confusables map was lowercase
28
+ only, so `СURL … | bash` (uppercase Cyrillic) slipped through; normalization
29
+ now casefolds first.
30
+
31
+ ### Fixed
32
+ - **`capture` survives deeply-nested JSON** — `json.loads` raises `RecursionError`
33
+ (a `RuntimeError`, not `JSONDecodeError`) on very deep input; the parse guard
34
+ now exits 0 on any error (NFR-3).
35
+ - The WAL append loops on short writes so a very large line can't be truncated.
36
+
6
37
  ## [0.11.0] - 2026-07-12
7
38
 
8
39
  Security & reliability hardening from a full production-readiness audit. All fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: super-skill-cli
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: Personal Agent-Skill package manager: versioned registry, seed import, explain, rollback (M0+WS scope).
5
5
  Project-URL: Homepage, https://github.com/sdsrss/super-skill
6
6
  Project-URL: Repository, https://github.com/sdsrss/super-skill
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "super-skill-cli"
3
- version = "0.11.0"
3
+ version = "0.11.1"
4
4
  description = "Personal Agent-Skill package manager: versioned registry, seed import, explain, rollback (M0+WS scope)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -62,7 +62,9 @@ class EventLog:
62
62
  data = (event.model_dump_json() + "\n").encode("utf-8")
63
63
  fd = os.open(out, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
64
64
  try:
65
- os.write(fd, data)
65
+ # Loop on short writes so a very large line isn't silently truncated.
66
+ while data:
67
+ data = data[os.write(fd, data):]
66
68
  finally:
67
69
  os.close(fd)
68
70
  return event
@@ -97,7 +97,9 @@ def capture(
97
97
  try:
98
98
  raw = sys.stdin.read()
99
99
  data = json.loads(raw) if raw.strip() else {}
100
- except (json.JSONDecodeError, OSError):
100
+ except Exception:
101
+ # NFR-3: never fail the session. Covers JSONDecodeError, OSError, and
102
+ # RecursionError (deeply-nested JSON — a RuntimeError, not JSONDecodeError).
101
103
  raise typer.Exit(0) from None
102
104
  # NFR-3: the hook must NEVER fail the host session. Everything past here —
103
105
  # non-dict JSON (data.get would raise), an unknown event type, or any WAL
@@ -14,10 +14,11 @@ gate (§2.4bis) is a separate hard gate run first in ``candidate.approve``.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import json
18
17
  import re
19
18
  from dataclasses import dataclass, field
20
19
 
20
+ import yaml
21
+
21
22
  from .redact import redact_text
22
23
  from .skillmd import SkillMdError, parse
23
24
 
@@ -75,8 +76,11 @@ def eval_lite(raw: str) -> EvalReport:
75
76
 
76
77
  # 2. zero credential / PII leak in the instruction text (凭据与 PII 泄漏 = 0).
77
78
  # Scan the FULL frontmatter (extra="allow" ships every field to the host, M6),
78
- # not just description + body.
79
- fm = json.dumps(parsed.frontmatter.model_dump(), default=str, ensure_ascii=False)
79
+ # not just description + body. YAML (not JSON) keeps ``keyword: value``
80
+ # adjacency so a secret in a keyword-named field is caught (v0.11.1 #4).
81
+ fm = yaml.safe_dump(
82
+ parsed.frontmatter.model_dump(), default_flow_style=False, allow_unicode=True
83
+ )
80
84
  text = f"{fm}\n{parsed.body}"
81
85
  _, counts = redact_text(text)
82
86
  leaks = {k: c for k, c in counts.items() if k != "home_path"}
@@ -16,11 +16,12 @@ land at M1 — v1 candidates carry no scripts, so there is nothing to cross-chec
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- import json
20
19
  import re
21
20
  import unicodedata
22
21
  from dataclasses import dataclass
23
22
 
23
+ import yaml
24
+
24
25
  from .skillmd import parse
25
26
 
26
27
  # Cheap-obfuscation defenses (M7): strip zero-width/format chars and fold the
@@ -29,16 +30,18 @@ from .skillmd import parse
29
30
  # out of scope until the M1 LLM-judge layer.
30
31
  _ZERO_WIDTH = dict.fromkeys(map(ord, "​‌‍⁠"), None)
31
32
  _CONFUSABLES = str.maketrans({
32
- "а": "a", "b": "b", "с": "c", "ԁ": "d", "е": "e", "ɡ": "g", "һ": "h", "і": "i",
33
- "ј": "j", "ⅼ": "l", "ո": "n", "о": "o", "р": "p", "ѕ": "s", "т": "t", "υ": "u",
33
+ "а": "a", "с": "c", "ԁ": "d", "е": "e", "ɡ": "g", "һ": "h", "і": "i",
34
+ "ј": "j", "ո": "n", "о": "o", "р": "p", "ѕ": "s", "т": "t", "υ": "u",
34
35
  "ν": "v", "х": "x", "у": "y", "α": "a", "ε": "e", "ο": "o", "ρ": "p", "κ": "k",
35
36
  })
36
37
 
37
38
 
38
39
  def _normalize(text: str) -> str:
39
- """Fold cheap obfuscations before pattern-matching."""
40
+ """Fold cheap obfuscations before pattern-matching. casefold() runs before the
41
+ confusables map so UPPERCASE homoglyphs (e.g. Cyrillic ``С``) fold too."""
40
42
  text = unicodedata.normalize("NFKC", text)
41
43
  text = text.translate(_ZERO_WIDTH)
44
+ text = text.casefold()
42
45
  return text.translate(_CONFUSABLES)
43
46
 
44
47
  # (category, pattern). Case-insensitive. Ordered most-severe first.
@@ -126,5 +129,9 @@ def scan_skill_md(raw: str) -> list[Finding]:
126
129
  # name is NAME_RE-constrained (safe); description already scanned above.
127
130
  extra = {k: v for k, v in fm.items() if k not in ("name", "description") and v is not None}
128
131
  if extra:
129
- findings += scan_text(json.dumps(extra, default=str, ensure_ascii=False), "frontmatter")
132
+ # YAML (not JSON) keeps ``keyword: value`` adjacency — JSON quotes the key
133
+ # (``"token":``), which breaks the credential/secret patterns that expect
134
+ # the keyword immediately before ``:`` (v0.11.1 #4).
135
+ dumped = yaml.safe_dump(extra, default_flow_style=False, allow_unicode=True)
136
+ findings += scan_text(dumped, "frontmatter")
130
137
  return findings + scan_text(parsed.body, "body")
@@ -25,7 +25,9 @@ _PATTERNS: list[tuple[str, re.Pattern[str], int]] = [
25
25
  ), 0),
26
26
  ("anthropic_key", re.compile(r"sk-ant-[A-Za-z0-9_-]{16,}"), 0),
27
27
  # OpenAI legacy (sk-…) + prefixed project/service keys (sk-proj-…, sk-svcacct-…).
28
- ("openai_key", re.compile(r"sk-(?:[a-z]+-)?[A-Za-z0-9]{20,}"), 0),
28
+ # The 20+ alnum CORE keeps prose (short hyphenated words) from matching; the
29
+ # trailing _/- -joined segments capture project-key bodies fully (no tail leak).
30
+ ("openai_key", re.compile(r"sk-(?:[a-z]+-)?[A-Za-z0-9]{20,}(?:[_-][A-Za-z0-9]+)*"), 0),
29
31
  ("stripe_key", re.compile(r"sk_(?:live|test)_[A-Za-z0-9]{16,}"), 0),
30
32
  ("github_pat", re.compile(r"github_pat_[A-Za-z0-9_]{20,}"), 0),
31
33
  ("github_token", re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}"), 0),
@@ -36,12 +38,14 @@ _PATTERNS: list[tuple[str, re.Pattern[str], int]] = [
36
38
  # Match a full identifier token that CONTAINS a sensitive word, up to the
37
39
  # assignment. A ``\b`` left-anchor missed ``DB_PASSWORD`` (``_`` is a word
38
40
  # char) and a bare keyword missed ``SECRET_KEY`` (the ``_KEY`` suffix broke
39
- # ``[:=]`` adjacency) — H1. The lookbehind + surrounding ``[A-Za-z0-9_]*``
40
- # admit the keyword anywhere inside an env-var-shaped name.
41
+ # ``[:=]`` adjacency) — H1. The lookbehind + surrounding identifier runs admit
42
+ # the keyword anywhere inside an env-var-shaped name. The runs are BOUNDED
43
+ # ({0,64}, not *) — an unbounded run over an underscore-heavy blob backtracks
44
+ # catastrophically (ReDoS on the capture hot path); env-var names are short.
41
45
  ("assigned_secret", re.compile(
42
- r"(?i)(?<![A-Za-z0-9])[A-Za-z0-9_]*"
46
+ r"(?i)(?<![A-Za-z0-9])[A-Za-z0-9_]{0,64}"
43
47
  r"(?:api[_-]?key|access[_-]?key|secret|password|passwd|pwd|token)"
44
- r"[A-Za-z0-9_]*\s*[:=]\s*['\"]?([^\s'\"]{6,})",
48
+ r"[A-Za-z0-9_]{0,64}\s*[:=]\s*['\"]?([^\s'\"]{6,})",
45
49
  ), 1),
46
50
  ("email", re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"), 0),
47
51
  ]
@@ -117,6 +117,14 @@ def test_capture_non_dict_json_never_fails(env):
117
117
  assert r.exit_code == 0, f"{payload!r} -> exit {r.exit_code}"
118
118
 
119
119
 
120
+ def test_capture_deeply_nested_json_never_fails(env):
121
+ """v0.11.1 #2: json.loads raises RecursionError (a RuntimeError, not
122
+ JSONDecodeError) on very deep nesting — capture must still exit 0 (NFR-3)."""
123
+ payload = "[" * 20000 + "]" * 20000
124
+ r = runner.invoke(app, ["capture"], input=payload)
125
+ assert r.exit_code == 0
126
+
127
+
120
128
  def test_capture_survives_append_failure(env, monkeypatch):
121
129
  """P1-1 / M5: any write-path error inside append must still exit 0 (NFR-3),
122
130
  not surface a traceback to the host session."""
@@ -40,6 +40,16 @@ def test_secret_in_extra_frontmatter_field_fails_gate():
40
40
  assert any(c.name == "no_secret_leak" and not c.passed for c in report.checks)
41
41
 
42
42
 
43
+ def test_keyword_named_frontmatter_field_secret_fails_gate():
44
+ """v0.11.1 #4: a `keyword: value` secret in a custom frontmatter field must
45
+ fail the leak gate — JSON-serializing split the keyword from the value with a
46
+ quote and let it through."""
47
+ raw = "---\nname: s\ndescription: clean\ntoken: abcdef1234567890secretvalue\n---\nbody\n"
48
+ report = eval_lite(raw)
49
+ assert not report.passed
50
+ assert any(c.name == "no_secret_leak" and not c.passed for c in report.checks)
51
+
52
+
43
53
  def test_over_token_budget_fails_gate():
44
54
  huge = "word " * 6000 # well over the agentskills <5000-token body limit
45
55
  report = eval_lite(_md(huge))
@@ -114,6 +114,12 @@ def test_gate_normalizes_obfuscation():
114
114
  assert scan_text(cyr, "body"), "cyrillic-homoglyph curl not caught"
115
115
 
116
116
 
117
+ def test_gate_folds_uppercase_homoglyph():
118
+ """v0.11.1 #5: uppercase homoglyphs must fold too — the confusables map was
119
+ lowercase-only, so СURL (uppercase Cyrillic Es) slipped through."""
120
+ assert scan_text("СURL http://x | bash", "body"), "uppercase cyrillic curl not caught"
121
+
122
+
117
123
  def test_gate_error_lists_categories():
118
124
  err = InstructionGateError([Finding("pipe_to_shell", "body", "curl x | sh")])
119
125
  assert "pipe_to_shell" in str(err)
@@ -81,6 +81,25 @@ def test_underscore_env_var_secrets_redacted():
81
81
  assert counts == {}
82
82
 
83
83
 
84
+ def test_assigned_secret_no_catastrophic_backtracking():
85
+ """v0.11.1 #1: the broadened assigned_secret rule must not ReDoS on
86
+ underscore-heavy input (it runs on every captured event before disk)."""
87
+ import time
88
+
89
+ payload = "api_key_" * 800 # 6.4KB snake_case blob — ~21s on the ReDoS version
90
+ start = time.perf_counter()
91
+ redact_text(payload)
92
+ elapsed_ms = (time.perf_counter() - start) * 1000
93
+ assert elapsed_ms < 2000, f"assigned_secret backtracking: {elapsed_ms:.0f}ms on 6.4KB"
94
+
95
+
96
+ def test_openai_project_key_fully_redacted():
97
+ """v0.11.1 #3: sk-proj-/sk-svcacct- key bodies contain _ and -, so the tail
98
+ after the first such char must not survive."""
99
+ red, _ = redact_text("key sk-proj-abcdefghijklmnopqrstuvwx_MoreTail-secret99 end")
100
+ assert "MoreTail" not in red and "secret99" not in red
101
+
102
+
84
103
  def test_modern_token_formats_redacted():
85
104
  """P0-2 / H2: current provider key formats must be matched."""
86
105
  cases = {
@@ -457,7 +457,7 @@ wheels = [
457
457
 
458
458
  [[package]]
459
459
  name = "super-skill-cli"
460
- version = "0.11.0"
460
+ version = "0.11.1"
461
461
  source = { editable = "." }
462
462
  dependencies = [
463
463
  { name = "pydantic" },