mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Task credentials (#3): declare test accounts at task creation, and never lose the accounts the agent registers on its own. - task_credentials table (unique per task+domain+username; upsert overwrites the password in place), idempotent schema migration - POST /api/tasks accepts credentials; GET/POST/DELETE /api/tasks/{id}/credentials serve the UI (manual add, e.g. relaying an OTP-verified account; delete) - empty password = the "agent generates" mode: the entry surfaces in the worker's graph overview with an explicit instruction to register that account with a strong password and record it - new tools save_task_credential / list_task_credentials for worker and pentest; the graph overview injects the task's credentials each round with a standing note: reuse matching entries, always record accounts you register - New Task dialog: optional credentials section (domain, username, password, note) with a dedicated-test-accounts hint - task detail: Credentials tab, passwords masked with explicit reveal/copy, agent-recorded badge, light polling so mid-run recordings appear One-shot binding migration (settings-flag-guarded, same pattern as the other seeds) grants the two tools to worker+pentest on databases seeded before this change; fresh DBs get them via WorkerTools()/the pentest catalog. Closes #3
126 lines
4.5 KiB
Go
126 lines
4.5 KiB
Go
package db
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// TaskCredential is a per-task test account (#3): either declared by the user at
|
|
// task creation (source=user, e.g. a program-provided alias account) or registered
|
|
// by the agent itself and recorded so it is not forgotten (source=agent).
|
|
//
|
|
// Passwords are stored in the local Postgres, plaintext — the same trust domain as
|
|
// captured traffic evidence, which already contains credentials by nature. The UI
|
|
// masks them by default; anything injected into agent context transits to the LLM
|
|
// provider, so only dedicated test accounts belong here.
|
|
type TaskCredential struct {
|
|
ID int64 `json:"id"`
|
|
TaskID int64 `json:"task_id"`
|
|
Domain string `json:"domain"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Source string `json:"source"` // "user" | "agent"
|
|
Note string `json:"note,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// TaskCredentialInput validates and normalizes one upsert. Empty username or
|
|
// domain is rejected; an empty password is allowed only for source=user (the
|
|
// "agent generates and records it later" mode from #3).
|
|
type TaskCredentialInput struct {
|
|
Domain string
|
|
Username string
|
|
Password string
|
|
Source string
|
|
Note string
|
|
}
|
|
|
|
var ErrTaskCredentialInvalid = errors.New("credential domain and username are required")
|
|
|
|
func (in TaskCredentialInput) normalize() (TaskCredentialInput, error) {
|
|
in.Domain = strings.TrimSpace(in.Domain)
|
|
in.Username = strings.TrimSpace(in.Username)
|
|
in.Password = strings.TrimSpace(in.Password)
|
|
in.Note = strings.TrimSpace(in.Note)
|
|
if in.Source != "agent" {
|
|
in.Source = "user"
|
|
}
|
|
if in.Domain == "" || in.Username == "" {
|
|
return in, ErrTaskCredentialInvalid
|
|
}
|
|
if in.Password == "" && in.Source == "agent" {
|
|
return in, errors.New("agent-recorded credentials need the password that was set")
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
// UpsertTaskCredential inserts or updates the (task, domain, username) entry: an
|
|
// agent re-registering the same account overwrites the password in place instead
|
|
// of piling up a second row.
|
|
func (d *DB) UpsertTaskCredential(taskID int64, in TaskCredentialInput) (TaskCredential, error) {
|
|
in, err := in.normalize()
|
|
if err != nil {
|
|
return TaskCredential{}, err
|
|
}
|
|
var c TaskCredential
|
|
err = d.QueryRow(`
|
|
INSERT INTO task_credentials (task_id, domain, username, password, source, note)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (task_id, domain, username) DO UPDATE
|
|
SET password = EXCLUDED.password,
|
|
source = EXCLUDED.source,
|
|
note = CASE WHEN EXCLUDED.note = '' THEN task_credentials.note ELSE EXCLUDED.note END,
|
|
updated_at = now()
|
|
RETURNING id, task_id, domain, username, password, source, note, created_at, updated_at`,
|
|
taskID, in.Domain, in.Username, in.Password, in.Source, in.Note).
|
|
Scan(&c.ID, &c.TaskID, &c.Domain, &c.Username, &c.Password, &c.Source, &c.Note, &c.CreatedAt, &c.UpdatedAt)
|
|
return c, err
|
|
}
|
|
|
|
// TaskCredentials lists a task's credentials, oldest first (the user-declared
|
|
// ones keep their position ahead of agent-discovered ones).
|
|
func (d *DB) TaskCredentials(taskID int64) ([]TaskCredential, error) {
|
|
rows, err := d.Query(`
|
|
SELECT id, task_id, domain, username, password, source, note, created_at, updated_at
|
|
FROM task_credentials WHERE task_id = $1 ORDER BY id`, taskID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []TaskCredential
|
|
for rows.Next() {
|
|
var c TaskCredential
|
|
if err := rows.Scan(&c.ID, &c.TaskID, &c.Domain, &c.Username, &c.Password, &c.Source, &c.Note, &c.CreatedAt, &c.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// DeleteTaskCredential removes one entry; taskID scopes the delete so a stale
|
|
// request cannot cross tasks.
|
|
func (d *DB) DeleteTaskCredential(taskID, id int64) error {
|
|
tag, err := d.Exec(`DELETE FROM task_credentials WHERE id = $1 AND task_id = $2`, id, taskID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n, _ := tag.RowsAffected(); n == 0 {
|
|
return errors.New("credential not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Thin wrappers on ExplorationStore: the ToolSet already holds the task's
|
|
// exploration store, so credential tools need no extra wiring.
|
|
|
|
func (s *ExplorationStore) UpsertTaskCredential(taskID int64, in TaskCredentialInput) (TaskCredential, error) {
|
|
return s.db.UpsertTaskCredential(taskID, in)
|
|
}
|
|
|
|
func (s *ExplorationStore) TaskCredentials(taskID int64) ([]TaskCredential, error) {
|
|
return s.db.TaskCredentials(taskID)
|
|
}
|