mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): restricted Skill pipeline step executor
This commit is contained in:
parent
15a8b2d73c
commit
526a361488
@ -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.
|
||||
*
|
||||
* <p><b>Scope (MVP, security-restricted)</b>: 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
|
||||
* <b>not</b> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@ -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<String, Object> 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"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user