diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/pipeline/WikiSkillStepExecutor.java b/mateclaw-server/src/main/java/vip/mate/wiki/pipeline/WikiSkillStepExecutor.java
new file mode 100644
index 00000000..b3445e59
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/wiki/pipeline/WikiSkillStepExecutor.java
@@ -0,0 +1,73 @@
+package vip.mate.wiki.pipeline;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import vip.mate.skill.model.SkillEntity;
+import vip.mate.skill.service.SkillService;
+
+import java.util.Set;
+
+/**
+ * Pipeline step executor that resolves a registered skill and contributes its
+ * declarative content to the chain.
+ *
+ *
Scope (MVP, security-restricted): the skill must be installed,
+ * enabled and not have a failed/blocked security scan. This executor injects
+ * the skill's instructions/content as the step output; it deliberately does
+ * not execute skill scripts — arbitrary script execution from a
+ * system-triggered pipeline requires a real sandbox and a separate security
+ * review (the same constraint that keeps a Python executor out of the MVP).
+ *
+ * @author MateClaw Team
+ */
+@Slf4j
+@Component
+public class WikiSkillStepExecutor implements WikiStepExecutor {
+
+ private static final Set BLOCKED_SCAN_STATUSES = Set.of("FAILED", "BLOCKED", "REJECTED");
+
+ private final SkillService skillService;
+
+ public WikiSkillStepExecutor(SkillService skillService) {
+ this.skillService = skillService;
+ }
+
+ @Override
+ public String type() {
+ return "skill";
+ }
+
+ @Override
+ public String execute(WikiStepContext context) {
+ String skillName = context.stepConfig() == null ? null
+ : (String) (context.stepConfig().get("skill") instanceof String s ? s : null);
+ if (skillName == null || skillName.isBlank()) {
+ throw new IllegalArgumentException("skill step '" + context.stepId() + "' has no skill name");
+ }
+ SkillEntity skill = skillService.findByName(skillName);
+ if (skill == null) {
+ throw new IllegalArgumentException("Skill not found: " + skillName);
+ }
+ if (Boolean.FALSE.equals(skill.getEnabled())) {
+ throw new IllegalStateException("Skill is disabled: " + skillName);
+ }
+ String scan = skill.getSecurityScanStatus();
+ if (scan != null && BLOCKED_SCAN_STATUSES.contains(scan.trim().toUpperCase())) {
+ throw new IllegalStateException("Skill failed its security scan and cannot run in a pipeline: "
+ + skillName + " (" + scan + ")");
+ }
+ // Reject any attempt to run the skill's script from a pipeline — not in MVP.
+ Object runScript = context.stepConfig().get("run_script");
+ if (Boolean.TRUE.equals(runScript) || "true".equalsIgnoreCase(String.valueOf(runScript))) {
+ throw new IllegalStateException("Script execution is not permitted for pipeline skill steps "
+ + "(requires a sandbox + approval); skill: " + skillName);
+ }
+ String content = skill.getSkillContent();
+ if (content == null || content.isBlank()) {
+ content = skill.getDescription();
+ }
+ log.info("[WikiPipeline] skill step '{}' contributed content from skill '{}'",
+ context.stepId(), skillName);
+ return content == null ? "" : content;
+ }
+}
diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/pipeline/WikiSkillStepExecutorTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/pipeline/WikiSkillStepExecutorTest.java
new file mode 100644
index 00000000..2ea48913
--- /dev/null
+++ b/mateclaw-server/src/test/java/vip/mate/wiki/pipeline/WikiSkillStepExecutorTest.java
@@ -0,0 +1,81 @@
+package vip.mate.wiki.pipeline;
+
+import org.junit.jupiter.api.Test;
+import vip.mate.skill.model.SkillEntity;
+import vip.mate.skill.service.SkillService;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link WikiSkillStepExecutor}: returns the skill's content
+ * when allowed, and refuses disabled / scan-failed skills and script execution.
+ */
+class WikiSkillStepExecutorTest {
+
+ private SkillEntity skill(String name, Boolean enabled, String scan, String content) {
+ SkillEntity s = new SkillEntity();
+ s.setName(name);
+ s.setEnabled(enabled);
+ s.setSecurityScanStatus(scan);
+ s.setSkillContent(content);
+ return s;
+ }
+
+ private WikiSkillStepExecutor executor(SkillEntity skill) {
+ SkillService service = mock(SkillService.class);
+ when(service.findByName("wiki-link-enrich")).thenReturn(skill);
+ return new WikiSkillStepExecutor(service);
+ }
+
+ private WikiStepContext ctx(Map config) {
+ return new WikiStepContext(1L, 42L, "s1", config, "prior");
+ }
+
+ @Test
+ void returnsSkillContent_whenAllowed() throws Exception {
+ WikiSkillStepExecutor e = executor(skill("wiki-link-enrich", true, "PASSED", "do the thing"));
+ assertEquals("do the thing", e.execute(ctx(Map.of("skill", "wiki-link-enrich"))));
+ }
+
+ @Test
+ void missingSkillName_throws() {
+ WikiSkillStepExecutor e = executor(skill("wiki-link-enrich", true, "PASSED", "x"));
+ assertThrows(IllegalArgumentException.class, () -> e.execute(ctx(Map.of())));
+ }
+
+ @Test
+ void unknownSkill_throws() {
+ WikiSkillStepExecutor e = executor(skill("wiki-link-enrich", true, "PASSED", "x"));
+ assertThrows(IllegalArgumentException.class,
+ () -> e.execute(ctx(Map.of("skill", "does-not-exist"))));
+ }
+
+ @Test
+ void disabledSkill_throws() {
+ WikiSkillStepExecutor e = executor(skill("wiki-link-enrich", false, "PASSED", "x"));
+ assertThrows(IllegalStateException.class,
+ () -> e.execute(ctx(Map.of("skill", "wiki-link-enrich"))));
+ }
+
+ @Test
+ void scanFailedSkill_throws() {
+ WikiSkillStepExecutor e = executor(skill("wiki-link-enrich", true, "FAILED", "x"));
+ IllegalStateException ex = assertThrows(IllegalStateException.class,
+ () -> e.execute(ctx(Map.of("skill", "wiki-link-enrich"))));
+ assertTrue(ex.getMessage().contains("security scan"));
+ }
+
+ @Test
+ void scriptExecutionRequest_isRefused() {
+ WikiSkillStepExecutor e = executor(skill("wiki-link-enrich", true, "PASSED", "x"));
+ IllegalStateException ex = assertThrows(IllegalStateException.class,
+ () -> e.execute(ctx(Map.of("skill", "wiki-link-enrich", "run_script", true))));
+ assertTrue(ex.getMessage().contains("Script execution is not permitted"));
+ }
+}