Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions agent-os/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions agent-os/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ members = [
"crates/covenant-budget",
"crates/covenant-x402",
"crates/covenant-hyre",
"crates/covenant-wurk",
"crates/covenant-zauth",
"crates/covenant-metaplex",
"crates/covenant-sap-bridge",
Expand Down
23 changes: 23 additions & 0 deletions agent-os/crates/covenant-wurk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "covenant-wurk"
version = "0.0.0"
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "WURK agent-to-human x402 provider profile for Covenant — paid microjob creation, free submission view, and creator-mode winner selection. Agent-to-human family only."

[dependencies]
covenant-x402 = { path = "../covenant-x402" }
covenant-mcp = { path = "../covenant-mcp" }
async-trait = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
reqwest = { workspace = true }
base64 = "0.22"

[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
wiremock = "0.6"
124 changes: 124 additions & 0 deletions agent-os/crates/covenant-wurk/examples/live_create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! Live WURK agent-to-human create, end to end through covenant-wurk.
//!
//! Drives the real `execute_paid` 402-then-pay loop against WURK's
//! production endpoint, signing with the funding key via the
//! covenant-x402-signer sidecar (so no Solana deps enter this crate).
//!
//! Usage:
//! WURK_SIGNER_BIN=.../target/release/covenant-x402-signer \
//! WURK_KEYPAIR=~/.config/solana/solana-id.json \
//! WURK_RPC=https://api.mainnet-beta.solana.com \
//! cargo run -p covenant-wurk --example live_create -- "<description>" <winners> <perUser>

use std::io::Write;
use std::process::{Command, Stdio};

use async_trait::async_trait;
use covenant_wurk::{config, execute_paid, PaidRequest};
use covenant_x402::{PaymentRequirements, Signer};

/// Signs by piping the requirements to the standalone signer sidecar.
struct SubprocessSigner {
bin: String,
keypair: String,
rpc: String,
}

#[async_trait]
impl Signer for SubprocessSigner {
async fn build_payment(
&self,
requirements: &PaymentRequirements,
) -> covenant_x402::Result<String> {
let json = serde_json::to_string(requirements)
.map_err(|e| covenant_x402::X402Error::Sign(e.to_string()))?;
let mut child = Command::new(&self.bin)
.env("COVENANT_X402_FUNDING_KEYPAIR", &self.keypair)
.env("COVENANT_X402_RPC_URL", &self.rpc)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| covenant_x402::X402Error::Sign(format!("spawn signer: {e}")))?;
{
let mut stdin = child.stdin.take().expect("piped stdin");
stdin
.write_all(json.as_bytes())
.map_err(|e| covenant_x402::X402Error::Sign(format!("write signer stdin: {e}")))?;
}
let out = child
.wait_with_output()
.map_err(|e| covenant_x402::X402Error::Sign(format!("wait signer: {e}")))?;
if !out.status.success() {
return Err(covenant_x402::X402Error::Sign(format!(
"signer exited {:?}",
out.status.code()
)));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
}

fn encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}

#[tokio::main]
async fn main() {
let bin = std::env::var("WURK_SIGNER_BIN").expect("set WURK_SIGNER_BIN");
let keypair = std::env::var("WURK_KEYPAIR").expect("set WURK_KEYPAIR");
let rpc =
std::env::var("WURK_RPC").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into());

let args: Vec<String> = std::env::args().collect();
let description = args
.get(1)
.cloned()
.unwrap_or_else(|| "Covenant x402 connectivity test. Please reply: ok".into());
let winners: u64 = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
let per_user: f64 = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0.01);
let cap = (winners as f64 * per_user * 1_000_000.0).round() as u128;

let url = format!(
"{}/solana/agenttohuman?description={}&winners={}&perUser={}",
config::BASE_URL,
encode(&description),
winners,
per_user
);
eprintln!("create: {url}\ncap atomic: {cap}");

let plan = PaidRequest {
provider: "wurk".into(),
slug: "agenttohuman".into(),
url,
method: "GET".into(),
body: None,
network: config::SOLANA_NETWORK.into(),
asset: config::USDC_MINT.into(),
per_call_cap: cap,
};

let signer = SubprocessSigner { bin, keypair, rpc };
let http = reqwest::Client::new();
match execute_paid(&http, &signer, &plan).await {
Ok(r) => {
println!("status: {}", r.status);
println!("paid_amount: {:?}", r.paid_amount);
println!("body: {}", r.body);
}
Err(e) => {
eprintln!("ERROR: {e}");
std::process::exit(1);
}
}
}
101 changes: 101 additions & 0 deletions agent-os/crates/covenant-wurk/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! WURK provider configuration. Agent-to-human family only.
//!
//! Constants are pinned from WURK's live 402 challenge and agent-card so
//! a manipulated challenge can't steer the funding key to another payee
//! or sponsor (enforced in [`crate::x402::to_requirements`]).

use serde::{Deserialize, Serialize};

/// CAIP-2 id for WURK's Solana settlement (mainnet), from the live 402.
pub const SOLANA_NETWORK: &str = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
/// USDC mint payments are denominated in.
pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
/// WURK's Solana payee (holds job funds until payout), from the live 402.
pub const PAY_TO: &str = "SAT8g2xU7AFy7eUmNJ9SNrM6yYo7LDCi13GXJ8Ez9kC";
/// PayAI sponsor pubkey co-signing as `feePayer` (same facilitator Hyre uses).
pub const FEE_PAYER: &str = "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4";
/// API host.
pub const BASE_URL: &str = "https://wurkapi.fun";

/// The ONLY endpoint families this crate may touch. The engagement,
/// raid, and vote endpoints are deliberately excluded — covenant-wurk
/// cannot construct a request against them.
pub const ALLOWED_PATHS: &[&str] = &["agenttohuman", "agenttohumanadvanced"];

/// Whether `path_segment` is in the agent-to-human allowlist.
pub fn is_allowed(path_segment: &str) -> bool {
ALLOWED_PATHS.contains(&path_segment)
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WurkConfig {
/// Master switch. `false` means no WURK tools are registered.
#[serde(default)]
pub enabled: bool,
/// API host. Endpoint URLs are built against this.
#[serde(default = "default_base_url")]
pub base_url: String,
/// CAIP-2 settlement network.
#[serde(default = "default_network")]
pub network: String,
/// Payment asset (USDC mint).
#[serde(default = "default_asset")]
pub asset: String,
/// Per-call ceiling in atomic USDC. A 402 amount above this is
/// rejected before the signer runs. `0` defers to the caller-supplied
/// cap (winners * perUser).
#[serde(default)]
pub per_call_cap: u128,
}

fn default_base_url() -> String {
BASE_URL.to_string()
}
fn default_network() -> String {
SOLANA_NETWORK.to_string()
}
fn default_asset() -> String {
USDC_MINT.to_string()
}

impl Default for WurkConfig {
fn default() -> Self {
Self {
enabled: false,
base_url: default_base_url(),
network: default_network(),
asset: default_asset(),
per_call_cap: 0,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_is_disabled_with_solana_rail() {
let c = WurkConfig::default();
assert!(!c.enabled);
assert_eq!(c.network, SOLANA_NETWORK);
assert_eq!(c.asset, USDC_MINT);
}

#[test]
fn only_agent_to_human_paths_are_allowed() {
assert!(is_allowed("agenttohuman"));
assert!(is_allowed("agenttohumanadvanced"));
for blocked in [
"xraid",
"xlikes",
"xfollowers",
"dex",
"cmcvote",
"cgvote",
"tgmembers",
] {
assert!(!is_allowed(blocked), "{blocked} must be blocked");
}
}
}
41 changes: 41 additions & 0 deletions agent-os/crates/covenant-wurk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! WURK agent-to-human provider profile for Covenant.
//!
//! WURK (wurk.fun) is an x402 marketplace. This crate wires ONLY its
//! agent-to-human microjob family: create a human-review job, read the
//! submissions, and pick winners. The social-engagement/vote endpoints
//! are never reachable through here (see [`config::ALLOWED_PATHS`]).
//!
//! Payment reuses covenant-x402's [`covenant_x402::Signer`] (the
//! funding-key sidecar) and [`covenant_x402::PaymentRequirements`].
//! WURK's 402 is the object/`accepts` shape with `amount` +
//! `extra.feePayer`, but the payment header is **`PAYMENT-SIGNATURE`**
//! (not x402's `x-payment`), so this crate runs its own 402-then-pay
//! loop in [`x402`]. View and winner-selection are free (secret-authed)
//! and need no signer.

pub mod config;
pub mod tools;
pub mod view;
pub mod x402;

pub use config::WurkConfig;
pub use tools::{wurk_specs, wurk_tool, wurk_tools, PaidRequest, PaidResponse, WurkExecutor};
pub use view::{parse_view, JobView, Submission};
pub use x402::{execute_paid, parse_challenge, PaidHttp};

/// Errors raised across the WURK profile.
#[derive(Debug, thiserror::Error)]
pub enum WurkError {
#[error("challenge: {0}")]
Challenge(String),
#[error("not allowed: {0}")]
NotAllowed(String),
#[error("execute: {0}")]
Execute(String),
#[error("view: {0}")]
View(String),
#[error(transparent)]
Http(#[from] reqwest::Error),
}

pub type Result<T> = std::result::Result<T, WurkError>;
Loading
Loading