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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Core modules:
- `connect/mech.py` — mech marketplace requests via mech-client's `Signer` protocol, so every transaction/digest still passes the choke point. An optional caller-chosen `request_id` makes a request replayable, and an optional `request_context` dict rides along in the request metadata for the tool (README, "Mech requests"). `_metadata_extras` builds those extras once so the off-chain digest and mech-client hash the same bytes; `_request_stamp` covers the context too, since the same prompt with a different market price is a different question.
- `connect/mech_allowances.py` — the single-use grants that flow registers with the guard before it signs or spends: the safe's ERC-1271 wrap of a request id it derived locally (trusting no RPC for anything signed) and the one capped safe→tracker deposit a 402 top-up would send. Split out of `mech.py`, which was over pylint's 1000-line limit; the seam is real, and `MechService` no longer holds the guard at all — this is its whole relationship with it.
- `connect/mech_budget.py` — the default `max_payment` per mech-client `PaymentType` (0.1 of the payment asset) and the token each type is paid in on a chain, which `mech_tools` reports as `payment_token`. Split out of `mech.py` for the same line limit.
- Mech terms come from mech-client, not this repo: `tools()` merges `service.tool_manager.terms_report(priority_mech, document)` (`valory_operated`, `terms` for a Valory mech, `identification_note` when the DNS check could not complete, and the operator's validated `terms_url`) into a single-mech report, and `_prepare` calls it before arming any allowance, so a failure spends nothing: with the document its off-chain pre-flight already read, or none on-chain. `mech_request`'s report therefore carries `valory_operated`, `terms` or `identification_note`, and `terms_url` off-chain. `tools()` adds `terms_note` when the document was unreadable, as it does `tools_note`. Each report runs its own DNS check, and mech-client 0.23.3 runs one more inside `send_request`; there is no memoisation upstream. `route_mech_client_logs` in `__main__.py` sends mech-client's logger (stdout-only, propagation off, by its own setup) through the root handlers, so its warnings reach `log.txt`. The rule that only a Valory mech gets the Valory statement lives upstream in one place; do not restate it here. `test_tools_use_mech_client_s_real_terms_report` runs the pinned mech-client's real implementation past a faked resolver, so a pin bump that changes its shape fails there.
- Mech report: `tools()` for the single-mech report, and `_prepare` before any allowance is armed (so a failure spends nothing), call mech-client's `ToolManager.mech_report_or_terms(mech, document)` directly, inside `listing_mechs()` so its indexer reads do not log false RPC mismatch warnings after a request on another chain. There is no deadline of connect's own: a slow proof host stalls only the report on the mech being paid, and the request's `timeout` does not bound the other steps before payment either. Everything is mech-client's: with a readable document and record the report is `ToolManager.mech_report`, the terms keys (`valory_operated`, `terms` for a Valory mech, `identification_note` when the DNS check could not complete, the operator's validated `terms_url`) plus `operator` (`domain`, `url`, `domain_verified`, `verification_note`, `claimed_name`, `name_note`), `benchmarks` (tool to link) and `benchmark_note`, `delivery` (counters, `since`, `age_days`, `note`) and `payment` (`type`, `note`); an unreadable manifest or record gives the terms plus `report_note`. The on-chain flow never reads the document and calls `terms_report(mech, None)`. The domain is the operator's identity and the name only its claim; the skill tells the agent to name the domain, and that a missing `operator` means the operator is unknown unless `valory_operated`. The report fields explain themselves through their notes, so the skill and tool descriptions name them and add only what the report cannot say. Each report is its own check: a DNS lookup, an indexer read, an agent-id lookup and a proof fetch, none memoised upstream, and mech-client 0.24.0 runs one more DNS check inside `send_request`; two reports in one flow can disagree after a blip, and `mech_request`'s governs the request it sent. `route_mech_client_logs` in `__main__.py` sends mech-client's logger (stdout-only, propagation off, by its own setup) through the root handlers, so its warnings reach `log.txt`. Do not restate or recompute any of mech-client's rules here. `test_tools_use_mech_client_s_real_report` and the two fallback guards run the pinned mech-client's real implementation past a faked resolver, indexer, agent-id lookup and proof fetch, so a pin bump that changes its shape fails there.
- `connect/mech_rpc.py` — mech-client reads the RPC from the process-global `MECHX_CHAIN_RPC` (valory-xyz/mech-client#247), so `RPC_LOCK` serialises the places that must set it: building a chain's service, and just before a request is sent, because an auto-deposit rebuilds a mech-client service mid-send. Listing never sets it or takes the lock — a subgraph query can run for minutes — and only reads it for a chain-id check, so `listing_mechs()` drops the stale-RPC warning that check raises, for that thread only.
- `connect/mech_types.py` — the error and shapes `mech.py` and `mech_allowances.py` share, in neither of them so the pair does not import in a circle. `from connect.mech import MechError` still resolves.
- `connect/idempotency.py` — at-most-once execution keyed by caller-chosen request ids, for actions paid for before they are answered; `mech.py` is the only user, and `signer.py` keeps its own simpler cache mapping ids to tx hashes. Its entries hold a whole report plus a stamp of what was asked, because a replay has to resume a watch rather than repeat a value, and a reused id must not answer a different question.
Expand All @@ -84,6 +84,6 @@ Other trees:
## Dependency quirks

- `open-aea-ledger-ethereum` is pinned direct, though mech-client already resolved it at the same version. Importing mech-client already loads the `aea` core and this plugin, so the pin costs no modules in the process or the bundle; it only stops a transitive bump from moving it. The mypy env silences the import rather than installing it: the package ships `py.typed`, but installing it drags in a real `web3` too, which un-silences `[mypy-web3.*]` across the whole tree.
- `mech-client` is pinned at `0.23.3`, the first release with `ToolManager.terms_report` and DNS identification of Valory mechs. `0.23.0` was the first that lists Robinhood Chain's mechs (its marketplace indexer is a squid, which 0.22.x could not query). `0.22.0` was the first carrying the `deliveries`/`DeliveryResult` shape `connect/mech.py` reads. `mech_request`'s default `max_payment` is 0.1 of the mech's payment asset, keyed by mech-client's `PaymentType` (`DEFAULT_MAX_PAYMENT` in `connect/mech_budget.py`), so a payment type added upstream is refused until it gets a default.
- `mech-client` is pinned at `0.24.0`, the first release with `ToolManager.mech_report` and `mech_report_or_terms` (operator domain verification, benchmark links, delivery record, payment method, and the fallback to the terms). `0.23.3` was the first with `ToolManager.terms_report` and DNS identification of Valory mechs. `0.23.0` was the first that lists Robinhood Chain's mechs (its marketplace indexer is a squid, which 0.22.x could not query). `0.22.0` was the first carrying the `deliveries`/`DeliveryResult` shape `connect/mech.py` reads. `mech_request`'s default `max_payment` is 0.1 of the mech's payment asset, keyed by mech-client's `PaymentType` (`DEFAULT_MAX_PAYMENT` in `connect/mech_budget.py`), so a payment type added upstream is refused until it gets a default.
- `[tool.uv] override-dependencies` pins fastapi past mech-client's transitive `fastapi<0.118` pin (operate paths in mech-client never run here). This is why the pylint/test tox envs use `uv sync` in `commands_pre` instead of a normal package install — pip can't honor the override.
- pytest runs with `-p no:anchorpy` (a transitive plugin whose own deps aren't installed).
45 changes: 36 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,15 +312,42 @@ does not resolve, a failed or timed-out lookup, or a zone that answers every
name all read as not identified, and a check that could not complete says so
in `identification_note`. Whatever terms link an operator published in its
metadata is passed through as `terms_url`, reported as found rather than
endorsed; `terms_note` says when the metadata could not be read, so an
absent link is unknown rather than unpublished. `mech_request` reports
`valory_operated` and `terms` or `identification_note` too, plus `terms_url`
on the off-chain flow, decided before anything is paid or any allowance is
armed, so a session that skips `mech_tools` still learns whose terms apply.
Each report runs its own check, and `mech_request`'s is the one that governs
the request it sent. mech-client's warnings, including a check that could not
complete, are routed into `log.txt`. The check and the terms statement come from mech-client
(`ToolManager.terms_report`), so the rule lives in one place.
endorsed.

The same report says who runs the mech and how it has behaved, from
mech-client's `ToolManager.mech_report_or_terms`. In `operator` the domain is
the identity: `domain_verified` is true only when
`https://<domain>/.well-known/agent-registration.json` lists the mech's
ERC-8004 agent id for the chain's identity registry (the agent id is read from
the service registry indexer; it is not the Olas service id except by
coincidence). Otherwise `verification_note` says why, including a domain that
redirects instead of serving the proof itself, and the proof is fetched again
next time rather than remembered as a no. The name is `claimed_name`, the
operator's own text, never checked, which `name_note` says: a verified domain
proves the domain, not the name. `benchmarks` maps each tool to the link the
operator publishes, as a claim and never as a figure, which `benchmark_note`
says. `delivery` is the
on-chain record from the marketplace indexer: requests received,
self-delivered and delivered by others, the creation time in ISO 8601 UTC and
`age_days`; its `note` says the counters cover on-chain requests and off-chain
requests the mech settled on-chain, so an off-chain request that was never
delivered appears nowhere. `payment` names the method with no price, since the
contract quotes the charge at request time. The record is only an aid to
choosing: when the manifest or the indexer cannot be read, mech-client reports
the terms alone and says so in `report_note`.

`mech_request` reports the same fields, decided before anything is paid or any
allowance is armed, so a session that skips `mech_tools` still learns whose
mech it pays and whose terms apply: `valory_operated` and `terms` or
`identification_note` on both flows, plus `operator`, `benchmarks`,
`delivery`, `payment` and `terms_url` on the off-chain flow, which is the one
that reads the manifest. Each report is its own check, resolver, indexer and
proof fetch alike, so two reports in one flow can disagree after a DNS blip
or an indexer hiccup; `mech_request`'s is the one that governs the request it
sent. mech-client's warnings, including a check that could not complete, are
routed into `log.txt`. Every check and label comes from mech-client, so the
rules live in one place; this server recomputes none of the domain check, the
id resolution or the labels.

An optional `request_context` dict is passed through unmodified as a
top-level key of the request metadata, for the tool to read; the bundled skill
Expand Down
7 changes: 5 additions & 2 deletions connect/assets/skills/pearl-connect/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,15 @@ composing a prompt:
or the check could not complete, in which case `identification_note` says
so; its operator's own terms apply. The report passes through whatever link
that operator published as `terms_url`, which is reported as found, not
endorsed; `terms_note` means the metadata could not be read, so any link
is unknown. `mech_request` reports the same fields (`terms_url` only on
endorsed. `mech_request` reports the same fields (`terms_url` only on
the off-chain flow). Each report is its own check, so the two can differ
after a DNS hiccup: state whose terms apply from `mech_request`'s report,
which governs the request it sent. Tell the user whose terms a request
falls under before spending their funds on an unfamiliar mech.
When saying who runs a mech, name `operator.domain` and whether it is
verified; `claimed_name` is only the operator's claim. With no `operator`,
who runs the mech is unknown unless `valory_operated` is true, so do not
guess. Tell the user if the domain is unverified before paying.
- **`offchain_capable`.** The off-chain flow needs an endpoint published in
the mech's service metadata, and few mechs publish one; the rest serve
on-chain requests only. When it is `false`, `offchain_note` says why — a
Expand Down
18 changes: 9 additions & 9 deletions connect/mech.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class _RequestPlan(t.NamedTuple):
legacy_on_chain: bool
tool: str
max_payment: int
terms: dict # mech-client's terms_report, merged into the request's report
terms: dict # mech-client's mech report, merged into the request's report


class PendingDelivery(t.NamedTuple):
Expand Down Expand Up @@ -444,13 +444,10 @@ def tools(
info["offchain_capable"] = blocker is None
if blocker is not None:
info["offchain_note"] = blocker
# Who the session contracts with, and whose terms that request falls
# under. Only the Valory claim is ours to make; a published terms link
# is reported as found.
# mech-client owns the rule that only a Valory mech gets the statement.
info.update(service.tool_manager.terms_report(priority_mech, read.document))
if read.document is None: # like tools_note: unread is not unpublished
info["terms_note"] = "metadata unreadable, so any terms link is unknown"
with listing_mechs():
info |= service.tool_manager.mech_report_or_terms(
priority_mech, read.document
)
return info

def _list_mechs(self, chain: str, *, limit: int, offset: int) -> dict:
Expand Down Expand Up @@ -724,7 +721,10 @@ def _prepare( # pylint: disable=too-many-arguments,too-many-locals
f"off-chain requests: {blocker}"
)
# before any allowance is armed, so a failure here spends nothing
terms = service.tool_manager.terms_report(priority_mech, read.document)
with listing_mechs():
terms = service.tool_manager.mech_report_or_terms(
priority_mech, read.document
)
# Pin the metadata salt so the CID — and with it the request id
# mech-client will derive and sign — is known here first; the
# matching allowance is registered before the send. Armed in
Expand Down
2 changes: 1 addition & 1 deletion connect/mech_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def filter(self, record: logging.LogRecord) -> bool:

@contextlib.contextmanager
def listing_mechs() -> t.Iterator[None]:
"""Run a mech listing, which reads MECHX_CHAIN_RPC only to warn about it."""
"""Run a mech listing or report; both read MECHX_CHAIN_RPC only to warn."""
_LISTING.active = True
try:
yield
Expand Down
5 changes: 4 additions & 1 deletion connect/server/mcp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ async def mech_request( # pylint: disable=too-many-arguments
Off-chain by default: few mechs can serve that, so check
`offchain_capable` with mech_tools first; legacy_on_chain=true goes
through the marketplace instead.
Off-chain the report also carries mech-client's `operator`,
`delivery`, `payment` and `benchmarks`, decided before paying.
Refused before paying if the mech's price exceeds max_payment
(base units of the mech's payment asset, default 0.1 of that asset;
mech_tools reports the asset as payment_token).
Expand Down Expand Up @@ -294,7 +296,8 @@ async def mech_tools(
(`total` says how many exist). With it: that mech's payment type,
service id, tool names for mech_request's `tool`, and
`offchain_capable` — false means only legacy_on_chain=true reaches it,
and `offchain_note` says why.
and `offchain_note` says why. The report also carries mech-client's
`operator`, `delivery`, `payment` and `benchmarks`.
"""
# same as mech_request: the sync gql subgraph client refuses to run
# on an already-running loop
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dependencies = [
"web3==7.16.0",
"eth-account==0.13.7",
"mcp==1.28.1",
"mech-client==0.23.3",
"mech-client==0.24.0",
Comment thread
DIvyaNautiyal07 marked this conversation as resolved.
"open-aea-ledger-ethereum==2.2.9",
"tomli-w==1.2.0",
]
Expand Down
Loading
Loading