Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions core/runner/prompt_test.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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)
}
}
})
}
}
28 changes: 24 additions & 4 deletions core/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand All @@ -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))

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions skills/aiscan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down