diff --git a/core/runner/prompt_test.go b/core/runner/prompt_test.go index 5d4446ca..3970a9d0 100644 --- a/core/runner/prompt_test.go +++ b/core/runner/prompt_test.go @@ -1,10 +1,13 @@ package runner import ( + "context" "strings" "testing" + cfg "github.com/chainreactors/aiscan/core/config" "github.com/chainreactors/aiscan/pkg/commands" + "github.com/chainreactors/aiscan/pkg/telemetry" "github.com/chainreactors/aiscan/skills" ) @@ -81,3 +84,39 @@ func TestBuildSystemPromptLoadsSkillBody(t *testing.T) { t.Fatal("loaded skills should appear before principles") } } + +func TestAgentRuntimePreloadsBaseSkillOnce(t *testing.T) { + for _, tc := range []struct { + name string + skills []string + }{ + {name: "default"}, + {name: "explicit duplicate", skills: []string{"aiscan"}}, + } { + t.Run(tc.name, func(t *testing.T) { + option := &cfg.Option{} + option.Skills = tc.skills + rt, err := NewAgentRuntime(context.Background(), option, telemetry.NopLogger(), &RuntimeConfig{ + ProviderOptional: true, + NoOutput: true, + }) + if err != nil { + t.Fatalf("NewAgentRuntime() error = %v", err) + } + defer rt.Close() + + if count := strings.Count(rt.systemPrompt, "## Skill: aiscan"); count != 1 { + t.Fatalf("base skill count = %d, want 1", count) + } + for _, want := range []string{ + "## User Tool Restrictions", + "map the application before focused testing", + "capture same-origin network/API calls", + } { + if !strings.Contains(rt.systemPrompt, want) { + t.Fatalf("system prompt missing base skill rule %q", want) + } + } + }) + } +} diff --git a/core/runner/runner.go b/core/runner/runner.go index a9ac8c3c..a0df525b 100644 --- a/core/runner/runner.go +++ b/core/runner/runner.go @@ -76,6 +76,8 @@ type RuntimeConfig struct { MaxPending int } +const baseAgentSkillName = "aiscan" + func NewAgentRuntime(ctx context.Context, option *cfg.Option, logger telemetry.Logger, rc *RuntimeConfig) (*AgentRuntime, error) { if ctx == nil { ctx = context.Background() @@ -155,7 +157,19 @@ func NewAgentRuntime(ctx context.Context, option *cfg.Option, logger telemetry.L NodeName: nodeName, Space: option.Space, } - for _, name := range option.Skills { + if rc != nil && rc.PromptConfig != nil { + promptConfig := *rc.PromptConfig + promptConfig.LoadedSkills = append([]LoadedSkill(nil), rc.PromptConfig.LoadedSkills...) + pc = &promptConfig + } + skillNames := option.Skills + if !pc.ScannerAgentMode { + skillNames = append([]string{baseAgentSkillName}, skillNames...) + } + for _, name := range skillNames { + if promptHasLoadedSkill(pc, name) { + continue + } body := rt.app.Skills.ReadBody(name) if body == "" { body = skills.ReadFile("skills/" + name + ".md") @@ -167,9 +181,6 @@ func NewAgentRuntime(ctx context.Context, option *cfg.Option, logger telemetry.L pc.LoadedSkills = append(pc.LoadedSkills, LoadedSkill{Name: name, Body: body}) } } - if rc != nil && rc.PromptConfig != nil { - pc = rc.PromptConfig - } rt.systemPrompt = BuildSystemPrompt(pc, nil) logger.Debugf("system prompt length: %d chars", len(rt.systemPrompt)) @@ -300,6 +311,15 @@ func NewAgentRuntime(ctx context.Context, option *cfg.Option, logger telemetry.L return rt, nil } +func promptHasLoadedSkill(pc *PromptConfig, name string) bool { + for _, loaded := range pc.LoadedSkills { + if loaded.Name == name { + return true + } + } + return false +} + func (rt *AgentRuntime) Close() { if rt == nil { return diff --git a/skills/aiscan/SKILL.md b/skills/aiscan/SKILL.md index 101b6321..13c9688e 100644 --- a/skills/aiscan/SKILL.md +++ b/skills/aiscan/SKILL.md @@ -26,6 +26,18 @@ Core agent tools: - `web_search`: search the web for CVEs, advisories, exploits, and documentation. - `fetch`: fetch and read a specific URL. +## User Tool Restrictions + +Treat a user restriction as a constraint on tools and traffic, not as permission to reduce the requested assessment depth. Follow explicit scope and rate limits exactly. + +When the user says not to use scanners or automated scanning: + +- Do not invoke `scan`, `gogo`, `spray`, `zombie`, `neutron`, `proton`, `passive`, or `katana` unless the user later allows it. +- Continue with allowed manual techniques unless the user also narrows the task itself. Do not silently reduce a broad web assessment to one vulnerability class or one browser action. +- For a web target, map the application before focused testing: inspect the rendered page and forms, identify loaded JavaScript, capture same-origin network/API calls, check route or source-map clues, and review authentication/session boundaries. +- Use `playwright`, `fetch`, and bounded shell requests only when they remain within the user's stated restrictions. Keep requests targeted and do not expand to related hosts without permission. +- Explain any material coverage gap caused by the restriction in the final result. + ## Pseudo-Commands All pseudo-commands run through `bash`. They are **not** system binaries.