Skip to content

Commit 1476d22

Browse files
committed
dashboard/app: AI patching jobs
1 parent 8f2b5c6 commit 1476d22

File tree

14 files changed

+324
-3
lines changed

14 files changed

+324
-3
lines changed

dashboard/app/ai.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package main
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
11+
"github.com/google/syzkaller/dashboard/app/aidb"
12+
"github.com/google/syzkaller/dashboard/dashapi"
13+
"google.golang.org/appengine/v2/log"
14+
)
15+
16+
type uiAIPage struct {
17+
Header *uiHeader
18+
Workflows []*aidb.Workflow
19+
}
20+
21+
func handleAIPage(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
22+
hdr, err := commonHeader(ctx, r, w, "")
23+
if err != nil {
24+
return err
25+
}
26+
workflows, err := aidb.LoadWorkflows(ctx)
27+
if err != nil {
28+
return err
29+
}
30+
page := &uiAIPage{
31+
Header: hdr,
32+
Workflows: workflows,
33+
}
34+
return serveTemplate(w, "ai.html", page)
35+
}
36+
37+
func apiAIJobPoll(ctx context.Context, req *dashapi.AIJobPollReq) (any, error) {
38+
if err := aidb.UpdateWorkflows(ctx, req.Workflows); err != nil {
39+
log.Errorf(ctx, "storeAIWorkflows: %v", err)
40+
}
41+
resp := &dashapi.AIJobPollResp{}
42+
return resp, nil
43+
}
44+
45+
func apiAIJobDone(ctx context.Context, req *dashapi.AIJobDoneReq) (any, error) {
46+
return nil, nil
47+
}
48+
49+
func apiAIJournal(ctx context.Context, req *dashapi.AIJournalReq) (any, error) {
50+
return nil, nil
51+
}
52+
53+
func aiWorkflowCreate(ctx context.Context, bug *Bug) error {
54+
crash, _, err := findCrashForBug(ctx, bug)
55+
if err != nil {
56+
return err
57+
}
58+
if crash.ReproSyz == 0 {
59+
return fmt.Errorf("the bug does not have a reproducer")
60+
}
61+
return nil
62+
}

dashboard/app/aidb/entities.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package aidb
5+
6+
type Workflow struct {
7+
Name string
8+
Active bool
9+
}

dashboard/app/aidb/helpers.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package aidb
5+
6+
import (
7+
"context"
8+
9+
"cloud.google.com/go/spanner"
10+
"google.golang.org/appengine/v2"
11+
)
12+
13+
func LoadWorkflows(ctx context.Context) ([]*Workflow, error) {
14+
client, err := dbClient(ctx)
15+
if err != nil {
16+
return nil, err
17+
}
18+
defer client.Close()
19+
iter := client.Single().Query(ctx, spanner.Statement{
20+
SQL: `select * from workflows`,
21+
})
22+
defer iter.Stop()
23+
var workflows []*Workflow
24+
err = spanner.SelectAll(iter, &workflows)
25+
return workflows, err
26+
}
27+
28+
func LoadActiveWorkflows(ctx context.Context) ([]string, error) {
29+
workflows, err := LoadWorkflows(ctx)
30+
if err != nil {
31+
return nil, err
32+
}
33+
var active []string
34+
for _, flow := range workflows {
35+
if flow.Active {
36+
active = append(active, flow.Name)
37+
}
38+
}
39+
return active, nil
40+
}
41+
42+
func UpdateWorkflows(ctx context.Context, active []string) error {
43+
workflows, err := LoadWorkflows(ctx)
44+
if err != nil {
45+
return err
46+
}
47+
m := make(map[string]bool)
48+
for _, flow := range active {
49+
m[flow] = true
50+
}
51+
update := false
52+
for _, flow := range workflows {
53+
active := m[flow.Name]
54+
delete(m, flow.Name)
55+
if flow.Active != active {
56+
update = true
57+
flow.Active = active
58+
}
59+
}
60+
for name := range m {
61+
update = true
62+
workflows = append(workflows, &Workflow{
63+
Name: name,
64+
Active: true,
65+
})
66+
}
67+
if !update {
68+
return nil
69+
}
70+
client, err := dbClient(ctx)
71+
if err != nil {
72+
return err
73+
}
74+
defer client.Close()
75+
var mutations []*spanner.Mutation
76+
for _, flow := range workflows {
77+
mut, err := spanner.InsertOrUpdateStruct("workflows", flow)
78+
if err != nil {
79+
return err
80+
}
81+
mutations = append(mutations, mut)
82+
}
83+
_, err = client.Apply(ctx, mutations)
84+
return err
85+
}
86+
87+
func dbClient(ctx context.Context) (*spanner.Client, error) {
88+
database := "projects/" + appengine.AppID(ctx) + "/instances/syzbot/databases/ai"
89+
return spanner.NewClient(ctx, database)
90+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE workflows;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
CREATE TABLE Workflows (
2+
Name STRING(1000) NOT NULL,
3+
Active BOOL NOT NULL,
4+
) PRIMARY KEY (Name);
5+
6+
CREATE TABLE Jobs (
7+
ID STRING(1000) NOT NULL,
8+
Workflow STRING(1000) NOT NULL,
9+
-- Status STRING(128) NOT NULL,
10+
Error STRING(1000),
11+
Created TIMESTAMP NOT NULL,
12+
Started TIMESTAMP,
13+
Finished TIMESTAMP,
14+
-- Patching STRUCT<>,
15+
16+
-- CONSTRAINT StatusEnum CHECK (Status IN ('pending', 'running', 'finished')),
17+
CONSTRAINT FK_JobWorkflowJob FOREIGN KEY (Workflow) REFERENCES Workflows (Name),
18+
) PRIMARY KEY (ID);
19+
20+
CREATE TABLE PatchingJobs (
21+
ID STRING(1000) NOT NULL,
22+
ReproOpts INT64 NOT NULL,
23+
ReproSyz INT64 NOT NULL,
24+
ReproC INT64,
25+
KernelConfig INT64 NOT NULL,
26+
SyzkallerCommit STRING(100) NOT NULL,
27+
28+
CONSTRAINT FK_PatchingJobJob FOREIGN KEY (ID) REFERENCES Jobs (ID),
29+
) PRIMARY KEY (ID);
30+
31+
CREATE TABLE Events (
32+
JobID STRING(1000) NOT NULL,
33+
Seq INT64,
34+
-- XXXXXXXXXXXXXXXXXXXX
35+
36+
CONSTRAINT FK_EventJob FOREIGN KEY (JobID) REFERENCES Jobs (ID),
37+
) PRIMARY KEY (JobID, Seq);
38+

dashboard/app/api.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ var apiHandlers = map[string]APIHandler{
6060
"save_discussion": typedHandler(apiSaveDiscussion),
6161
"create_upload_url": typedHandler(apiCreateUploadURL),
6262
"send_email": typedHandler(apiSendEmail),
63+
"ai_job_poll": typedHandler(apiAIJobPoll),
64+
"ai_job_done": typedHandler(apiAIJobDone),
65+
"ai_journal": typedHandler(apiAIJournal),
6366
"save_coverage": gcsPayloadHandler(apiSaveCoverage),
6467
"upload_build": nsHandler(apiUploadBuild),
6568
"builder_poll": nsHandler(apiBuilderPoll),

dashboard/app/app.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ handlers:
2626
static_dir: dashboard/app/static
2727
secure: always
2828
# debug is for net/http/pprof handlers.
29-
- url: /(admin|debug/.*|cron/.*)
29+
- url: /(admin|ai|debug/.*|cron/.*)
3030
script: auto
3131
login: admin
3232
secure: always

dashboard/app/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ type Config struct {
8787
// If set, this namespace is not actively tested, no notifications are sent, etc.
8888
// It's kept mostly read-only for historical reference.
8989
Decommissioned bool
90+
// Enable AI workflows for the namespace.
91+
AI bool
9092
// Name used in UI.
9193
DisplayTitle string
9294
// Unique string that allows to show "similar bugs" across different namespaces.

dashboard/app/handler.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ func serveTemplate(w http.ResponseWriter, name string, data any) error {
214214

215215
type uiHeader struct {
216216
Admin bool
217+
AI bool
217218
URLPath string
218219
LoginLink string
219220
AnalyticsTrackingID string
@@ -225,6 +226,7 @@ type uiHeader struct {
225226
Namespaces []uiNamespace
226227
ShowSubsystems bool
227228
ShowCoverageMenu bool
229+
Message string // Show message box with this message when the page loads.
228230
}
229231

230232
type uiNamespace struct {
@@ -299,8 +301,10 @@ func commonHeader(c context.Context, r *http.Request, w http.ResponseWriter, ns
299301
}
300302
}
301303
if ns != adminPage {
304+
cfg := getNsConfig(c, ns)
302305
h.Namespace = ns
303-
h.ShowSubsystems = getNsConfig(c, ns).Subsystems.Service != nil
306+
h.AI = h.Admin && cfg.AI
307+
h.ShowSubsystems = cfg.Subsystems.Service != nil
304308
cookie.Namespace = ns
305309
encodeCookie(w, cookie)
306310
cached, err := CacheGet(c, r, ns)
@@ -309,7 +313,7 @@ func commonHeader(c context.Context, r *http.Request, w http.ResponseWriter, ns
309313
}
310314
h.BugCounts = &cached.Total
311315
h.MissingBackports = cached.MissingBackports
312-
h.ShowCoverageMenu = getNsConfig(c, ns).Coverage != nil
316+
h.ShowCoverageMenu = cfg.Coverage != nil
313317
}
314318
return h, nil
315319
}

dashboard/app/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"cloud.google.com/go/logging"
2323
"cloud.google.com/go/logging/logadmin"
24+
"github.com/google/syzkaller/dashboard/app/aidb"
2425
"github.com/google/syzkaller/dashboard/dashapi"
2526
"github.com/google/syzkaller/pkg/debugtracer"
2627
"github.com/google/syzkaller/pkg/email"
@@ -83,6 +84,7 @@ func initHTTPHandlers() {
8384
http.Handle("/"+ns+"/backports", handlerWrapper(handleBackports))
8485
http.Handle("/"+ns+"/s/", handlerWrapper(handleSubsystemPage))
8586
http.Handle("/"+ns+"/manager/", handlerWrapper(handleManagerPage))
87+
http.Handle("/"+ns+"/ai/", handlerWrapper(handleAIPage))
8688
}
8789
http.HandleFunc("/cron/cache_update", cacheUpdate)
8890
http.HandleFunc("/cron/minute_cache_update", handleMinuteCacheUpdate)
@@ -300,6 +302,7 @@ type uiBugPage struct {
300302
TestPatchJobs *uiJobList
301303
LabelGroups []*uiBugLabelGroup
302304
DebugSubsystems string
305+
AIWorkflows []string
303306
}
304307

305308
type uiBugLabelGroup struct {
@@ -1215,6 +1218,13 @@ func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error
12151218
Crashes: crashesTable,
12161219
LabelGroups: getLabelGroups(c, bug),
12171220
}
1221+
if hdr.AI && bug.ReproLevel > dashapi.ReproLevelNone {
1222+
workflows, err := aidb.LoadActiveWorkflows(c)
1223+
if err != nil {
1224+
return err
1225+
}
1226+
data.AIWorkflows = workflows
1227+
}
12181228
if accessLevel == AccessAdmin && !bug.hasUserSubsystems() {
12191229
data.DebugSubsystems = urlutil.SetParam(data.Bug.Link, "debug_subsystems", "1")
12201230
}
@@ -1243,6 +1253,13 @@ func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error
12431253
"Cause bisection attempts", uiList))
12441254
}
12451255
}
1256+
1257+
if workflow := r.FormValue("ai-workflow-create"); workflow != "" {
1258+
if err := aiWorkflowCreate(c, bug); err != nil {
1259+
return err
1260+
}
1261+
hdr.Message = fmt.Sprintf("AI workflow %v is created", workflow)
1262+
}
12461263
if r.FormValue("json") == "1" {
12471264
w.Header().Set("Content-Type", "application/json")
12481265
return writeJSONVersionOf(w, data)

0 commit comments

Comments
 (0)