135 lines
5.2 KiB
Python
135 lines
5.2 KiB
Python
"""Phase 2b โ inject Gitea Actions workflows (PR gates) into the recruiter package.
|
|
|
|
- sanitize.yml: fails on PII, e-mail addresses, secrets, internal URLs
|
|
- eval.yml: validates package structure & required files
|
|
|
|
Both run on the self-hosted act_runner (host mode, label "windows").
|
|
Idempotent: files are simply (re)written.
|
|
"""
|
|
import os
|
|
|
|
SKILLS_DIR = os.path.join(os.path.dirname(__file__), "..", "skills")
|
|
|
|
SANITIZE = r'''name: sanitize
|
|
# PR gate: block personal data, secrets and tenant-internal references from
|
|
# entering the shared skill library. Every layer boundary is a gate.
|
|
# Deliberately PR-only: INSIDE a private tenant repo internal details are
|
|
# legitimate; the gate guards the boundary to the shared layers.
|
|
on:
|
|
pull_request:
|
|
|
|
jobs:
|
|
pii-scan:
|
|
runs-on: windows
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Scan for PII / secrets / internal URLs
|
|
shell: python
|
|
run: |
|
|
import os, re, sys
|
|
|
|
RULES = [
|
|
("e-mail address", re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")),
|
|
("phone number", re.compile(r"(?<![\w/.-])\+?\d[\d ()\-]{8,}\d(?![\w/])")),
|
|
("secret assignment", re.compile(r"(?i)(password|passwd|secret|api[_-]?key|token)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{6,}")),
|
|
("internal URL/host", re.compile(r"(?i)(\bintranet\b|\.internal\b|\.local\b|\.corp\b|\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|\b192\.168\.\d{1,3}\.\d{1,3}\b)")),
|
|
("person name marker", re.compile(r"(?i)\b(hiring manager|contact):\s*[A-Z][a-z]+ [A-Z][a-z]+")),
|
|
]
|
|
ALLOW = re.compile(r"(?i)(esco\.ec\.europa\.eu|onetcenter\.org|data\.europa\.eu|creativecommons\.org)")
|
|
|
|
findings = []
|
|
for root, dirs, files in os.walk("."):
|
|
dirs[:] = [d for d in dirs if d not in (".git", ".gitea")]
|
|
for name in files:
|
|
path = os.path.join(root, name)
|
|
try:
|
|
text = open(path, encoding="utf-8", errors="ignore").read()
|
|
except OSError:
|
|
continue
|
|
for lineno, line in enumerate(text.splitlines(), 1):
|
|
if ALLOW.search(line):
|
|
continue
|
|
for label, rx in RULES:
|
|
m = rx.search(line)
|
|
if not m:
|
|
continue
|
|
# ISO-Datumsangaben (2026-07-06) sind keine Telefonnummern
|
|
if label == "phone number" and re.fullmatch(r"\d{4}-\d{2}-\d{2}", m.group(0)):
|
|
continue
|
|
findings.append(f"{path}:{lineno}: {label}: {line.strip()[:120]}")
|
|
|
|
if findings:
|
|
print("SANITIZE GATE FAILED โ remove/anonymize before publishing:")
|
|
print("\n".join(findings))
|
|
sys.exit(1)
|
|
print("sanitize: clean โ no PII, secrets or internal references found.")
|
|
'''
|
|
|
|
EVAL = r'''name: eval
|
|
# Structural eval: a skill package must be complete and within limits before
|
|
# it can be merged. Complements sanitize.yml (content gate).
|
|
on:
|
|
pull_request:
|
|
push:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
structure:
|
|
runs-on: windows
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Validate package structure
|
|
shell: python
|
|
run: |
|
|
import json, os, sys
|
|
|
|
errors = []
|
|
REQUIRED = [
|
|
"SKILL.md",
|
|
"manifest.json",
|
|
"references/profile.md",
|
|
"references/tasks.md",
|
|
"references/skills.md",
|
|
"references/tools.md",
|
|
]
|
|
for f in REQUIRED:
|
|
if not os.path.isfile(f):
|
|
errors.append(f"missing required file: {f}")
|
|
|
|
if os.path.isfile("SKILL.md"):
|
|
lines = open("SKILL.md", encoding="utf-8").read().splitlines()
|
|
if len(lines) > 300:
|
|
errors.append(f"SKILL.md too long: {len(lines)} lines (max 300)")
|
|
if not lines or lines[0].strip() != "---":
|
|
errors.append("SKILL.md missing frontmatter")
|
|
|
|
if os.path.isfile("manifest.json"):
|
|
m = json.load(open("manifest.json", encoding="utf-8"))
|
|
for key in ("name", "version", "layer", "ids", "sources", "attribution"):
|
|
if key not in m:
|
|
errors.append(f"manifest.json missing key: {key}")
|
|
|
|
if errors:
|
|
print("EVAL FAILED:")
|
|
print("\n".join(f"- {e}" for e in errors))
|
|
sys.exit(1)
|
|
print("eval: package structure OK.")
|
|
'''
|
|
|
|
|
|
def main():
|
|
recruiter = next((d for d in sorted(os.listdir(SKILLS_DIR))
|
|
if "recruit" in d and os.path.isdir(os.path.join(SKILLS_DIR, d))), None)
|
|
if not recruiter:
|
|
print("SKIP: no recruiter package found yet (run p2 first)")
|
|
return
|
|
wf = os.path.join(SKILLS_DIR, recruiter, ".gitea", "workflows")
|
|
os.makedirs(wf, exist_ok=True)
|
|
open(os.path.join(wf, "sanitize.yml"), "w", encoding="utf-8", newline="\n").write(SANITIZE)
|
|
open(os.path.join(wf, "eval.yml"), "w", encoding="utf-8", newline="\n").write(EVAL)
|
|
print(f"workflows injected into skills/{recruiter}/.gitea/workflows/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|