RC-7
Some checks failed
release-tag / release-image (push) Failing after 2m44s

This commit is contained in:
2026-08-11 16:58:07 +02:00
parent 185ccf1101
commit bcfbef390f
44 changed files with 4778 additions and 172 deletions

View File

@@ -32,6 +32,10 @@ CREATE TABLE IF NOT EXISTS tasks (
created_at INTEGER NOT NULL,
completed_at INTEGER,
winner_client_id TEXT REFERENCES clients(id),
winner_worker_client_id TEXT REFERENCES clients(id),
winner_beacon_path TEXT NOT NULL DEFAULT '',
winner_beacon_boosted_path TEXT NOT NULL DEFAULT '',
winner_beacon_round INTEGER NOT NULL DEFAULT 0,
winner_signature TEXT,
winning_guess TEXT,
artifact_status TEXT NOT NULL DEFAULT 'none' CHECK (artifact_status IN ('none','pending','generating','ready','error')),
@@ -128,3 +132,44 @@ CREATE TABLE IF NOT EXISTS artifact_api_usage (
CREATE INDEX IF NOT EXISTS artifact_api_usage_created_idx ON artifact_api_usage(created_at DESC);
CREATE INDEX IF NOT EXISTS artifact_api_usage_kind_created_idx ON artifact_api_usage(kind, created_at DESC);
CREATE INDEX IF NOT EXISTS artifact_api_usage_task_idx ON artifact_api_usage(task_id);
-- Publicly auditable Beacon Hunt draws. Each row records the externally
-- sourced drand reveal used for the weighted path/draw decision.
CREATE TABLE IF NOT EXISTS beacon_draws (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
window_end INTEGER NOT NULL,
beacon_id TEXT NOT NULL,
beacon_round INTEGER NOT NULL,
randomness TEXT NOT NULL,
signature TEXT NOT NULL DEFAULT '',
boosted_path TEXT NOT NULL,
ticket_count INTEGER NOT NULL,
selected_count INTEGER NOT NULL,
created_at INTEGER NOT NULL,
UNIQUE(task_id, window_end)
);
CREATE INDEX IF NOT EXISTS beacon_draws_task_idx ON beacon_draws(task_id, window_end DESC);
-- A hosted worker uses its own cryptographic identity/presence, while prizes can
-- be delegated to a durable customer-owned reward identity. The worker remains
-- auditable on the completed task through winner_worker_client_id.
CREATE TABLE IF NOT EXISTS identity_delegations (
worker_client_id TEXT PRIMARY KEY REFERENCES clients(id) ON DELETE CASCADE,
owner_client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS identity_delegations_owner_idx ON identity_delegations(owner_client_id);
-- One-shot pairing codes let a logged-in owner prove control of the reward
-- identity to Customer Service without sharing the P-256 private key. Only the
-- SHA-256 token hash is stored and codes expire quickly.
CREATE TABLE IF NOT EXISTS customer_link_tokens (
token_hash TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS customer_link_tokens_exp_idx ON customer_link_tokens(expires_at);

View File

@@ -109,6 +109,10 @@ func OpenSQLite(ctx context.Context, path string) (*sql.DB, error) {
{"nft_prompt_instructions", "TEXT NOT NULL DEFAULT ''"},
{"nft_negative_prompt", "TEXT NOT NULL DEFAULT ''"},
{"nft_style_reference", "TEXT NOT NULL DEFAULT ''"},
{"winner_worker_client_id", "TEXT REFERENCES clients(id)"},
{"winner_beacon_path", "TEXT NOT NULL DEFAULT ''"},
{"winner_beacon_boosted_path", "TEXT NOT NULL DEFAULT ''"},
{"winner_beacon_round", "INTEGER NOT NULL DEFAULT 0"},
} {
if err := ensureColumn(ctx, db, "tasks", m.name, m.def); err != nil {
db.Close()
@@ -220,22 +224,23 @@ type Task struct {
CreatedAt time.Time
CompletedAt *time.Time
WinnerClientID *string
WinnerWorkerClientID *string
ArtifactStatus string
ArtifactURI *string
ArtifactManifestURI *string
}
const taskColumns = `id,public_seed,range_bits,status,paused,guess_min_interval_sec,client_submit_interval_sec,revision,parent_task_id,display_name,description,nft_prompt_instructions,nft_negative_prompt,nft_style_reference,created_at,completed_at,winner_client_id,artifact_status,artifact_uri,artifact_manifest_uri`
const taskColumns = `id,public_seed,range_bits,status,paused,guess_min_interval_sec,client_submit_interval_sec,revision,parent_task_id,display_name,description,nft_prompt_instructions,nft_negative_prompt,nft_style_reference,created_at,completed_at,winner_client_id,winner_worker_client_id,artifact_status,artifact_uri,artifact_manifest_uri`
func scanTask(scanner interface{ Scan(...any) error }, withSecret bool) (Task, string, error) {
var t Task
var created int64
var completed sql.NullInt64
var winner, artifactURI, manifestURI, parent sql.NullString
var winner, winnerWorker, artifactURI, manifestURI, parent sql.NullString
var guessMin, clientSubmit sql.NullInt64
var paused int
var secret string
args := []any{&t.ID, &t.PublicSeed, &t.RangeBits, &t.Status, &paused, &guessMin, &clientSubmit, &t.Revision, &parent, &t.DisplayName, &t.Description, &t.NFTPromptInstructions, &t.NFTNegativePrompt, &t.NFTStyleReference, &created, &completed, &winner, &t.ArtifactStatus, &artifactURI, &manifestURI}
args := []any{&t.ID, &t.PublicSeed, &t.RangeBits, &t.Status, &paused, &guessMin, &clientSubmit, &t.Revision, &parent, &t.DisplayName, &t.Description, &t.NFTPromptInstructions, &t.NFTNegativePrompt, &t.NFTStyleReference, &created, &completed, &winner, &winnerWorker, &t.ArtifactStatus, &artifactURI, &manifestURI}
if withSecret {
args = append(args, &secret)
}
@@ -264,6 +269,10 @@ func scanTask(scanner interface{ Scan(...any) error }, withSecret bool) (Task, s
v := winner.String
t.WinnerClientID = &v
}
if winnerWorker.Valid {
v := winnerWorker.String
t.WinnerWorkerClientID = &v
}
if artifactURI.Valid {
v := artifactURI.String
t.ArtifactURI = &v
@@ -864,6 +873,62 @@ func (s *Store) PublicArtifacts(ctx context.Context, limit int, winner string) (
return out, rows.Err()
}
// OwnedArtifact is returned only to the authenticated winner. Unlike the
// public gallery it includes a private download URL for the original artifact.
type OwnedArtifact struct {
TaskID string `json:"task_id"`
DisplayName string `json:"display_name"`
RangeBits int `json:"range_bits"`
CompletedAt time.Time `json:"completed_at"`
PreviewURI string `json:"preview_uri"`
DownloadURI string `json:"download_uri"`
}
func (s *Store) OwnedArtifacts(ctx context.Context, cid string, limit int) ([]OwnedArtifact, error) {
if limit < 1 || limit > 200 {
limit = 48
}
cid = strings.TrimSpace(cid)
rows, err := s.DB.QueryContext(ctx, `SELECT id,COALESCE(display_name,''),range_bits,COALESCE(completed_at,created_at)
FROM tasks
WHERE status='completed' AND artifact_status='ready' AND artifact_uri IS NOT NULL AND winner_client_id=?
ORDER BY COALESCE(completed_at,created_at) DESC LIMIT ?`, cid, limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]OwnedArtifact, 0)
for rows.Next() {
var a OwnedArtifact
var completed int64
if err := rows.Scan(&a.TaskID, &a.DisplayName, &a.RangeBits, &completed); err != nil {
return nil, err
}
a.CompletedAt = fromUnixMS(completed)
escaped := url.PathEscape(a.TaskID)
a.PreviewURI = "/api/public/artifacts/" + escaped + "/preview"
a.DownloadURI = "/api/me/artifacts/" + escaped + "/download"
out = append(out, a)
}
return out, rows.Err()
}
// OwnedArtifactSource returns the original artifact only when taskID belongs to
// cid. Keeping the ownership check in the database query makes it difficult for
// future handlers to accidentally turn the private download endpoint into an
// insecure direct object reference.
func (s *Store) OwnedArtifactSource(ctx context.Context, taskID, cid string) (artifactURI string, ok bool, err error) {
err = s.DB.QueryRowContext(ctx, `SELECT artifact_uri FROM tasks
WHERE id=? AND winner_client_id=? AND status='completed' AND artifact_status='ready' AND artifact_uri IS NOT NULL`, taskID, cid).Scan(&artifactURI)
if errors.Is(err, sql.ErrNoRows) {
return "", false, nil
}
if err != nil {
return "", false, err
}
return artifactURI, true, nil
}
func (s *Store) PublicArtifactSource(ctx context.Context, taskID string) (artifactURI, winner string, ok bool, err error) {
err = s.DB.QueryRowContext(ctx, `SELECT artifact_uri,winner_client_id FROM tasks
WHERE id=? AND status='completed' AND artifact_status='ready' AND artifact_uri IS NOT NULL AND winner_client_id IS NOT NULL`, taskID).Scan(&artifactURI, &winner)
@@ -1351,7 +1416,8 @@ func (s *Store) InactiveNonWinnerClients(ctx context.Context, cutoffMS int64) ([
rows, err := s.DB.QueryContext(ctx, `SELECT c.id,c.last_seen
FROM clients c
WHERE c.last_seen < ?
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=c.id)
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=c.id OR t.winner_worker_client_id=c.id)
AND NOT EXISTS (SELECT 1 FROM identity_delegations d WHERE d.worker_client_id=c.id OR d.owner_client_id=c.id)
ORDER BY c.last_seen ASC`, cutoffMS)
if err != nil {
return nil, err
@@ -1374,7 +1440,7 @@ func (s *Store) OldWinnerCount(ctx context.Context, cutoffMS int64) (int64, erro
var n int64
err := s.DB.QueryRowContext(ctx, `SELECT count(*) FROM clients c
WHERE c.last_seen < ?
AND EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=c.id)`, cutoffMS).Scan(&n)
AND EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=c.id OR t.winner_worker_client_id=c.id)`, cutoffMS).Scan(&n)
return n, err
}
@@ -1401,7 +1467,8 @@ func (s *Store) DeleteInactiveNonWinnerClients(ctx context.Context, cutoffMS int
_, _ = tx.ExecContext(ctx, `DELETE FROM presence_leases WHERE client_id=? AND expires_at<=?`, id, time.Now().UTC().UnixMilli())
res, err := tx.ExecContext(ctx, `DELETE FROM clients
WHERE id=? AND last_seen < ?
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=clients.id)`, id, cutoffMS)
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=clients.id OR t.winner_worker_client_id=clients.id)
AND NOT EXISTS (SELECT 1 FROM identity_delegations d WHERE d.worker_client_id=clients.id OR d.owner_client_id=clients.id)`, id, cutoffMS)
if err != nil {
return nil, err
}
@@ -1516,7 +1583,7 @@ func (s *Store) LoadGuessState(ctx context.Context, taskID, cid string) (GuessSt
// sequence/count values include all losing guesses that happened in memory
// since the previous checkpoint, so a restart resumes from the latest durable
// improvement rather than writing every false guess.
func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid string, nextSeq, guessCount int64, lastGuess time.Time, score float64, guess, sig string, correct bool) (Point, error) {
func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid, rewardOwner string, nextSeq, guessCount int64, lastGuess time.Time, score float64, guess, sig string, correct bool, beaconPath, beaconBoostedPath string, beaconRound uint64) (Point, error) {
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return Point{}, err
@@ -1543,7 +1610,10 @@ func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid string
return Point{}, err
}
if correct {
res, err := tx.ExecContext(ctx, `UPDATE tasks SET status='completed',completed_at=?,winner_client_id=?,winner_signature=?,winning_guess=?,artifact_status='pending',revision=revision+1 WHERE id=? AND status='active'`, lastMS, cid, sig, guess, t.ID)
if rewardOwner == "" {
rewardOwner = cid
}
res, err := tx.ExecContext(ctx, `UPDATE tasks SET status='completed',completed_at=?,winner_client_id=?,winner_worker_client_id=?,winner_beacon_path=?,winner_beacon_boosted_path=?,winner_beacon_round=?,winner_signature=?,winning_guess=?,artifact_status='pending',revision=revision+1 WHERE id=? AND status='active'`, lastMS, rewardOwner, cid, beaconPath, beaconBoostedPath, int64(beaconRound), sig, guess, t.ID)
if err != nil {
return Point{}, err
}
@@ -1551,7 +1621,7 @@ func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid string
if n != 1 {
return Point{}, ErrTaskCompleted
}
if err := s.unlocksTx(ctx, tx, cid, t.ID, lastMS); err != nil {
if err := s.unlocksTx(ctx, tx, rewardOwner, t.ID, lastMS); err != nil {
return Point{}, err
}
}
@@ -1608,3 +1678,109 @@ func (s *Store) PointsForClient(ctx context.Context, taskID, cid string, limit i
}
return append(ps, own), nil
}
// RecordBeaconDraw stores the externally auditable randomness used by an
// optional Beacon Hunt lottery window. Duplicate callbacks are idempotent.
func (s *Store) RecordBeaconDraw(ctx context.Context, taskID string, windowEnd time.Time, beaconID string, round uint64, randomness, signature, boostedPath string, tickets, selected int) error {
_, err := s.DB.ExecContext(ctx, `INSERT INTO beacon_draws(task_id,window_end,beacon_id,beacon_round,randomness,signature,boosted_path,ticket_count,selected_count,created_at)
VALUES(?,?,?,?,?,?,?,?,?,?) ON CONFLICT(task_id,window_end) DO UPDATE SET beacon_id=excluded.beacon_id,beacon_round=excluded.beacon_round,randomness=excluded.randomness,signature=excluded.signature,boosted_path=excluded.boosted_path,ticket_count=excluded.ticket_count,selected_count=excluded.selected_count`,
taskID, windowEnd.UTC().UnixMilli(), beaconID, int64(round), randomness, signature, boostedPath, tickets, selected, time.Now().UTC().UnixMilli())
return err
}
type BeaconDraw struct {
TaskID string `json:"task_id"`
WindowEnd time.Time `json:"window_end"`
BeaconID string `json:"beacon_id"`
BeaconRound uint64 `json:"beacon_round"`
Randomness string `json:"randomness"`
Signature string `json:"signature"`
BoostedPath string `json:"boosted_path"`
TicketCount int `json:"ticket_count"`
SelectedCount int `json:"selected_count"`
}
func (s *Store) LatestBeaconDraw(ctx context.Context, taskID string) (BeaconDraw, error) {
var d BeaconDraw
var endMS int64
var round int64
err := s.DB.QueryRowContext(ctx, `SELECT task_id,window_end,beacon_id,beacon_round,randomness,signature,boosted_path,ticket_count,selected_count FROM beacon_draws WHERE task_id=? ORDER BY window_end DESC LIMIT 1`, taskID).
Scan(&d.TaskID, &endMS, &d.BeaconID, &round, &d.Randomness, &d.Signature, &d.BoostedPath, &d.TicketCount, &d.SelectedCount)
if err != nil {
return d, err
}
d.WindowEnd = time.UnixMilli(endMS).UTC()
d.BeaconRound = uint64(round)
return d, nil
}
func (s *Store) SetIdentityDelegation(ctx context.Context, workerClientID, ownerClientID string) error {
workerClientID = strings.TrimSpace(workerClientID)
ownerClientID = strings.TrimSpace(ownerClientID)
if workerClientID == "" {
return errors.New("worker_client_id required")
}
if ownerClientID == "" {
_, err := s.DB.ExecContext(ctx, `DELETE FROM identity_delegations WHERE worker_client_id=?`, workerClientID)
return err
}
if workerClientID == ownerClientID {
_, err := s.DB.ExecContext(ctx, `DELETE FROM identity_delegations WHERE worker_client_id=?`, workerClientID)
return err
}
if !s.ClientExists(ctx, workerClientID) || !s.ClientExists(ctx, ownerClientID) {
return errors.New("worker and owner identities must already exist")
}
now := time.Now().UTC().UnixMilli()
_, err := s.DB.ExecContext(ctx, `INSERT INTO identity_delegations(worker_client_id,owner_client_id,created_at,updated_at) VALUES(?,?,?,?)
ON CONFLICT(worker_client_id) DO UPDATE SET owner_client_id=excluded.owner_client_id,updated_at=excluded.updated_at`, workerClientID, ownerClientID, now, now)
return err
}
func (s *Store) RewardOwnerForWorker(ctx context.Context, workerClientID string) string {
var owner string
if err := s.DB.QueryRowContext(ctx, `SELECT owner_client_id FROM identity_delegations WHERE worker_client_id=?`, workerClientID).Scan(&owner); err == nil && owner != "" {
return owner
}
return workerClientID
}
// CreateCustomerLinkToken stores only a SHA-256 hash of the short-lived pairing
// code. A customer can therefore prove control of a Neural Hunt identity to the
// private hosted-service control plane without ever uploading its private key.
func (s *Store) CreateCustomerLinkToken(ctx context.Context, tokenHash, clientID string, expiresAt time.Time) error {
now := time.Now().UTC().UnixMilli()
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, _ = tx.ExecContext(ctx, `DELETE FROM customer_link_tokens WHERE expires_at<=? OR client_id=?`, now, clientID)
if _, err := tx.ExecContext(ctx, `INSERT INTO customer_link_tokens(token_hash,client_id,expires_at,created_at) VALUES(?,?,?,?)`, tokenHash, clientID, expiresAt.UTC().UnixMilli(), now); err != nil {
return err
}
return tx.Commit()
}
// ConsumeCustomerLinkToken is intentionally one-shot. The delete happens in
// the same transaction as the lookup so the code cannot be replayed by a
// second Customer Service request.
func (s *Store) ConsumeCustomerLinkToken(ctx context.Context, tokenHash string) (string, error) {
now := time.Now().UTC().UnixMilli()
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return "", err
}
defer tx.Rollback()
var clientID string
if err := tx.QueryRowContext(ctx, `SELECT client_id FROM customer_link_tokens WHERE token_hash=? AND expires_at>?`, tokenHash, now).Scan(&clientID); err != nil {
return "", err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM customer_link_tokens WHERE token_hash=?`, tokenHash); err != nil {
return "", err
}
if err := tx.Commit(); err != nil {
return "", err
}
return clientID, nil
}