---
name: sup-assessor-dev
description: >-
  Write a CONFORMING Sup escrow ASSESSOR in Move — a tiny package that decides
  when an agent-to-agent escrow job releases funds to the provider (or refunds
  the payer). An assessor is the release-gate of `sup_escrow::job`: it stamps a
  `ReleaseRequest` hot potato once its condition holds — a bearer cap, an owner
  signoff, an oracle/attestation of delivery, a ZK proof, an M-of-N quorum, or
  another agent. It plugs in using only the public interface — no core changes,
  no permission. Use when a developer wants to build a new way to settle escrow
  ("release when X is proven"). This is the SETTLEMENT-leg sibling of
  `sup-policy-rule-dev` (which gates spending FROM a wallet). For funding escrow
  from a vault, that's the delegation policy (`sup-policy-rule-dev`).
license: MIT
---

# Sup Escrow Assessor — Developer Skill

You help a developer **write a Move assessor** that decides when an escrow
`Job<CoinT>` settles. The escrow flow has two policy-shaped moments:

1. **Fund** — money leaves the vault into the job through the **delegation policy**
   (`confirm_spend_into`, bounded by the owner's cap). That's `sup-policy-rule-dev`.
2. **Release** — money leaves the **job** to the provider (approve) or back to the
   payer (reject), gated by an **assessor**. That's *this* skill.

An assessor mirrors a policy rule exactly — a witness that stamps a hot potato —
but it gates the **job's release**, not a wallet spend. `Job.assessor_type`
(chosen by the owner at `create`) plays the role `auth_rules` plays in a policy.

Your job: a package that (a) **compiles**, (b) is **compliant** (touches a release
only by stamping, after checking its condition + binding to the job; never moves
funds), and (c) can be listed in the rule/condition marketplace.

## The two hard requirements

A package is a Sup assessor iff:

1. **It declares a witness type** — a struct with `has drop` and nothing else:
   `public struct MyAssessor has drop {}`. Its fully-qualified name
   `0x<pkg>::<module>::MyAssessor` is the **assessor-type**: the owner picks it at
   `job::create<CoinT, MyAssessor>(...)`, and `confirm_release` only accepts a
   stamp of that exact type. Only the defining module can build `MyAssessor {}`,
   which makes the stamp unforgeable.
2. **It influences a release ONLY by stamping the request** — call
   `job::judge(MyAssessor {}, req, approve)` after your condition holds and you've
   bound to the right job. Never take the job's `Balance`/`Coin`, never call
   `confirm_release` / `payout` yourself, never `transfer` job funds.

## The safety boundary (why compliance is structural)

`job::begin_release` returns a `ReleaseRequest` with **no abilities** (no
`drop`/`store`/`copy`). It must reach `confirm_release` or the whole transaction
aborts. Your assessor receives it as `&mut ReleaseRequest`, but the only mutation
the `job` API exposes is `judge` (append your witness + set the approve flag).
You can:

- **read the facts** — `job::request_job_id(req)`, `request_provider(req)`,
  `request_amount(req)`;
- **stamp + decide** — `job::judge(MyAssessor {}, req, approve)` (`approve = true`
  pays the provider; `false` refunds the payer);
- **abort** — if your condition fails or the request isn't for your job.

You **cannot** change the provider, the amount, or the funds. So a buggy or
malicious assessor can only block / mis-route *within the two pre-set outcomes*
(pay provider / refund payer) of jobs that **opted into it** — it can never
redirect funds to a new address or inflate them. **Always bind to the job id** so
a stamp minted for job A can't settle job B.

## Pattern — a cap-based assessor (bearer, the reference)

Whoever holds the cap (bound to a job) may settle it. This is `cap_assessor`.

```move
module my_assessor::cap_assessor;

use sui::{object::{Self, ID, UID}, tx_context::{Self, TxContext}, transfer};
use sup_escrow::job::{Self, Job, ReleaseRequest};

const EWrongJob: u64 = 1;
const ENotOwner: u64 = 2;

public struct AssessorCap has key, store { id: UID, job_id: ID }   // bearer, bound to a job
public struct MyAssessor has drop {}                               // the assessor-type witness

/// Owner (the job's payer) mints the cap and hands it to whoever judges delivery.
public fun mint<CoinT>(job: &Job<CoinT>, ctx: &mut TxContext): AssessorCap {
    // The Job stores no payer address — admin rights resolve from the FUNDING
    // WALLET's current owner, so the job survives an owner rotation. This helper
    // checks `job.wallet_id == wallet::id(wallet)` AND `sender == wallet::owner(wallet)`.
    job::assert_job_owner(job, wallet, ctx);
    AssessorCap { id: object::new(ctx), job_id: object::id(job) }
}

/// Stamp the request iff this cap is bound to its job. `approve` pays the
/// provider; otherwise the payer is refunded. Read-only apart from the stamp.
public fun assess(cap: &AssessorCap, req: &mut ReleaseRequest, approve: bool) {
    assert!(cap.job_id == job::request_job_id(req), EWrongJob);
    job::judge(MyAssessor {}, req, approve);
}
```

## Pattern — an oracle/condition assessor (no cap, gate on a fact)

```move
public struct DeliveredAssessor has drop {}

/// Stamp APPROVE iff a trusted oracle/attestation object says this job's work
/// was delivered. (Swap the `Attestation` read for Pyth / a ZK verifier / an
/// 8183 Job marked Terminal — the shape is identical.)
public fun assess(att: &Attestation, req: &mut ReleaseRequest) {
    assert!(att.job_id == job::request_job_id(req), EWrongJob);
    assert!(att.delivered, ENotDelivered);
    job::judge(DeliveredAssessor {}, req, true);   // condition proven → release
}
```

## Assessor ideas — a menu to build from

The owner chooses *one* assessor-type per job; the design space is wide:

- **Owner signoff** — `assess` asserts the sender is the funding wallet's current owner (`job::assert_job_owner`); the human approves.
- **Bearer cap** — the reference above; hand the cap to a reviewer or another agent.
- **M-of-N quorum** — require N distinct co-signer caps to stamp before approve.
- **Oracle / attestation** — release when a trusted feed marks the work delivered.
- **ZK proof** — release when a Groth16/zk proof of completion verifies.
- **Timeout auto-release / auto-refund** — pair with a `&Clock`: approve after a
  review window, or refund if never submitted.
- **8183 job assessor** — release when an external `Job` reaches `Terminal`.

## How a release is composed (one PTB)

```move
let mut req = job::begin_release<CoinT>(&job);     // only valid when job is Submitted
my_assessor::cap_assessor::assess(&cap, &mut req, true);   // your condition → stamp + decide
job::confirm_release<CoinT>(&mut job, req, ctx);   // pays provider (approve) / refunds payer
```

The owner picks your assessor at job creation:
`job::create<CoinT, my_assessor::cap_assessor::MyAssessor>(wallet, provider, ctx)`.
Until a job trusts your assessor-type, your package has zero power over it.

## Move.toml

```toml
[package]
name = "my_assessor"
edition = "2024.beta"

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet", override = true }
sup_escrow = { local = "../sup_escrow" }   # or a git/published dep

[addresses]
my_assessor = "0x0"
```

## Compliance checklist (run before "done")

- [ ] Witness struct is `has drop` **only** (no `copy`/`store`/`key`).
- [ ] Touches a release **only** via `job::judge` — never takes the job's `Coin`/`Balance`, never calls `confirm_release` / `payout`, never `transfer`s job funds.
- [ ] `assess` **binds to the job** — asserts your cap/config/attestation `job_id == job::request_job_id(req)` (so a stamp for job A can't settle job B).
- [ ] The approve/reject decision is deliberate and checked (don't blindly `judge(.., true)`).
- [ ] Any cap/config mint is owner-gated via `job::assert_job_owner(job, wallet, ctx)` — never a stored owner address.
- [ ] A test composes `begin_release → assess → confirm_release` for both approve (provider paid) and reject (payer refunded), plus an `#[expected_failure]` for the wrong-job / unmet-condition path. (See `sup_escrow`'s `escrow_tests`.)
- [ ] Consider freezing the package — owners trust immutable assessors more.

## Register it (reuse the rule/condition marketplace)

An assessor is a condition witness, like an auth/caveat rule — so it lists in the
**same `policy_rule_registry`**, tagged as an assessor in its off-chain manifest
(don't spin up a separate registry):

```
policy_rule_registry::registry::register(
    registry, package_id, rule_type /* the assessor-type */, kind, manifest_uri, manifest_hash, clock, ctx)
```

Off-chain manifest (hash-pinned at `manifest_uri`): set `"category": "assessor"`
so UIs/agents filter it as an escrow settler rather than a spend rule. On-chain
stores only the anchor; listing grants no power — only an owner choosing your
assessor-type at `job::create` does.

```jsonc
{
  "name": "Delivery oracle",
  "category": "assessor",
  "assessorType": "0xPKG::assessor::DeliveredAssessor",
  "summary": "Releases escrow when a trusted oracle marks the work delivered.",
  "settles": "sup_escrow::job"
}
```

## Reference

- **Cap assessor** (bearer, the worked example) —
  historical [`cap_assessor.move`](https://github.com/ZzyzxLabs/zzyzx-full-repo/blob/16fceb6d1de5bc13a1cfe580034abd7efc9a4538/ZZYZX-Contract/Sup_Contract/official/sup_escrow/sources/cap_assessor.move).
- **The escrow engine** (read once to internalize the contract) —
  `.../sup_escrow/sources/job.move`, plus the design in
  `.../sup_wallet/ESCROW_AND_AP2.md`.
- **The funding side** (delegation policy, the other leg) — `sup-policy-rule-dev`.

> The surface is intentionally tiny: a witness + a function that checks a
> condition, binds to the job, and stamps a decision. "Who may release escrow" is
> as pluggable as "who may delegate" — learn the hot-potato contract once and
> every assessor is "read the request → check your thing → judge (or abort)."
> An assessor's only power is to choose between *pay the provider* and *refund the
> payer* — never to touch the funds itself.
