Skip to content

feat: implement task submit service#6

Merged
TeoSlayer merged 6 commits into
mainfrom
alex
Feb 18, 2026
Merged

feat: implement task submit service#6
TeoSlayer merged 6 commits into
mainfrom
alex

Conversation

@Alexgodoroja

@Alexgodoroja Alexgodoroja commented Feb 18, 2026

Copy link
Copy Markdown
Collaborator

PR: Task Submit Service with Weighted Polo Score Calculation

Overview

This PR implements the Task Submit Service (port 1003), a core feature that enables AI agents to request work from other agents on the Pilot Protocol network. The service includes a sophisticated polo score reputation system that rewards agents for completing compute-intensive tasks promptly.

Features

1. Task Submit Service (Port 1003)

  • Task submission, acceptance, decline, execution, and result delivery
  • FIFO task queue with automatic monitoring
  • Time metadata tracking for polo score calculation
  • File and text result support with extension validation

2. Weighted Polo Score Formula

  • Logarithmic CPU bonus for compute time
  • Efficiency multiplier for responsiveness
  • Automatic timeout/expiry handling with score penalties

3. Task Lifecycle Management

  • 1-minute accept timeout with auto-cancellation
  • 1-hour queue head expiry with polo score penalty
  • Time tracking: idle, staged, and CPU time

Architecture Flow

┌─────────────────────────────────────────────────────────────────────────────────┐
│                          TASK SUBMIT SERVICE ARCHITECTURE                        │
└─────────────────────────────────────────────────────────────────────────────────┘

┌──────────────┐                                              ┌──────────────┐
│   AGENT A    │                                              │   AGENT B    │
│  (Submitter) │                                              │  (Receiver)  │
└──────┬───────┘                                              └──────┬───────┘
       │                                                             │
       │  1. Submit Task                                             │
       │  ─────────────────────────────────────────────────────────► │
       │  TaskDescription, TaskID                                    │
       │                                                             │
       │                           ┌─────────────────────────────────┤
       │                           │ 2. Polo Score Check             │
       │                           │    (submitter >= receiver)      │
       │                           │                                 │
       │                           │ 3. Save to ~/.pilot/tasks/      │
       │                           │    received/<task_id>.json      │
       │                           │                                 │
       │                           │ 4. Add to Task Queue (FIFO)     │
       │  ◄─────────────────────────────────────────────────────────┤
       │  Response: ACCEPTED/REJECTED                                │
       │                                                             │
       │                                                             │
       │                           ┌─────────────────────────────────┤
       │                           │ 5. Agent decides:               │
       │                           │    - Accept (within 1 min)      │
       │                           │    - Decline (with reason)      │
       │                           │                                 │
       │  6. Status Update         │ 7. Calculate time_idle          │
       │  ◄─────────────────────────────────────────────────────────┤
       │  ACCEPTED/DECLINED                                          │
       │                                                             │
       │                           ┌─────────────────────────────────┤
       │                           │ 8. Task reaches queue head      │
       │                           │    (staged_at timestamp set)    │
       │                           │                                 │
       │                           │ 9. pilotctl task execute        │
       │                           │    Calculate time_staged        │
       │                           │                                 │
       │                           │ 10. Agent performs work...      │
       │                           │     (CPU time tracked)          │
       │                           │                                 │
       │                           │ 11. pilotctl task send-results  │
       │  ◄─────────────────────────────────────────────────────────┤
       │  Results + Time Metadata                                    │
       │                                                             │
┌──────┴───────┐                                              ┌──────┴───────┐
│  Submitter   │                                              │   Receiver   │
│  Polo: -1    │                                              │  Polo: +N    │
└──────────────┘                                              └──────────────┘
       │                                                             │
       │                    ┌─────────────────────┐                  │
       │                    │      REGISTRY       │                  │
       │                    │  ┌───────────────┐  │                  │
       │  UpdatePoloScore   │  │  Polo Scores  │  │  UpdatePoloScore │
       └────────────────────►  │  node → score │  ◄──────────────────┘
                            │  │   1 → 5       │  │
                            │  │   2 → 12      │  │
                            │  │   3 → 8       │  │
                            │  └───────────────┘  │
                            └─────────────────────┘

Polo Score System

Storage

Polo scores are stored in the Registry Server as a map of node_id → score:

// In pkg/registry/server.go
type Server struct {
    // ...
    poloScores map[uint32]int  // node_id → polo score
    poloMu     sync.RWMutex
}

Registry API

Operation Description
GetPoloScore(nodeID) Returns current polo score for a node (default: 0)
SetPoloScore(nodeID, score) Sets absolute polo score
UpdatePoloScore(nodeID, delta) Atomic increment/decrement

Polo Score Delta Formula

When a task is completed successfully, the receiver's polo score increases by:

reward = (1 + cpuBonus) × efficiency

Components

Component Formula Description
Base 1 Guaranteed minimum for completing any task
cpuBonus log₂(1 + cpu_minutes) Logarithmic scaling of compute time
efficiency 1.0 - idleFactor - stagedFactor Penalty multiplier for delays
idleFactor min(idle_seconds / 60, 0.3) Up to 30% penalty for slow accept
stagedFactor min(staged_minutes / 10, 0.3) Up to 30% penalty for queue delays

CPU Bonus Examples

CPU Time log₂(1 + minutes) Total Reward (100% efficiency)
0 min 0 1
1 min 1.0 2
3 min 2.0 3
7 min 3.0 4
15 min 4.0 5
31 min 5.0 6
63 min 6.0 7

Efficiency Examples

Scenario Idle Staged Efficiency Impact
Perfect 0s 0s 100% Full bonus
Slow accept 30s 0s 85% -15%
Slow execute 0s 5min 85% -15%
Both slow 30s 5min 70% -30%
Max penalty 60s+ 10min+ 40% -60%

Score Changes Summary

Event Submitter Receiver
Task completed -1 +1 to +7 (weighted)
Task expired (1hr queue) 0 -1
Task cancelled/declined 0 0

Task Lifecycle State Machine

                                    ┌─────────────────┐
                                    │                 │
                              ┌─────►   CANCELLED     │
                              │     │  (auto 1 min)   │
                              │     └─────────────────┘
                              │
┌─────────┐   submit    ┌─────┴─────┐   accept    ┌───────────┐
│         │ ──────────► │           │ ──────────► │           │
│  (new)  │             │    NEW    │             │  ACCEPTED │
│         │             │           │ ◄────┐      │           │
└─────────┘             └─────┬─────┘      │      └─────┬─────┘
                              │            │            │
                              │ decline    │            │ execute
                              ▼            │            ▼
                        ┌───────────┐      │      ┌───────────┐
                        │           │      │      │           │
                        │ DECLINED  │      │      │ EXECUTING │
                        │           │      │      │           │
                        └───────────┘      │      └─────┬─────┘
                                           │            │
                                           │            │ send-results
                                           │            ▼
                              ┌────────────┴──────┐ ┌───────────┐
                              │                   │ │           │
                              │     EXPIRED       │ │ SUCCEEDED │
                              │  (auto 1 hour)    │ │  (+polo)  │
                              │     (-1 polo)     │ │           │
                              └───────────────────┘ └───────────┘

Time Metadata Tracking

Timeline:
─────────────────────────────────────────────────────────────────────►

    Task Created          Accepted           Execute Started        Completed
         │                   │                      │                   │
         ▼                   ▼                      ▼                   ▼
    ┌─────────┐         ┌─────────┐            ┌─────────┐         ┌─────────┐
    │ NEW     │         │ACCEPTED │            │EXECUTING│         │SUCCEEDED│
    │         │         │         │            │         │         │         │
    └─────────┘         └─────────┘            └─────────┘         └─────────┘
         │                   │                      │                   │
         │◄─── time_idle ───►│                      │                   │
         │   (creation to    │◄── time_staged ─────►│                   │
         │    accept)        │   (queue head to     │◄── time_cpu ─────►│
         │                   │    execute start)    │   (execute to     │
         │                   │                      │    complete)      │

TaskFile JSON Structure

{
  "task_id": "abc123-def456-...",
  "task_description": "Summarize this document...",
  "created_at": "2026-02-18T03:30:00Z",
  "status": "SUCCEEDED",
  "status_justification": "Results sent successfully",
  "from": "0:0000.0000.0001",
  "to": "0:0000.0000.0002",
  
  "accepted_at": "2026-02-18T03:30:05Z",
  "staged_at": "2026-02-18T03:30:05Z",
  "execute_started_at": "2026-02-18T03:30:10Z",
  "completed_at": "2026-02-18T03:35:10Z",
  
  "time_idle_ms": 5000,
  "time_staged_ms": 5000,
  "time_cpu_ms": 300000
}

File Structure

Task Directories

~/.pilot/
├── tasks/
│   ├── received/           # Tasks sent to you
│   │   └── <task_id>.json
│   ├── submitted/          # Tasks you sent
│   │   └── <task_id>.json
│   └── results/            # Results from completed tasks
│       ├── <task_id>_result.txt
│       └── <task_id>_report.pdf

Key Files Modified/Added

pkg/
├── tasksubmit/
│   ├── tasksubmit.go      # Core types, polo score formula, frame protocol
│   ├── client.go          # Client for submitting tasks
│   └── server.go          # Server for receiving tasks
├── daemon/
│   └── services.go        # Task queue, monitoring, handlers
├── registry/
│   ├── server.go          # Polo score storage
│   └── client.go          # Polo score API
cmd/
└── pilotctl/
    └── main.go            # Task CLI commands
docs/
├── POLO_SCORE.md          # Detailed formula documentation
└── SKILLS.md              # Agent skill documentation
tests/
└── tasksubmit_test.go     # Unit and integration tests

CLI Commands

# Submit a task
pilotctl task submit <target> --task "Description"

# List received tasks
pilotctl task list --type received

# Accept/decline a task (within 1 minute)
pilotctl task accept --id <task_id>
pilotctl task decline --id <task_id> --justification "Reason"

# View task queue
pilotctl task queue

# Execute next task in queue
pilotctl task execute

# Send results
pilotctl task send-results --id <task_id> --results "Text results"
pilotctl task send-results --id <task_id> --file report.pdf

Test Coverage

Overall Codebase Coverage

Total: 60.8%

Task Submit Package Coverage

Package: pkg/tasksubmit
Average: 57.9%

Key Functions:
- PoloScoreReward:         100.0%
- PoloScoreRewardDetailed:  91.3%
- MarshalTaskFile:         100.0%
- GenerateTaskID:          100.0%
- NewTaskFile:             100.0%
- Dial:                     87.5%
- SubmitTask:               72.7%
- WriteFrame:               85.7%
- ReadFrame:                72.7%

Test Files

  • tests/tasksubmit_test.go - 1385 lines of comprehensive tests
  • tests/polo_score_test.go - Registry polo score tests

Test Categories

Category Tests Status
Basic Task Submit 5 ✅ Pass
Polo Score Validation 3 ✅ Pass
Task Queue Operations 4 ✅ Pass
Concurrent Submission 1 ✅ Pass
Frame Protocol 3 ✅ Pass
Time Metadata 5 ✅ Pass
Polo Score Reward 8 ✅ Pass
Task Lifecycle 4 ✅ Pass

Security Considerations

Task Submission Requirements

  1. Mutual Trust - Both agents must have established trust via handshake
  2. Polo Score Gate - Submitter's polo score must be ≥ receiver's polo score
  3. Result Validation - Source code files are forbidden in results

Forbidden Result Extensions

.go, .py, .js, .ts, .java, .c, .cpp, .rs, .rb, .php, .swift, .sh, .bash, ...

Allowed Result Extensions

.md, .txt, .pdf, .csv, .jpg, .png, .pth, .onnx, .safetensors, ...

Monitoring & Timeouts

Background Monitoring Goroutines

Monitor Interval Action
Accept Timeout 10s Cancels NEW tasks older than 1 minute
Queue Expiry 30s Expires queue head tasks older than 1 hour

Timeout Configuration

const (
    TaskAcceptTimeout    = 1 * time.Minute  // Max time to accept/decline
    TaskQueueHeadTimeout = 1 * time.Hour    // Max time at queue head
)

Breaking Changes

None. This is a new feature addition.


Migration Guide

No migration required. New installations will have the Task Submit service enabled by default on port 1003.


Future Improvements

  1. Configurable tasks directory - Allow per-daemon task isolation for better testing
  2. Task priority levels - Allow high-priority tasks to skip queue
  3. Task categories/tags - Allow agents to filter tasks by capability
  4. Batch task submission - Submit multiple related tasks atomically
  5. Task dependencies - Chain tasks with prerequisite relationships

Related Documentation

@TeoSlayer TeoSlayer self-requested a review February 18, 2026 01:49
@TeoSlayer TeoSlayer self-assigned this Feb 18, 2026
@TeoSlayer TeoSlayer added the enhancement New feature or request label Feb 18, 2026
@Alexgodoroja Alexgodoroja marked this pull request as ready for review February 18, 2026 01:55
@Alexgodoroja Alexgodoroja self-assigned this Feb 18, 2026
@TeoSlayer TeoSlayer merged commit 301fba8 into main Feb 18, 2026
1 of 2 checks passed
@TeoSlayer TeoSlayer deleted the alex branch March 23, 2026 22:28
TeoSlayer pushed a commit that referenced this pull request Apr 30, 2026
* init: tweak irrelevant stuff

* test: add e2e testing, coverage and pre-commits

* feat: add karma implementation

* feat: implement task management

* feat: implement task submit service

---------

Co-authored-by: Alex Godoroja <alex@vulturelabs.io>
TeoSlayer pushed a commit that referenced this pull request May 3, 2026
Symptom: a low-traffic peer pair has chatted briefly (our cached
pc.maxRecvNonce sits at, say, 5). The peer restarts and resumes
sending; their fresh counter starts at 1. Their first packet is
REJECTED by our replay window (we already recorded counter=1 from
the pre-restart session, replay bit is still set), and we DO NOT
trigger a rekey because the existing "low counter, far from maxN"
recovery branch only fires when maxN-recvCounter >= replayWindowSize
(256). With maxN=5, the gap is 4, so we fall into the unconditional
"replay detected" branch and just log + return forever.

Code path (pkg/daemon/tunnel.go::handleEncrypted ~lines 977-1009):

  ok := pc.checkAndRecordNonce(recvCounter)         // false (replay)
  maxN := pc.maxRecvNonce                           // 5
  ...
  if recvCounter < maxN && maxN-recvCounter >= replayWindowSize {
      // not entered: 5-1 = 4 < 256
  } else {
      slog.Warn("tunnel nonce replay detected", ...)
      tm.webhook.Emit("security.nonce_replay", ...)
      // NO rekey trigger — peer stays silently stuck
  }

Why this matters: every short-lived test pair, every just-bootstrapped
peer, every "I sent five hello packets and went idle" session is
vulnerable. Their daemon may be perfectly healthy; we will never
rekey, and the connection stays a one-way black hole until the
connection-layer retransmit times out.

Replication (peer_restart_nonce_bug_test.go):
  - tm.Listen + EnableEncryption + install peerCrypto
  - Phase 1: deliver counter=1 then counter=5 (clean small session)
  - Phase 2: deliver counter=1 again (peer restart, fresh counter)
  - assert: tm.lastRekeyReq[peer] is NOT set (current bug)

Plus a guard (TestPeerRestartHighCounterReplayDoesNotTriggerRekey):
high-counter replay (real attack pattern, captured from active
session) must NOT trigger a rekey — that would let an attacker who
captures any single old packet force a key rotation on demand.

GREEN flips the first assertion: low-counter replay must trigger
maybeRequestRekey. The high-counter guard stays passing.
TeoSlayer pushed a commit that referenced this pull request May 3, 2026
…indow

Bug fixed: handleEncrypted's "replay detected" branch now triggers
a rate-limited rekey (maybeRequestRekey) when recvCounter < 1024,
regardless of how close it is to maxN. Previously, the rekey trigger
was gated behind `maxN-recvCounter >= replayWindowSize` (256), which
meant a peer pair with maxN < 256 could never recover from a peer
restart — every fresh-from-1 packet hit the replay window, fell
into the unconditional "replay detected" branch, and silently
returned. The peer was alive on the wire but our connection-layer
retransmits timed out long before we'd realize.

Implementation (pkg/daemon/tunnel.go::handleEncrypted, else branch):

  } else {
      slog.Warn("tunnel nonce replay detected", ...)
      tm.webhook.Emit("security.nonce_replay", ...)
      // v1.9.1 peer-restart resync: a "replay" with low counter is
      // far more likely peer-restart than real replay. Real attacks
      // reuse a HIGH counter captured from active session — attacker
      // has no way to lower their own counter without our private key.
      if recvCounter < 1024 {
          tm.maybeRequestRekey(peerNodeID, from)
      }
  }

Why this is safe against replay attacks: the rekey is rate-limited
(rekeyRequestInterval = 3s per peer, capped at maxRekeyRequesters=4096
peers). An attacker spraying captured low-counter packets can at most
trigger one rekey every 3s. The rekey itself is a self-signed
authenticated key-exchange the attacker cannot forge (Ed25519 sig).
The legitimate peer receives the rekey, installs fresh crypto, and
the connection recovers. The attacker's spray accomplishes nothing
beyond a small CPU cost we already bound with maxRekeyRequesters.

Tests:
- TestPeerRestartLowMaxNDeadlock: post-fix, one low-counter "replay"
  now sets tm.lastRekeyReq[peer] within the call.
- TestPeerRestartHighCounterReplayDoesNotTriggerRekey: high-counter
  replay (counter=4900, maxN=5000) still does NOT trigger rekey —
  the heuristic correctly distinguishes restart from real attack.

Race-clean across full pkg/daemon (62s).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants