Skip to content
Merged
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
117 changes: 15 additions & 102 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,110 +1,23 @@
# Security Policy

ai-net coordinates AI agents that hold and move value on the Stellar
network — smart contracts manage escrow, bonds, and payments, and the
backend holds Stellar keypairs and API credentials. We take security
reports seriously and ask that you report vulnerabilities privately so we
can ship a fix before details become public.
## Responsible Disclosure

## Reporting a Vulnerability
Please report suspected vulnerabilities privately instead of opening a public issue with exploit details.

**Do not open a public GitHub issue for a security vulnerability.** Public
issues are indexed and searchable immediately, which gives an attacker a
head start before a fix ships.
- Email: security@example.com
- Include affected component, impact, reproduction steps, and any relevant logs or transaction ids.
- Do not access data that is not yours, modify production state, or interrupt network availability while validating a report.
- We acknowledge reports within 3 business days and provide a remediation status update within 10 business days.

Instead, report privately using the following:
## Severity Targets

**GitHub Private Vulnerability Reporting**: open the
[Security tab](../../security/advisories/new) on this repository and
submit a new draft security advisory. This is the project's primary
private-disclosure channel — it keeps the report, discussion, and eventual
publication in one place, notifies maintainers directly, and requires no
extra setup on your end (no key exchange, no separate account).
| Severity | Examples | Target |
| --- | --- | --- |
| Critical | Fund loss, private key exposure, remote code execution | Patch or mitigation within 48 hours |
| High | Auth bypass, task tampering, payment reconciliation bypass | Patch within 7 days |
| Medium | Privilege confusion, sensitive metadata leakage | Patch within 30 days |
| Low | Hardening gaps, low-impact information disclosure | Next regular release |

> A dedicated security-contact email address is not yet published for this
> project. If one is set up in the future, it will be listed here alongside
> a PGP key for encrypting sensitive report contents. Until then, GitHub
> Private Vulnerability Reporting is the channel to use.
## Coordinated Release

### What to include

- A clear description of the vulnerability and its impact (what an
attacker could do, and to whom — a single user, all users of a contract,
the whole network).
- Steps to reproduce, or a minimal proof-of-concept. For smart contract
issues, a failing test against the relevant contract
(`smart-contracts/contracts/<name>/src/test.rs`) is ideal.
- The affected component(s): a specific contract
(`agent_registry`, `agent_bidding`, `agent_marketplace`, `task_store`,
`dispute_resolution`, `error-registry`/`error-resolver`,
`upgrade-manager`), the backend, or the frontend.
- Whether the issue requires funds at risk to reproduce (testnet vs
mainnet), and your Stellar account/network if relevant to reproduction.

## Disclosure Timeline

We follow a coordinated disclosure process:

| Stage | Target timeline |
|---|---|
| Acknowledge receipt | Within 3 business days |
| Initial assessment (severity, affected components) | Within 7 days |
| Fix developed and validated | Depends on severity — see below |
| Fix deployed / patched release published | Before public disclosure |
| Public disclosure (advisory + credit) | Coordinated with reporter, typically 90 days after report or once a fix ships, whichever is sooner |

We will keep you informed of progress throughout and will coordinate the
disclosure date with you. If a fix cannot reasonably ship within 90 days
(e.g. it requires a contract migration or upgrade coordination), we will
explain why and propose a revised timeline rather than let the report go
stale.

Please keep the vulnerability confidential until we've published a fix or
otherwise agreed on a disclosure date with you.

## Severity & Scope

This is an early-stage, testnet-first project. Severity is judged primarily
by **impact**, not by whether funds are currently at risk on mainnet:

| Severity | Examples |
|---|---|
| **Critical** | Unauthorized fund movement from escrow/bonds; contract authorization bypass (calling a privileged entrypoint without the required `require_auth`); ability to forge or replay agent registration/task events |
| **High** | Denial of service against a contract or the backend (e.g. unbounded storage growth an attacker controls, as tracked in prior issues on the rate limiter and reward simulator); reputation or scoring manipulation that changes auction outcomes |
| **Medium** | Information disclosure of non-sensitive internal state; logic bugs that produce incorrect but non-exploitable results (e.g. an off-by-one in a cap check) |
| **Low** | Best-practice deviations with no direct exploit path; issues requiring an already-compromised keypair |

### In scope

- All contracts under `smart-contracts/contracts/`
- The backend API and WebSocket server (`backend/src/`)
- The frontend, where the vulnerability affects other users (not just the
reporter's own browser) — e.g. XSS, or a vulnerability in how
transactions are constructed/signed
- CI/CD configuration and dependency supply chain issues affecting the
above

### Out of scope

- Vulnerabilities requiring physical access to a user's device
- Social engineering against maintainers or contributors
- Denial of service via sheer traffic volume (rather than an amplification
or resource-exhaustion bug) against infrastructure we do not operate as
a public service
- Issues only reproducible on a fork/local clone with modified source
- Missing security headers or best-practice nits with no demonstrated
impact

## Rewards

ai-net does not currently operate a funded bug bounty program. Reports
that meet the criteria above receive public credit in the security
advisory (unless you prefer to remain anonymous) and, at maintainer
discretion, may be highlighted as a recognized contribution. If a funded
bounty program launches in the future, this document will be updated with
its scope and reward table.

## Acknowledgments

We will list researchers who report valid vulnerabilities here (with
permission) once the first reports are received and resolved.
Security fixes should include tests, migration notes when state changes, and a short advisory that avoids publishing exploit-ready payloads until users have had time to update.
28 changes: 28 additions & 0 deletions docs/operations/health-and-shutdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Health Checks and Graceful Shutdown

## Probe Contract

| Probe | Purpose | Expected status |
| --- | --- | --- |
| `GET /health` or `GET /health/live` | Process liveness. Returns without checking dependencies. | `200` while the process can answer HTTP |
| `GET /health/ready` | Readiness for serving traffic. Checks local task and payment stores. | `200` when ready, `500` when local stores fail |
| `GET /health/deep` or `GET /health/dependencies` | Dependency probes for Venice AI and Stellar Horizon. | `200` when all dependencies are reachable, `503` when degraded |

Readiness should be removed from load balancers before shutdown starts. Liveness should remain successful until the process is ready to exit so supervisors do not hard-kill the server during drain.

## Shutdown Order

1. Stop accepting new HTTP and websocket connections.
2. Stop registry sync and recurring background workers.
3. Mark running tasks as failed or interrupted with a durable reason.
4. Mark online agents offline so stale capacity is not advertised.
5. Flush logs, metrics, and reconciliation state.
6. Close task, agent, payment, and queue databases.

`GRACEFUL_SHUTDOWN_TIMEOUT` bounds the full drain. Production deployments should set the platform termination grace period higher than this value.

## Operator Checks

- Confirm `/health/ready` returns non-200 before terminating an instance during rolling deploys.
- Confirm `/health/deep` reports both `venice` and `horizon` as `ok` before enabling traffic.
- Review shutdown logs for each phase when debugging interrupted task execution.
33 changes: 33 additions & 0 deletions docs/threat-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ai-net Threat Model

## Assets

- Task prompts, prompt hashes, compressed DAGs, and agent outputs.
- Submitter wallets, payment escrow state, and reconciliation records.
- Agent reputation, bid commitments, reveal data, and refund state.
- Backend API availability and websocket task streams.

## Trust Boundaries

| Boundary | Risks | Required Controls |
| --- | --- | --- |
| Wallet to frontend | spoofed accounts, wrong network, replayed signatures | explicit network display, challenge freshness, signature purpose binding |
| Frontend to backend | oversized payloads, auth confusion, stale task reads | schema validation, rate limits, request ids, tenant/task authorization |
| Backend to agents | forged assignments, replayed results, unavailable agents | signed dispatch payloads, idempotency keys, heartbeat expiry |
| Backend to Stellar | stale ledger reads, failed submissions, reconciliation drift | retry budget, event indexing, periodic reconciliation |
| Contracts to indexers | missed lifecycle events, ambiguous terms | typed event payloads, stable glossary terms, replayable indexes |

## Primary Abuse Cases

- A bidder loses an auction and cannot independently recover bond state if award execution is delayed.
- A coordinator or indexer misses a task transition because lifecycle events are not explicit.
- A degraded dependency keeps receiving production traffic because readiness does not include dependency probes.
- A shutdown interrupts task dispatch, websocket streams, or reconciliation before state is flushed.

## Required Mitigations

- Emit explicit task lifecycle events for on-chain task state changes.
- Provide liveness, readiness, and dependency health probes for deployment platforms.
- Drain HTTP and websocket traffic before closing databases and background workers.
- Keep migration rollbacks deterministic and operator-invokable.
- Document vulnerability reporting, triage targets, and coordinated disclosure expectations.
119 changes: 119 additions & 0 deletions smart-contracts/contracts/agent_bidding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,60 @@ impl AgentBiddingContract {
Ok(())
}

/// Allow a losing bidder to claim their bid bond refund after a winner is selected.
///
/// This path intentionally excludes the winner because the winning bid proceeds to
/// escrow award. It lets unsuccessful bidders recover independently if `award_contract`
/// is delayed by the creator or an off-chain coordinator.
pub fn claim_bid_refund(env: Env, task_id: Symbol, bidder: Address) -> Result<(), Error> {
bidder.require_auth();

let auction: Auction = env
.storage()
.persistent()
.get(&DataKey::Auction(task_id.clone()))
.ok_or(Error::NotFound)?;

if auction.phase != AuctionPhase::Reveal {
return Err(Error::NotInRevealPhase);
}

let winner: Address = env
.storage()
.persistent()
.get(&DataKey::Winner(task_id.clone()))
.ok_or(Error::WinnerNotDetermined)?;
if winner == bidder {
return Err(Error::WinnerCannotClaimRefund);
}

let bid_key = DataKey::Bid(task_id.clone(), bidder.clone());
let mut bid: SealedBid = env
.storage()
.persistent()
.get(&bid_key)
.ok_or(Error::NotFound)?;
if bid.refunded {
return Err(Error::RefundAlreadyClaimed);
}

bid.refunded = true;
let bond = bid.bond;
env.storage().persistent().set(&bid_key, &bid);
extend_ttl_for_key(&env, &bid_key);

env.events().publish(
(symbol_short!("bidding"), symbol_short!("ref_claim")),
BondRefundClaimedEvent {
task_id,
bidder,
bond,
},
);

Ok(())
}

// ── Award Contract ─────────────────────────────────────────────────────

/// Award the contract to the winner determined by `reveal_bids`.
Expand Down Expand Up @@ -2048,6 +2102,71 @@ mod test {
assert!(!ghost_bid.refunded);
}

#[test]
fn unsuccessful_bidder_can_claim_refund_after_reveal() {
let (env, client) = setup();
let creator = Address::generate(&env);
let task_id = Symbol::new(&env, "claim_ref");

create_test_auction(&env, &client, &creator, &task_id, 3600);

let winner = Address::generate(&env);
let loser = Address::generate(&env);
let terms = String::from_str(&env, "Terms");
let winner_salt = BytesN::<32>::from_array(&env, &[51u8; 32]);
let loser_salt = BytesN::<32>::from_array(&env, &[52u8; 32]);
let winner_price = 2_000_000i128;
let loser_price = 5_000_000i128;
let winner_commitment = test_commitment(&env, &winner, winner_price, &terms, &winner_salt);
let loser_commitment = test_commitment(&env, &loser, loser_price, &terms, &loser_salt);

client.submit_bid(&task_id, &winner, &winner_commitment, &500_000, &80);
client.submit_bid(&task_id, &loser, &loser_commitment, &500_000, &80);

env.ledger().set_timestamp(env.ledger().timestamp() + 3601);
client.reveal_bid(&task_id, &winner, &winner_price, &terms, &winner_salt);
client.reveal_bid(&task_id, &loser, &loser_price, &terms, &loser_salt);
client.reveal_bids(&task_id);

client.claim_bid_refund(&task_id, &loser);

let loser_bid = client.get_bid(&task_id, &loser).unwrap();
assert!(loser_bid.refunded);
let events = env.events().all();
assert_eq!(
events.last().unwrap().1,
(symbol_short!("bidding"), symbol_short!("ref_claim")).into_val(&env)
);
}

#[test]
fn winner_cannot_claim_unsuccessful_bidder_refund() {
let (env, client) = setup();
let creator = Address::generate(&env);
let task_id = Symbol::new(&env, "win_ref");

create_test_auction(&env, &client, &creator, &task_id, 3600);

let winner = Address::generate(&env);
let loser = Address::generate(&env);
let terms = String::from_str(&env, "Terms");
let winner_salt = BytesN::<32>::from_array(&env, &[53u8; 32]);
let loser_salt = BytesN::<32>::from_array(&env, &[54u8; 32]);
let winner_commitment = test_commitment(&env, &winner, 2_000_000, &terms, &winner_salt);
let loser_commitment = test_commitment(&env, &loser, 5_000_000, &terms, &loser_salt);

client.submit_bid(&task_id, &winner, &winner_commitment, &500_000, &80);
client.submit_bid(&task_id, &loser, &loser_commitment, &500_000, &80);

env.ledger().set_timestamp(env.ledger().timestamp() + 3601);
client.reveal_bid(&task_id, &winner, &2_000_000, &terms, &winner_salt);
client.reveal_bid(&task_id, &loser, &5_000_000, &terms, &loser_salt);
client.reveal_bids(&task_id);

let err = client.try_claim_bid_refund(&task_id, &winner);
assert_eq!(err.err(), Some(Ok(Error::WinnerCannotClaimRefund)));
}

#[test]
fn award_contract_before_reveal_bids_fails() {
let (env, client, _) = setup();
Expand Down
Loading