@mcsherrylabs/pi-evolver
v0.8.2
Published
Pi agent extension: chain-generic tools over an Evolver node — read the chain, sign a transfer, administer what you own, write to a channel, submit a signed transaction. Wraps @mcsherrylabs/evolver-client; implements no wire format of its own.
Maintainers
Readme
@mcsherrylabs/pi-evolver
A Pi agent extension: chain-generic tools over an Evolver node. Read the chain, sign a transfer, administer what you own, write to a channel, submit a signed transaction.
It wraps @mcsherrylabs/evolver-client and implements no wire format or signing encoding of its
own. It knows about no particular channel, ledger or service — a tool that names a channel belongs
in that channel's own repository.
Specs: 161-pi-evolver-tools, 162-agent-write-coverage, 164-sign-channel-write.
Running the checks
Every check runs with one command (Constitution Quality Gate 10), from this directory:
| Command | What it pins |
|---|---|
| npm test | everything below, after building the client. ⚠️ Held to it by check:packaging, which asserts that the check:* scripts, the test chain and this table name the same set — check:signing was declared, documented here as wired, and in no gate for eight days |
| npm run check:config | the precedence rule — a project file may override the chain url and nothing else |
| npm run check:result | the failure taxonomy — a transport error is never a refusal, fined is never committed |
| npm run check:principal | a name that exists as the OTHER kind is wrong_kind, never absent — and a 404 group is null, not an empty one |
| npm run check:decode | every transaction body decodes, through a reader written from the WIRE FORMAT rather than from the encoder it checks |
| npm run check:admin | every signing verb signs and submits nothing, records what it composed, and refuses what the chain would only refuse by FINING |
| npm run check:amount | amounts are strings out and bigint in, and the string is validated |
| npm run check:boundary | scan coverage is a round window, never a row count |
| npm run check:signing | signing does not submit |
| npm run check:bearer | no tool takes or returns the signed transaction — submit and inspect work by id, and the artefact never reaches the transcript in any encoding |
| npm run check:heights | the three heights a node reports mean three different things, and the read never collapses them |
| npm run check:coverage | every tool the extension registers is exercised by a check, so a new verb cannot arrive untested |
| npm run check:surface | no tool names a channel; a signing tool takes named typed fields only — and the ONE verb exempt from that is exempt by name and must refuse every core ledger id |
| npm run check:channelterms | a channel's owners are OBJECTS and the SDK's type says so — scraped from the node's own JSON builder, elements AND container, with a scrape-broke control |
| npm run check:coreledger | the core-ledger set is the one the node declares, scraped from its own source, with a scrape-broke control |
| npm run check:channelwrite | a channel write refuses a malformed body, an oversized one and an unstated toll ceiling — and costs the same whatever its length |
| npm run check:ownership | every signing verb is classified as ownership-changing or explicitly neutral; the node's identity is never guessed |
| npm run check:packaging | the package declares an entry point that resolves |
| npm run check:registration | loading the extension registers every tool |
| npm run check:signed-request | evolver_signed_request against a REAL local relying party: a proof it verifies; a NO or no human signs and sends nothing; no placeholder chooses the destination; redirects are not followed; the prompt shows what is sent |
⚠️ These now run in CI. ci/npm-test.sh is the last step of the test job and the root
npm test includes this package, so a red pipeline is this package's problem. (That is a change
from what this file said while issue #77 was open; a stale "CI does not run this" is how a gate
stops being read.)
⚠️ npm test builds @mcsherrylabs/evolver-client first on purpose. The checks import it by
package name, which resolves to its built output — without the build they would pass against stale
bytes.
⚠️ The ownership report is a tripwire, not decoration
evolver_status reports what the configured identity owns. It should say none.
⚠️ AND THE AGENT CAN NOW END THAT ITSELF. Before 162 the only way this identity came to administer anything was somebody else adding it to a group. It can now create groups, admit members and hand groups over — so "owns nothing" stops being true by the agent's own action, and the no-confirmation decision that rests on it (161 FR-012a) has to be re-made rather than re-weighed when it does.
Nothing enforces that. Ownership is reported, never a precondition for signing: the chain is the authority — a non-owner's membership change is FINED and a transfer spends only the sender's balance — and a gate here would add nothing while coupling every signature to a paged walk of every channel owner on the chain. The status tool is the tripwire, and a human reading it is what notices. It is re-asked after any submission that can change the answer, so it will not keep reporting a state the agent has already left.
This package spends without asking for confirmation, and that decision is sound because the consequence of a mistake — or of an instruction injected through chain content the agent reads — is bounded by this identity's own balance. That bound holds only while the identity owns nothing.
"Owns nothing" is a state a stranger can end. A group owner may add any identity to their group without that identity's consent or knowledge, and membership of a group that owns a channel confers administration of it — changing its toll, its write policy, its availability policy, or releasing it. The likeliest route is not an attack but a favour: adding the agent to the group is the only way to let it write to that group's channel.
So if owns ever reports anything but none, the premise behind spending-without-confirmation
has expired and that decision has to be made again rather than weighed again. Signing refuses
outright if ownership cannot be determined at all — an authority question with no answer is not a
yes.
Signing in to an off-chain service: evolver_signed_request
The transaction verbs sign for the chain; this one proves who we are to anything that verifies
evolver 157 login proofs. The model fetches a challenge itself and passes it with the service's
audience and the request to send, marking where the answer goes in the body or a header with
{answer} or {answer.<field>}, so any request shape works. The SDK's signedRequest shows the
human that request as it will be sent, escaped, with the host repeated last and warnings for
plain http or an audience that does not name the host. The choice defaults to No. Only on
yes does it sign, with target.signer, send the request itself (following no redirects),
and return the response.
⚠️ The tool sends because a returned answer could be relayed. Anyone holding a 157 answer can
redeem it at its audience, so a tool that handed one to the model would let text the model read
route a genuine-looking login to whoever asked for it. The answer leaves the process only inside
the one request the human approved, to the origin they saw. So nothing rendered comes back: the
answer may not be placed in the URL (URLs are echoed in errors, Location and logs), a failure
after signing reports the template and a fixed error class, and the signature is scrubbed from any
Location or response body that echoes it.
⚠️ What is accepted, and recorded (openstar/evolver-meerkat#1):
- The response goes to the model, and for a login it may be a session credential. A service that mints a token on sign-in hands it back, and the model then holds it. The prompt says so. A service that re-proves every call returns nothing redeemable.
- The audience is the model's to choose, and it decides where a proof is valid. It is bound to the destination only by the human reading the prompt and a label-aligned audience/host warning. Binding audience to origin in configuration would close relay whatever the human does; it is the follow-up.
- Plain http is warned, not refused, because the first relying party is served over http. An on-path party can read the answer and use it first.
- It signs AND sends, against 161 FR-006 ("signing must not submit"). Returning the answer
instead is the relay.
check:surface5c records the exemption by name. - The body is not signed, which is why the prompt shows it in full.
⚠️ In RPC mode the RPC client is the human. pi reports a UI there, and every yes/no is answered by whatever drives the RPC connection, which may be a program. Driving an agent over RPC makes that program responsible for every approval.
Configuration
.pi/agent/evolver.json in a project, or ~/.pi/agent/evolver.json globally.
⚠️ A project-level file may override the chain URL and nothing else. The identity, the key and how the endpoint is trusted come from the home-level file or the environment only — otherwise a repository you open could name a different signing key, and the only gate is a prompt about trusting a project.
What this package will not do
- Name a channel, ledger or service in any tool.
- Claim an identity. It is given one.
- Accept a transaction body, ledger, kind or sender in a signing tool — a signing tool composes what
it signs from named fields and pins the ledger itself. ⚠️ With exactly one exception, granted
by name and paid for:
evolver_sign_channel_writetakes both, because a channel write HAS no named fields — the body IS the payload and the ledger IS the channel. What it buys the exemption with is a refusal: it will not sign for any ledger the node would INTERPRET (the six core ledgers and the whole reservedce.evolver.namespace), because writing "opaque bytes" to the identity ledger is signing whatever that ledger'''s handler reads — a transfer, or a key registration.
