Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
feafd4b
refactor: decouple agent, tool, and web layers
M09Ic Jul 18, 2026
47fa41a
feat: switch to AOP event protocol + unify chat with cyber-ui viewer
M09Ic Jul 18, 2026
4653912
feat(agent): expose webproto stdio execution
M09Ic Jul 19, 2026
a321e03
refactor(agent): unify harness on webproto stdio
M09Ic Jul 19, 2026
6ad465f
refactor(harness): keep AOP data typed
M09Ic Jul 19, 2026
d9a7fc7
feat(aop): make AOP the single agent transport
M09Ic Jul 19, 2026
807f088
test(aop): remove versioned envelope fixtures
M09Ic Jul 19, 2026
b12c81c
fix(web): auto-start local agent and restore chat UI
M09Ic Jul 19, 2026
9dedd4d
fix(web): restore structured scanner results
M09Ic Jul 19, 2026
bd3eaca
feat(web): group asset table by SCO type
M09Ic Jul 19, 2026
b71b9e6
fix(web): replace replayed AOP session history
M09Ic Jul 19, 2026
5a8e7d9
build: use local libcstx during integration
M09Ic Jul 19, 2026
e6651a5
refactor(aop): unify transport identity and execution paths
M09Ic Jul 20, 2026
f8f6d2d
build(aop): pin IOA and cyber-ui protocol updates
M09Ic Jul 20, 2026
7b487a9
build: keep local module replaces out of CI
M09Ic Jul 20, 2026
4239248
build: align go.mod with linux tidy
M09Ic Jul 20, 2026
82d2251
build: add linux yaml module checksums
M09Ic Jul 20, 2026
9c88c98
fix: close native runner CI gaps
M09Ic Jul 20, 2026
2e9690a
build: drop superseded pty checksums
M09Ic Jul 20, 2026
9626944
build: restore portable libcstx profile
M09Ic Jul 20, 2026
2390f1c
fix(build): package frontend and generate CLI help
M09Ic Jul 20, 2026
9f099e7
refactor(pty): use canonical frames across transports
M09Ic Jul 20, 2026
b96d020
refactor(commands): unify execution with PTY sessions
M09Ic Jul 20, 2026
d1c26a1
refactor(stdio): accept plain request and stream raw AOP
M09Ic Jul 21, 2026
44b1f3a
refactor(runner): drop cairnrunner, expose tool-only node via webagent
M09Ic Jul 21, 2026
fed2aec
test(webagent): cover tool node wire interop with cairn bridge dialect
M09Ic Jul 21, 2026
8046a1f
feat(aop): execute runner tools over canonical events
M09Ic Jul 21, 2026
b1f0ae8
feat(commands): accept per-call timeout in bash tool
M09Ic Jul 21, 2026
45fc21b
refactor(aop): adopt AOP as the single agent semantics carrier
M09Ic Jul 21, 2026
02ad0a7
feat(web): add live IOA console coverage
M09Ic Jul 21, 2026
85ea7ee
fix(webagent): persist scanner assets from chat nodes
M09Ic Jul 21, 2026
f5c70a2
fix(scan): emit composite results to asset pipeline
M09Ic Jul 21, 2026
7b6a7ae
refactor(agent): drop SteerUserMessage, queue TUI input at run boundary
M09Ic Jul 21, 2026
e517401
refactor(web): converge chat tasks on the root session.end event
M09Ic Jul 21, 2026
1b2aa07
test(agent,runner,web): lock AOP encoder, input, stdio, and replay se…
M09Ic Jul 21, 2026
cbe3528
build(frontend): exclude test files from hub tsconfig, bump cyber-ui
M09Ic Jul 21, 2026
cbeb5c1
feat(frontend): align chat timeline and IOA references
M09Ic Jul 22, 2026
0d6bc40
fix(ci): restore portable module and embed inputs
M09Ic Jul 22, 2026
9427ff6
fix(ci): preserve generated template dependencies
M09Ic Jul 22, 2026
fbb1abb
feat: load prompt content from existing files
M09Ic Jul 22, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ jobs:
go-version-file: go.mod
cache: true

- name: Set up Node.js
if: matrix.id == 'full'
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: web/frontend/package-lock.json

- name: Build embedded frontend
if: matrix.id == 'full'
run: |
npm --prefix web/frontend ci
npm --prefix web/frontend run build
test -s web/static/index.html
test -n "$(find web/static/assets -type f -size +0c -print -quit)"

- name: Generate embedded resources
if: matrix.generate
run: go generate ./core/resources
Expand Down
20 changes: 18 additions & 2 deletions .github/workflows/go-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,13 @@ jobs:
profile: standard
main: ./cmd/aiscan
binary: aiscan
tags: "forceposix emptytemplates noembed osusergo netgo"
tags: "forceposix emptytemplates noembed osusergo netgo cstx_native"
generate: true
- id: aiscan-full
profile: full
main: ./cmd/aiscan
binary: aiscan-full
tags: "forceposix emptytemplates noembed osusergo netgo full sqlite"
tags: "forceposix emptytemplates noembed osusergo netgo full cstx_native katana_slim"
generate: true
- id: aiscan-agent
profile: agent
Expand All @@ -126,6 +126,22 @@ jobs:
go-version-file: go.mod
cache: true

- name: Set up Node.js
if: matrix.profile == 'full'
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: web/frontend/package-lock.json

- name: Build embedded frontend
if: matrix.profile == 'full'
run: |
npm --prefix web/frontend ci
npm --prefix web/frontend run build
test -s web/static/index.html
test -n "$(find web/static/assets -type f -size +0c -print -quit)"

- name: Generate embedded resources
if: matrix.generate
run: go generate ./core/resources
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ jobs:
go-version-file: go.mod
cache: true

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: web/frontend/package-lock.json

- name: Install upx
run: sudo apt install upx -y
continue-on-error: true
Expand All @@ -65,6 +72,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOPATH: "/home/runner/go"

- name: Verify embedded frontend was built
run: |
test -s web/static/index.html
test -n "$(find web/static/assets -type f -size +0c -print -quit)"

- name: Publish release (remove draft)
run: gh release edit "$TAG" --draft=false --prerelease
env:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ FULL_BIN ?= $(BIN_DIR)/aiscan-full$(EXE)
# Keep the local feature tiers aligned with the release workflow.
STANDARD_TAGS := forceposix emptytemplates noembed osusergo netgo cstx_native $(RE2_TAGS)
AGENT_TAGS := forceposix emptytemplates noembed osusergo netgo
FULL_TAGS := forceposix emptytemplates noembed osusergo netgo full sqlite cstx_native katana_slim $(RE2_TAGS)
FULL_TAGS := forceposix emptytemplates noembed osusergo netgo full cstx_native katana_slim $(RE2_TAGS)
BUILD_FLAGS := -trimpath -buildvcs=false

.PHONY: help prepare frontend standard agent full web-build web-run web all clean
Expand Down
30 changes: 22 additions & 8 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"

Expand Down Expand Up @@ -62,26 +63,39 @@ Examples:
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(option.Timeout)*time.Second)
defer cancel()

var interruptMu sync.RWMutex
var interruptFn func() bool
sigChan := make(chan os.Signal, 2)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(sigChan)
go func() {
for {
<-sigChan
if interruptFn != nil && interruptFn() {
for sig := range sigChan {
interruptMu.RLock()
fn := interruptFn
interruptMu.RUnlock()
if sig == os.Interrupt && fn != nil && fn() {
continue
}
fmt.Fprintf(os.Stderr, "\nPress Ctrl+C again to exit\n")
<-sigChan
os.Exit(1)
cancel()
return
}
}()

if option.WebURL != "" {
transport, transportErr := cfg.ResolveAgentTransport(&option)
if transportErr != nil {
logger.Errorf("agent failed: %s", transportErr)
os.Exit(1)
}
switch transport {
case cfg.AgentTransportWeb:
err = webagent.Run(ctx, &option, logger)
} else {
case cfg.AgentTransportStdio:
err = runner.RunStdio(ctx, &option, logger, os.Stdin, os.Stdout)
default:
err = runner.RunAgentMode(ctx, &option, logger, func(fn func() bool) {
interruptMu.Lock()
interruptFn = fn
interruptMu.Unlock()
})
}
if err != nil {
Expand Down
33 changes: 30 additions & 3 deletions cmd/aiscan/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"slices"
Expand Down Expand Up @@ -54,6 +55,8 @@ type agentCommand struct {
cfg.ReconOptions `group:"Recon Options"`
}

func (agentCommand) Usage() string { return "[OPTIONS]" }

type serveCommand struct {
Token string `long:"token" description:"Access key for the server (auto-generated if empty)"`
Addr string `long:"addr" default:"127.0.0.1:8765" description:"HTTP listen address"`
Expand Down Expand Up @@ -162,9 +165,17 @@ func aiscan() {
switch parsed.Mode {
case cfg.RunModeAgent:
var err error
if option.WebURL != "" {
transport, transportErr := cfg.ResolveAgentTransport(&option)
if transportErr != nil {
logger.Errorf("agent failed: %s", transportErr)
os.Exit(1)
}
switch transport {
case cfg.AgentTransportWeb:
err = webagent.Run(ctx, &option, logger)
} else {
case cfg.AgentTransportStdio:
err = runner.RunStdio(ctx, &option, logger, os.Stdin, os.Stdout)
default:
err = runner.RunAgentMode(ctx, &option, logger, sigHandler.SetStopFunc)
}
if err != nil {
Expand Down Expand Up @@ -728,5 +739,21 @@ func setupSignalHandler(cancel context.CancelFunc, logger telemetry.Logger) *sig
}

func printHelp(parser *goflags.Parser) {
parser.WriteHelp(os.Stdout)
writeHelp(parser, os.Stdout)
}

func writeHelp(parser *goflags.Parser, writer io.Writer) {
if parser.Active == nil {
parser.WriteHelp(writer)
return
}

// Parser.Usage contains the long root command catalog. go-flags reuses it
// verbatim when rendering subcommand help, which pushes the active command's
// flags below the fold. Keep the detailed catalog for `aiscan -h`, but use a
// compact root prefix for `aiscan <command> -h`.
rootUsage := parser.Usage
parser.Usage = "[GLOBAL OPTIONS]"
defer func() { parser.Usage = rootUsage }()
parser.WriteHelp(writer)
}
59 changes: 59 additions & 0 deletions cmd/aiscan/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@ import (
"github.com/chainreactors/aiscan/pkg/telemetry"
"github.com/chainreactors/aiscan/pkg/tui"
"github.com/chainreactors/aiscan/skills"
goflags "github.com/jessevdk/go-flags"
)

func containsAny(value string, candidates ...string) bool {
for _, candidate := range candidates {
if strings.Contains(value, candidate) {
return true
}
}
return false
}

type fakeConsoleProvider struct {
requests int
}
Expand Down Expand Up @@ -144,6 +154,55 @@ func TestParseCLIAgentAcceptsLLMFlags(t *testing.T) {
}
}

func TestAgentHelpRendersAgentOptionsWithoutRootCatalog(t *testing.T) {
var cli cliOptions
parser := newCLIParser(&cli, parserOptionsForArgs([]string{"agent", "-h"}))
_, err := parser.ParseArgs([]string{"agent", "-h"})
flagsErr, ok := err.(*goflags.Error)
if !ok || flagsErr.Type != goflags.ErrHelp {
t.Fatalf("ParseArgs() error = %v, want ErrHelp", err)
}

var buf bytes.Buffer
writeHelp(parser, &buf)
help := buf.String()
for _, wants := range [][]string{
{"agent [OPTIONS]"},
{"Agent Options:"},
{"--prompt", "/prompt"},
{"--transport", "/transport"},
{"--server-url", "/server-url"},
} {
if !containsAny(help, wants...) {
want := strings.Join(wants, " or ")
t.Fatalf("agent help missing %q:\n%s", want, help)
}
}
if strings.Contains(help, "Advanced scanners:") || strings.Contains(help, "Server management:") {
t.Fatalf("agent help leaked the root command catalog:\n%s", help)
}
}

func TestScannerHelpRegistryUsesGeneratedFlagHelp(t *testing.T) {
for _, name := range []string{"scan", "gogo", "spray", "zombie", "neutron"} {
t.Run(name, func(t *testing.T) {
help, ok := cfg.StaticScannerUsage(name)
if !ok {
t.Fatalf("StaticScannerUsage(%q) was not registered", name)
}
if !strings.Contains(help, "Usage:") || !strings.Contains(help, name+" [OPTIONS]") {
t.Fatalf("%s help was not rendered by its go-flags parser:\n%s", name, help)
}
if !strings.Contains(help, "Help Options:") {
t.Fatalf("%s help is missing go-flags help options:\n%s", name, help)
}
if strings.Count(help, "\n") < 10 {
t.Fatalf("%s help looks like a static placeholder:\n%s", name, help)
}
})
}
}

func TestParseCLIScanExtractsLLMFlags(t *testing.T) {
parsed, err := parseCLI([]string{
"scan",
Expand Down
9 changes: 6 additions & 3 deletions cmd/aiscan/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func scannerWithAgent(ctx context.Context, option *cfg.Option, application *runn
result, err := agent.NewAgent(rt.Config.
WithSystemPrompt(rt.SystemPrompt).
WithStream(false)).
Run(ctx, prompt)
Run(ctx, agent.TextInput(prompt))
if err != nil {
return err
}
Expand All @@ -171,7 +171,10 @@ func resolveScannerIntent(option *cfg.Option, store *skills.Store, command strin
}
}

intent := strings.TrimSpace(option.Prompt)
intent, err := cfg.ResolvePrompt(option.Prompt)
if err != nil {
return "", err
}
if intent == "" && option.TaskFile != "" {
data, err := os.ReadFile(option.TaskFile)
if err != nil {
Expand All @@ -182,7 +185,7 @@ func resolveScannerIntent(option *cfg.Option, store *skills.Store, command strin
if intent == "" {
intent = "Process the scanner output according to the user's intent. If no specific intent is provided, briefly explain the important evidence in the output."
}
intent, err := cfg.ApplySelectedSkills(intent, scan.FilterAutoSkill(option.Skills, command), store)
intent, err = cfg.ApplySelectedSkills(intent, scan.FilterAutoSkill(option.Skills, command), store)
if err != nil {
return "", err
}
Expand Down
Loading
Loading