RC-7
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
+162 -15
View File
@@ -190,18 +190,23 @@ type taskCard struct {
}
type taskDTO struct {
ID string `json:"id"`
PublicSeed string `json:"public_seed"`
RangeBits int `json:"range_bits"`
NextSeq int64 `json:"next_seq"`
ServerMinIntervalSec int `json:"server_min_interval_sec"`
ClientSubmitIntervalSec int `json:"client_submit_interval_sec"`
DefaultMaxNodes int `json:"default_max_nodes"`
Paused bool `json:"paused"`
Revision int64 `json:"revision"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
ParentTaskID *string `json:"parent_task_id"`
ID string `json:"id"`
PublicSeed string `json:"public_seed"`
RangeBits int `json:"range_bits"`
NextSeq int64 `json:"next_seq"`
ServerMinIntervalSec int `json:"server_min_interval_sec"`
ClientSubmitIntervalSec int `json:"client_submit_interval_sec"`
DefaultMaxNodes int `json:"default_max_nodes"`
GuessLotteryWindowSec int `json:"guess_lottery_window_sec"`
GuessLotteryMaxAccepted int `json:"guess_lottery_max_accepted"`
BeaconHuntEnabled int `json:"beacon_hunt_enabled"`
BeaconBonusWeight int `json:"beacon_bonus_weight"`
BeaconPaths []string `json:"beacon_paths"`
Paused bool `json:"paused"`
Revision int64 `json:"revision"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
ParentTaskID *string `json:"parent_task_id"`
}
type point struct {
@@ -243,6 +248,15 @@ type publicArtifact struct {
PreviewURI string `json:"preview_uri"`
}
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 (c *apiClient) tasks(ctx context.Context) ([]taskCard, error) {
var out []taskCard
err := c.do(ctx, http.MethodGet, "/api/tasks", nil, &out)
@@ -273,6 +287,18 @@ func (c *apiClient) me(ctx context.Context) (meDTO, error) {
return out, err
}
type hostedLinkCode struct {
Code string `json:"code"`
ClientID string `json:"client_id"`
ExpiresAt time.Time `json:"expires_at"`
}
func (c *apiClient) hostedLinkCode(ctx context.Context) (hostedLinkCode, error) {
var out hostedLinkCode
err := c.do(ctx, http.MethodPost, "/api/me/customer-link", map[string]any{}, &out)
return out, err
}
func (c *apiClient) leaderboard(ctx context.Context) ([]leader, error) {
var out []leader
err := c.do(ctx, http.MethodGet, "/api/leaderboard", nil, &out)
@@ -285,14 +311,25 @@ func (c *apiClient) artifacts(ctx context.Context, limit int) ([]publicArtifact,
return out, err
}
func (c *apiClient) guess(ctx context.Context, t taskDTO, seq int64) (bool, error) {
func (c *apiClient) ownedArtifacts(ctx context.Context, limit int) ([]ownedArtifact, error) {
var out []ownedArtifact
err := c.do(ctx, http.MethodGet, "/api/me/artifacts?limit="+strconv.Itoa(limit), nil, &out)
return out, err
}
func (c *apiClient) guess(ctx context.Context, t taskDTO, seq int64, beaconPath string) (bool, error) {
guess := expectedGuess(t.ID, t.PublicSeed, c.cid, seq, t.RangeBits)
sig, err := signRaw(c.key, fmt.Sprintf("guess|%s|%d|%s", t.ID, seq, guess))
msg := fmt.Sprintf("guess|%s|%d|%s", t.ID, seq, guess)
if t.BeaconHuntEnabled == 1 && t.GuessLotteryMaxAccepted > 0 {
beaconPath = strings.ToUpper(strings.TrimSpace(beaconPath))
msg += "|" + beaconPath
}
sig, err := signRaw(c.key, msg)
if err != nil {
return false, err
}
var correct bool
err = c.do(ctx, http.MethodPost, "/api/tasks/"+url.PathEscape(t.ID)+"/guess", map[string]any{"seq": seq, "guess": guess, "signature": sig}, &correct)
err = c.do(ctx, http.MethodPost, "/api/tasks/"+url.PathEscape(t.ID)+"/guess", map[string]any{"seq": seq, "guess": guess, "signature": sig, "beacon_path": beaconPath}, &correct)
return correct, err
}
@@ -348,6 +385,38 @@ func (c *apiClient) downloadPreview(ctx context.Context, taskID, dest string) er
return err
}
func (c *apiClient) downloadOwnedArtifact(ctx context.Context, taskID, dest string) error {
path := "/api/me/artifacts/" + url.PathEscape(taskID) + "/download"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.base+path, nil)
if err != nil {
return err
}
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return &apiError{Status: resp.StatusCode, Body: string(b)}
}
if dir := filepath.Dir(dest); dir != "." {
if err := os.MkdirAll(dir, 0o750); err != nil {
return err
}
}
f, err := os.Create(dest)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, io.LimitReader(resp.Body, 64<<20))
return err
}
func expectedGuess(taskID, seed, clientID string, seq int64, bits int) string {
// Keep this exactly aligned with internal/core.ExpectedGuess without importing
// implementation details into the terminal UX layer.
@@ -355,3 +424,81 @@ func expectedGuess(taskID, seed, clientID string, seq int64, bits int) string {
}
var errSwitching = errors.New("task switch in progress")
// registerHostedWorker binds this worker's freshly authenticated cryptographic
// identity to its Customer Service worker record. The endpoint is reachable
// only on the private Docker network and additionally requires the per-worker
// one-time bearer token injected by the Customer Service.
func (c *apiClient) registerHostedWorker(ctx context.Context) error {
registerURL := strings.TrimSpace(os.Getenv("NEURALHUNT_WORKER_REGISTER_URL"))
workerID := strings.TrimSpace(os.Getenv("NEURALHUNT_WORKER_ID"))
token := strings.TrimSpace(os.Getenv("NEURALHUNT_WORKER_REGISTER_TOKEN"))
if registerURL == "" && workerID == "" && token == "" {
return nil
}
if registerURL == "" || workerID == "" || token == "" {
return errors.New("hosted worker registration requires NEURALHUNT_WORKER_REGISTER_URL, NEURALHUNT_WORKER_ID and NEURALHUNT_WORKER_REGISTER_TOKEN")
}
body, _ := json.Marshal(map[string]string{"worker_id": workerID, "client_id": c.cid})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, registerURL, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
hc := &http.Client{Timeout: 10 * time.Second}
resp, err := hc.Do(req)
if err != nil {
return fmt.Errorf("hosted worker register: %w", err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
if resp.StatusCode/100 != 2 {
return fmt.Errorf("hosted worker register HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
}
return nil
}
// hostedWorkerLeaseLoop is a fail-closed control-plane lease. Billing remains
// authoritative in Customer Service; this loop merely prevents a managed
// worker from running indefinitely if the Customer Service/Docker controller
// disappears. After three consecutive missed 20-second renewals it cancels the
// client context and the managed container exits.
func (c *apiClient) hostedWorkerLeaseLoop(ctx context.Context, cancel context.CancelFunc) {
leaseURL := strings.TrimSpace(os.Getenv("NEURALHUNT_WORKER_LEASE_URL"))
workerID := strings.TrimSpace(os.Getenv("NEURALHUNT_WORKER_ID"))
token := strings.TrimSpace(os.Getenv("NEURALHUNT_WORKER_REGISTER_TOKEN"))
if leaseURL == "" || workerID == "" || token == "" {
return
}
t := time.NewTicker(20 * time.Second)
defer t.Stop()
failures := 0
for {
select {
case <-ctx.Done():
return
case <-t.C:
body, _ := json.Marshal(map[string]string{"worker_id": workerID})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, leaseURL, bytes.NewReader(body))
if err == nil {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, callErr := (&http.Client{Timeout: 8 * time.Second}).Do(req)
if callErr == nil {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
_ = resp.Body.Close()
if resp.StatusCode/100 == 2 {
failures = 0
continue
}
}
}
failures++
if failures >= 3 {
cancel()
return
}
}
}
}