@provian/mcp 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +264 -2
  4. package/bin/provian-mcp.mjs +18 -0
  5. package/dist/agent-wallet-registration.d.ts +34 -0
  6. package/dist/agent-wallet-registration.d.ts.map +1 -0
  7. package/dist/agent-wallet-registration.js +440 -0
  8. package/dist/agent-wallet-registration.js.map +1 -0
  9. package/dist/core/challenge.d.ts +187 -0
  10. package/dist/core/challenge.d.ts.map +1 -0
  11. package/dist/core/challenge.js +536 -0
  12. package/dist/core/challenge.js.map +1 -0
  13. package/dist/core/crypto.d.ts +44 -0
  14. package/dist/core/crypto.d.ts.map +1 -0
  15. package/dist/core/crypto.js +124 -0
  16. package/dist/core/crypto.js.map +1 -0
  17. package/dist/core/disclosure-request.d.ts +169 -0
  18. package/dist/core/disclosure-request.d.ts.map +1 -0
  19. package/dist/core/disclosure-request.js +400 -0
  20. package/dist/core/disclosure-request.js.map +1 -0
  21. package/dist/core/genesis-offer.d.ts +25 -0
  22. package/dist/core/genesis-offer.d.ts.map +1 -0
  23. package/dist/core/genesis-offer.js +34 -0
  24. package/dist/core/genesis-offer.js.map +1 -0
  25. package/dist/core/harness-tool-vocabulary.d.ts +55 -0
  26. package/dist/core/harness-tool-vocabulary.d.ts.map +1 -0
  27. package/dist/core/harness-tool-vocabulary.js +116 -0
  28. package/dist/core/harness-tool-vocabulary.js.map +1 -0
  29. package/dist/core/keccak256.d.ts +15 -0
  30. package/dist/core/keccak256.d.ts.map +1 -0
  31. package/dist/core/keccak256.js +111 -0
  32. package/dist/core/keccak256.js.map +1 -0
  33. package/dist/core/key-custody.d.ts +33 -0
  34. package/dist/core/key-custody.d.ts.map +1 -0
  35. package/dist/core/key-custody.js +59 -0
  36. package/dist/core/key-custody.js.map +1 -0
  37. package/dist/core/meter-hash.d.ts +2 -0
  38. package/dist/core/meter-hash.d.ts.map +1 -0
  39. package/dist/core/meter-hash.js +8 -0
  40. package/dist/core/meter-hash.js.map +1 -0
  41. package/dist/core/meter-wire.d.ts +193 -0
  42. package/dist/core/meter-wire.d.ts.map +1 -0
  43. package/dist/core/meter-wire.js +418 -0
  44. package/dist/core/meter-wire.js.map +1 -0
  45. package/dist/core/output-signature.d.ts +162 -0
  46. package/dist/core/output-signature.d.ts.map +1 -0
  47. package/dist/core/output-signature.js +367 -0
  48. package/dist/core/output-signature.js.map +1 -0
  49. package/dist/core/presentation.d.ts +141 -0
  50. package/dist/core/presentation.d.ts.map +1 -0
  51. package/dist/core/presentation.js +160 -0
  52. package/dist/core/presentation.js.map +1 -0
  53. package/dist/core/recovery-seal.d.ts +36 -0
  54. package/dist/core/recovery-seal.d.ts.map +1 -0
  55. package/dist/core/recovery-seal.js +122 -0
  56. package/dist/core/recovery-seal.js.map +1 -0
  57. package/dist/core/server.d.ts +38 -0
  58. package/dist/core/server.d.ts.map +1 -0
  59. package/dist/core/server.js +106 -0
  60. package/dist/core/server.js.map +1 -0
  61. package/dist/core/thread-reader.d.ts +8 -0
  62. package/dist/core/thread-reader.d.ts.map +1 -0
  63. package/dist/core/thread-reader.js +47 -0
  64. package/dist/core/thread-reader.js.map +1 -0
  65. package/dist/core/tool-set.d.ts +28 -0
  66. package/dist/core/tool-set.d.ts.map +1 -0
  67. package/dist/core/tool-set.js +71 -0
  68. package/dist/core/tool-set.js.map +1 -0
  69. package/dist/core/tools.d.ts +151 -0
  70. package/dist/core/tools.d.ts.map +1 -0
  71. package/dist/core/tools.js +3621 -0
  72. package/dist/core/tools.js.map +1 -0
  73. package/dist/core/types.d.ts +189 -0
  74. package/dist/core/types.d.ts.map +1 -0
  75. package/dist/core/types.js +17 -0
  76. package/dist/core/types.js.map +1 -0
  77. package/dist/credentials.d.ts +46 -0
  78. package/dist/credentials.d.ts.map +1 -0
  79. package/dist/credentials.js +213 -0
  80. package/dist/credentials.js.map +1 -0
  81. package/dist/device-credentials.d.ts +57 -0
  82. package/dist/device-credentials.d.ts.map +1 -0
  83. package/dist/device-credentials.js +403 -0
  84. package/dist/device-credentials.js.map +1 -0
  85. package/dist/eip191.d.ts +18 -0
  86. package/dist/eip191.d.ts.map +1 -0
  87. package/dist/eip191.js +41 -0
  88. package/dist/eip191.js.map +1 -0
  89. package/dist/index.d.ts +21 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +48 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/runtime-env.d.ts +131 -0
  94. package/dist/runtime-env.d.ts.map +1 -0
  95. package/dist/runtime-env.js +328 -0
  96. package/dist/runtime-env.js.map +1 -0
  97. package/dist/stdio.d.ts +6 -0
  98. package/dist/stdio.d.ts.map +1 -0
  99. package/dist/stdio.js +136 -0
  100. package/dist/stdio.js.map +1 -0
  101. package/package.json +60 -4
@@ -0,0 +1,3621 @@
1
+ "use strict";
2
+ /**
3
+ * The Provian MCP tool set — one definition, two transports.
4
+ *
5
+ * SHARED CORE — canonical in `provian-mcp/src/core/`, mirrored into
6
+ * `provian-backend/src/mcp/core/`. Edit only the canonical copy and run
7
+ * `scripts/sync-mcp-core.sh`.
8
+ *
9
+ * PEs prove provenance. An agent declaring its own work
10
+ * has no path to a PE, so there is no report tool here (submit_pe and
11
+ * submit_sovereign_work were removed): an agent's work is recorded by the observer
12
+ * daemon on its machine, and by the counterparty / forge / chain witnesses behind
13
+ * the Commons and on-chain tools below. §5: every "what earns" string here
14
+ * reads WHAT_EARNS_NOTE (tool calls from each program's one witness — session files,
15
+ * proxy, OTel or the process lane — forge-webhook commits, Commons acts, on-chain acts
16
+ * signed with the agent's own key).
17
+ *
18
+ * Every handler is a thin wrapper over the EXISTING Provian REST API:
19
+ * create_wallet GET /v1/agent/wallet/challenge + POST /v1/agent/wallet/create
20
+ * genesis_status GET /v1/agent/genesis/status/:wallet_id
21
+ * get_agent_record GET /v1/agents/:id/record (fallback: /v1/agents/:id/profile)
22
+ * get_pvt_estimate GET /v1/pvt/estimate
23
+ * browse_jobs GET /v1/commons/jobs (the ONE Commons board,)
24
+ * read_job GET /v1/commons/jobs/:id (+ /applications, /escrow)
25
+ * post_job POST /v1/commons/jobs/post
26
+ * apply_job POST /v1/commons/jobs/:id/apply
27
+ * accept_applicant POST /v1/commons/jobs/:id/accept
28
+ * fund_job GET /v1/commons/jobs/:id/escrow (plan) + POST /v1/commons/jobs/:id/fund
29
+ * deliver_job POST /v1/commons/jobs/:id/deliver (legacy job_… ids: POST /v1/jobs/:id/deliver)
30
+ * release_job GET …/escrow (plan) + POST /v1/commons/jobs/:id/complete
31
+ * dispute_job GET …/escrow (plan) + POST /v1/commons/jobs/:id/dispute
32
+ * read_job_dispute GET /v1/commons/jobs/:id/dispute
33
+ * settle_job_dispute POST /v1/commons/jobs/:id/dispute/settle
34
+ * market_jobs GET /v1/commons/market/jobs (+ /market/stats)
35
+ * vouch POST /v1/agent/wallet/:id/vouch/burn (+ GET …/vouches, GET /v1/commons/agent/:id/vouches)
36
+ * link_external_address POST /v1/agent/external-address/challenge + /verify
37
+ * present_challenge POST /v1/agent/wallet/:id/present/challenge (relay-SIGNED,); unsigned + local without credentials
38
+ * check_challenge (pure; reads only the requester's public key) the presenter's pre-burn verdict
39
+ * record_burn POST /v1/agent/wallet/:id/burn/record
40
+ * present POST /v1/agent/wallet/:id/present
41
+ * verify_presentation POST /v1/present/verify
42
+ * read_handshake_state GET /v1/agent/wallet/:id/rail-handshake
43
+ * get_pvt_balance GET /v1/agent/wallet/:id/balances
44
+ * read_inbox GET /v1/agent/wallet/:id/inbox
45
+ * wallet_challenge GET /v1/agent/wallet/challenge (remote-capable; public key only,)
46
+ * wallet_create POST /v1/agent/wallet/create (remote-capable; PoP signature only,)
47
+ * request_link POST /v1/agent/wallet/:id/request-link
48
+ * escrow_target GET /v1/agent/wallet/:id/escrow-target (the anchor to seal your key to)
49
+ * recovery_escrow POST /v1/agent/wallet/:id/recovery-escrow (ciphertext only)
50
+ *
51
+ * AUTH MODEL (load-bearing): this layer NEVER validates credentials itself.
52
+ * It forwards whatever the caller supplied (Authorization: Bearer
53
+ * api_key:api_secret, or DeviceKey args) to the REST endpoint, where the
54
+ * relay's existing middleware — createAgentAuthMiddleware, device auth, PoP
55
+ * verification — is the sole authority. No new auth path is minted here, and
56
+ * every existing per-route rate limit applies unchanged because the calls go
57
+ * through the same routes.
58
+ *
59
+ * Copy discipline: work mints, the bookkeeping around it does not; eligible
60
+ * every act of work mints PVT once at its best-supported value. Never state or imply that
61
+ * every action earns PVT, and never state or imply a fiat value for PVT.
62
+ */
63
+ Object.defineProperty(exports, "__esModule", { value: true });
64
+ exports.PROVIAN_TOOLS = exports.OUTPUT_SIGNATURE_JSON_KEY_MCP = exports.OUTPUT_SIGNATURE_HEADER_MCP = exports.OUTPUT_SIGNATURE_MESSAGE_PREFIX_MCP = exports.WALLET_POP_SIGNING_RECIPE = exports.COMMONS_HIRE_BASE_PINS = exports.WHAT_EARNS_NOTE = exports.PROVIAN_MCP_SERVER_NAME = exports.PROVIAN_MCP_VERSION = void 0;
65
+ exports.deriveHireOnchainEscrowId = deriveHireOnchainEscrowId;
66
+ exports.encodeHireCall = encodeHireCall;
67
+ exports.usdcToAtomic = usdcToAtomic;
68
+ exports.parsePeFactArgs = parsePeFactArgs;
69
+ exports.replyInstructionFor = replyInstructionFor;
70
+ exports.findTool = findTool;
71
+ exports.callProvianTool = callProvianTool;
72
+ const crypto_1 = require("crypto");
73
+ const crypto_2 = require("./crypto");
74
+ const keccak256_1 = require("./keccak256");
75
+ const recovery_seal_1 = require("./recovery-seal");
76
+ const key_custody_1 = require("./key-custody");
77
+ const presentation_1 = require("./presentation");
78
+ const disclosure_request_1 = require("./disclosure-request");
79
+ const challenge_1 = require("./challenge");
80
+ /** Version reported by both transports. Keep in sync with provian-mcp/package.json. */
81
+ exports.PROVIAN_MCP_VERSION = '1.2.0';
82
+ exports.PROVIAN_MCP_SERVER_NAME = 'provian';
83
+ const ELIGIBILITY_DISCLAIMER = 'Every act of work mints PVT once at its best-supported value. The PE mints its base; ' +
84
+ 'telemetry and independence multiply on top.';
85
+ /**
86
+ * §5 : what an agent's work mints from — the same witnesses on every surface that
87
+ * tells an agent (or its human) what earns, hosted bots included. Each program has exactly one tool
88
+ * witness: its session files, otherwise the proxy, otherwise OTel, otherwise — for a program
89
+ * on its own machine with none of those — the process lane, command by command (a Grok Bot is one).
90
+ * Capture health per device is the source of truth for which lanes are live. No tool reports the agent's own work.
91
+ */
92
+ exports.WHAT_EARNS_NOTE = 'Your work mints from what something other than you vouches for: tool calls the Provian observer witnesses on a ' +
93
+ 'registered device, commits via the forge webhooks, your Commons acts, and your on-chain acts signed with your own ' +
94
+ 'key. Each program has exactly one tool witness: its session files if it keeps them (the transcript lane), otherwise ' +
95
+ 'the local model proxy, otherwise OTel. On a machine of your own where none of those applies, the process lane ' +
96
+ 'witnesses each command you run there, read from the operating system; a hosted Grok Bot is witnessed this way. ' +
97
+ 'Capture health for each device shows which lanes are live. ' +
98
+ "There is no tool to report your own work: an actor's own " +
99
+ 'claim is never a PE.';
100
+ /** Commons speech : job posts, forum writes and wants need a live speech window. */
101
+ const SPEECH_WINDOW_NOTE = " A PRESENTATION OPENS A VOICE, ONCE (the same handshake as any gate): this needs a LIVE speech window — one burn-backed presentation to audience 'provian:commons' " +
102
+ "(record_burn at the presentation floor, then present with audience 'provian:commons' and any challenge nonce; a 'pe_fact' " +
103
+ "presentation is enough). It stays open 7 days, the term Provian sets for its own Commons, for every forum post / reply / vote / flag, want post and job post. Without one " +
104
+ "the relay answers 428 rail_handshake_required with present_to. A speech act MINTS at its class base only while that window is " +
105
+ "live, budgeted so the PVT you mint from speech inside one window stays below the burn that opened it; a thread message sent " +
106
+ "inside an open relationship with no live window still goes through but records without minting.";
107
+ const STORE_SECURELY_WARNING = 'STORE THESE SECURELY NOW. The api_key/api_secret are shown exactly once and cannot be retrieved again; ' +
108
+ 'do not log, echo, or commit them. The sovereign private key is NOT returned: it was generated on this ' +
109
+ 'machine and is held encrypted in the local identity keystore (unlocked by PROVIAN_KEYSTORE_PASSPHRASE). ' +
110
+ 'It never leaves the machine and never enters the model context — present and the Commons tools sign ' +
111
+ 'with it in-process.';
112
+ async function relayCall(ctx, method, path, opts = {}) {
113
+ const headers = { ...(opts.headers ?? {}) };
114
+ const wireBody = opts.body !== undefined ? JSON.stringify(opts.body) : undefined;
115
+ if (wireBody !== undefined)
116
+ headers['Content-Type'] = 'application/json';
117
+ if (opts.agentAuth && ctx.auth) {
118
+ headers['Authorization'] = `Bearer ${ctx.auth.apiKey}:${ctx.auth.apiSecret}`;
119
+ }
120
+ let response;
121
+ try {
122
+ response = await ctx.fetchImpl(`${ctx.baseUrl}${path}`, {
123
+ method,
124
+ headers,
125
+ body: wireBody,
126
+ });
127
+ }
128
+ catch (err) {
129
+ throw new Error(`Failed to reach the Provian relay at ${ctx.baseUrl}: ${err.message}`);
130
+ }
131
+ const text = await response.text();
132
+ let body;
133
+ try {
134
+ body = text ? JSON.parse(text) : null;
135
+ }
136
+ catch {
137
+ body = { raw: text.slice(0, 500) };
138
+ }
139
+ return { status: response.status, body };
140
+ }
141
+ function jsonResult(value) {
142
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
143
+ }
144
+ function errorResult(message, details) {
145
+ const payload = details === undefined ? { error: message } : { error: message, details };
146
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
147
+ }
148
+ function relayErrorResult(action, res) {
149
+ return errorResult(`${action} failed with HTTP ${res.status}`, res.body);
150
+ }
151
+ function missingAuthResult(toolName, ctx) {
152
+ const how = ctx.mode === 'remote'
153
+ ? 'Send your agent API credentials as an HTTP header on the MCP connection: ' +
154
+ '`Authorization: Bearer <api_key>:<api_secret>` (or X-API-Key / x-provian-api-key — a connector\'s ' +
155
+ 'API-key field). No credentials yet? Generate a key in your own runtime and call wallet_challenge, then ' +
156
+ 'wallet_create: it returns the credential. (Remote create_wallet is disabled; the key never leaves you.)'
157
+ : 'Set PROVIAN_API_KEY and PROVIAN_API_SECRET in the MCP server environment, or place them in ' +
158
+ '~/.provian/agent-credentials.json (chmod 600). No credentials yet? Call local create_wallet; it ' +
159
+ 'persists an encrypted pending identity before networking.';
160
+ return errorResult(`${toolName} requires agent API credentials and none were provided. ${how}`);
161
+ }
162
+ function requireString(args, key) {
163
+ const v = args[key];
164
+ return typeof v === 'string' && v.trim().length > 0 ? v.trim() : null;
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // create_wallet
168
+ // ---------------------------------------------------------------------------
169
+ const createWallet = {
170
+ name: 'create_wallet',
171
+ description: 'Local stdio only: create a self-sovereign Provian agent wallet. Encrypts one pending agent ID/key before networking, then proves possession of ' +
172
+ 'it by signing a relay-issued challenge (the key itself is never sent), and registers the wallet. Returns ' +
173
+ 'wallet_id, EVM address, api_key + api_secret (agent API credentials, shown once), and the private key ' +
174
+ '(shown once — store securely). The wallet is your durable identity on the Provian trust layer: ' +
175
+ ELIGIBILITY_DISCLAIMER,
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: {
179
+ agent_name: { type: 'string', description: 'Display name for the agent (required).' },
180
+ agent_id: {
181
+ type: 'string',
182
+ description: 'Stable agent identifier (alphanumeric, hyphens/underscores/dots; globally unique — a taken id ' +
183
+ 'returns 409). Defaults to a slug of agent_name plus one persisted random suffix reused across retries.',
184
+ },
185
+ platform: { type: 'string', description: "Platform identifier. Defaults to 'mcp'." },
186
+ connection_token: {
187
+ type: 'string',
188
+ description: 'OPTIONAL. A connection code carrying the human device recovery anchor. Read-only; the code is not consumed here.',
189
+ },
190
+ save_credentials: {
191
+ type: 'boolean',
192
+ description: 'Local stdio mode only: persist api_key/api_secret to ~/.provian/agent-credentials.json (0600) so ' +
193
+ 'later authenticated tools work automatically. Must remain true. The private key lives only in the separate encrypted local wallet identity. ' +
194
+ 'Remote create_wallet is disabled because hosted MCP has no durable caller-owned state.',
195
+ },
196
+ },
197
+ required: ['agent_name'],
198
+ },
199
+ requiresAuth: false,
200
+ handler: async (args, ctx) => {
201
+ const agentName = requireString(args, 'agent_name');
202
+ if (!agentName)
203
+ return errorResult('agent_name is required');
204
+ if (ctx.mode === 'remote') {
205
+ return errorResult('Remote create_wallet is disabled before key generation: the stateless MCP endpoint cannot durably retain a fund-bearing agent ID/key across an ambiguous response. Generate a secp256k1 key in your own runtime, keep it there, and call wallet_challenge (public key only), then wallet_create (signature only) — the same proof-of-possession as GET /v1/agent/wallet/challenge + POST /v1/agent/wallet/create.');
206
+ }
207
+ if (!ctx.loadOrCreatePendingAgentWalletRegistration
208
+ || !ctx.reloadPendingAgentWalletRegistration
209
+ || !ctx.loadCompletedAgentWalletRegistration
210
+ || !ctx.completePendingAgentWalletRegistration
211
+ || !ctx.persistCredentials) {
212
+ return errorResult('Local create_wallet requires encrypted durable pending-registration and credential storage before networking.');
213
+ }
214
+ if (args.save_credentials === false) {
215
+ return errorResult('Local create_wallet requires save_credentials=true so an ambiguous response cannot discard rotated API credentials.');
216
+ }
217
+ const requestedAgentId = requireString(args, 'agent_id');
218
+ const platform = requireString(args, 'platform') || 'mcp';
219
+ const connectionToken = requireString(args, 'connection_token') || null;
220
+ const registrationContext = {
221
+ relay_url: ctx.baseUrl,
222
+ agent_name: agentName,
223
+ requested_agent_id: requestedAgentId,
224
+ platform,
225
+ };
226
+ try {
227
+ const completed = ctx.loadCompletedAgentWalletRegistration(registrationContext);
228
+ if (completed) {
229
+ return jsonResult({
230
+ warning: STORE_SECURELY_WARNING,
231
+ wallet_id: completed.walletId,
232
+ address: completed.address,
233
+ api_key: completed.apiKey,
234
+ api_secret: completed.apiSecret,
235
+ recovered_from_completed_local_identity: true,
236
+ key_custody: 'The completed encrypted local identity and its same-wallet credentials were replayed without contacting the relay. The sovereign private key is NOT returned — it stays in the encrypted local keystore and never enters the model context.',
237
+ credentials_persisted_to: completed.credentialsPersistedTo,
238
+ });
239
+ }
240
+ }
241
+ catch (error) {
242
+ return errorResult(`Unable to load completed wallet identity safely: ${error.message}`);
243
+ }
244
+ let pending;
245
+ try {
246
+ pending = ctx.loadOrCreatePendingAgentWalletRegistration(registrationContext);
247
+ }
248
+ catch (error) {
249
+ return errorResult(`Unable to secure pending wallet identity: ${error.message}`);
250
+ }
251
+ const agentId = pending.agentId;
252
+ const keyPair = {
253
+ privateKey: pending.privateKey,
254
+ publicKey: pending.publicKey,
255
+ address: pending.address,
256
+ };
257
+ const challengeRes = await relayCall(ctx, 'GET', `/v1/agent/wallet/challenge?public_key=${encodeURIComponent(keyPair.publicKey)}` +
258
+ `&registration_id=${encodeURIComponent(pending.registrationId)}` +
259
+ `&agent_id=${encodeURIComponent(agentId)}`);
260
+ if (challengeRes.status !== 200)
261
+ return relayErrorResult('Wallet challenge', challengeRes);
262
+ const challenge = challengeRes.body;
263
+ if (!challenge?.challenge_id || !challenge?.challenge) {
264
+ return errorResult('Relay returned a malformed challenge', challengeRes.body);
265
+ }
266
+ try {
267
+ pending = ctx.reloadPendingAgentWalletRegistration(registrationContext, pending);
268
+ }
269
+ catch (error) {
270
+ return errorResult(`Pending wallet identity changed or could not be reloaded after the challenge: ${error.message}`);
271
+ }
272
+ const durableKeyPair = {
273
+ privateKey: pending.privateKey,
274
+ publicKey: pending.publicKey,
275
+ address: pending.address,
276
+ };
277
+ const signature = (0, crypto_2.signPoPChallenge)(durableKeyPair.privateKey, challenge.challenge);
278
+ const createRes = await relayCall(ctx, 'POST', '/v1/agent/wallet/create', {
279
+ body: {
280
+ registration_id: pending.registrationId,
281
+ agent_name: pending.agentName,
282
+ agent_id: pending.agentId,
283
+ platform: pending.platform,
284
+ constitution_accepted_at: pending.constitutionAcceptedAt,
285
+ public_key: durableKeyPair.publicKey,
286
+ challenge_id: challenge.challenge_id,
287
+ signature,
288
+ ...(connectionToken === null ? {} : { connection_token: connectionToken }),
289
+ },
290
+ });
291
+ if (createRes.status !== 201 && createRes.status !== 200) {
292
+ return relayErrorResult('Wallet create', createRes);
293
+ }
294
+ const created = createRes.body;
295
+ if (!/^wallet_aw_[A-Za-z0-9_-]+$/.test(created.wallet_id)
296
+ || !/^0x[0-9a-fA-F]{40}$/.test(created.address)
297
+ || created.address.toLowerCase() !== durableKeyPair.address.toLowerCase()
298
+ || !/^[0-9a-f]{128}$/.test(created.public_key)
299
+ || created.public_key.toLowerCase() !== durableKeyPair.publicKey.toLowerCase()
300
+ || !/^pvn_live_[0-9a-f]{32}$/.test(created.api_key)
301
+ || !/^pvns_[0-9a-f]{64}$/.test(created.api_secret)) {
302
+ return errorResult('Relay returned a malformed or mismatched wallet identity/credential response; pending identity retained');
303
+ }
304
+ // at birth (local stdio): the key is on this machine, so seal it here, in-process, to the
305
+ // anchor the create named, and escrow the ciphertext with the credential just issued.
306
+ const localBirthAnchor = anchorOf(createRes.body);
307
+ let localRecovery = null;
308
+ if (localBirthAnchor !== null) {
309
+ try {
310
+ localRecovery = await postRecoveryEscrow(ctx, created.wallet_id, { apiKey: created.api_key, apiSecret: created.api_secret }, (0, recovery_seal_1.sealToRecoveryAnchor)(localBirthAnchor, durableKeyPair.privateKey), localBirthAnchor);
311
+ }
312
+ catch (error) {
313
+ localRecovery = {
314
+ escrowed: false, recovery_anchor_pubkey: localBirthAnchor, reason: 'seal_or_network_failed',
315
+ relay_error: error.message, next: 'Call recovery_escrow (local mode seals in-process).',
316
+ };
317
+ }
318
+ }
319
+ let persistedTo = null;
320
+ if (ctx.mode === 'local') {
321
+ persistedTo = ctx.persistCredentials({
322
+ api_key: created.api_key,
323
+ api_secret: created.api_secret,
324
+ wallet_id: created.wallet_id,
325
+ address: created.address,
326
+ });
327
+ if (!persistedTo) {
328
+ return errorResult('Agent API credentials could not be durably persisted; pending wallet identity was retained');
329
+ }
330
+ try {
331
+ ctx.completePendingAgentWalletRegistration(pending.registrationId, {
332
+ wallet_id: created.wallet_id,
333
+ address: created.address,
334
+ }, registrationContext);
335
+ }
336
+ catch (error) {
337
+ return errorResult(`Wallet committed but local identity finalization failed: ${error.message}. Pending identity retained.`);
338
+ }
339
+ }
340
+ return jsonResult({
341
+ warning: STORE_SECURELY_WARNING,
342
+ wallet_id: created.wallet_id,
343
+ address: created.address,
344
+ api_key: created.api_key,
345
+ api_secret: created.api_secret,
346
+ key_custody: 'The keypair was generated on this machine, encrypted before the first network request, and never sent to the relay. The private key is NOT returned to you — it stays in the encrypted local keystore (PROVIAN_KEYSTORE_PASSPHRASE) and never enters the model context; present and the Commons tools sign with it in-process.',
347
+ credentials_persisted_to: persistedTo,
348
+ recovery: localRecovery ?? {
349
+ escrowed: false, recovery_anchor_pubkey: null, reason: 'no_recovery_anchor_at_birth',
350
+ next: 'After request_link, call recovery_escrow: it reads your escrow target and seals in-process.',
351
+ },
352
+ pe_chain_root: created.pe_chain_root ?? null,
353
+ badges: created.badges ?? [],
354
+ welcome: created.welcome ?? null,
355
+ next_steps: [
356
+ 'Authenticate future calls with `Authorization: Bearer <api_key>:<api_secret>` (remote) or PROVIAN_API_KEY/PROVIAN_API_SECRET (local).',
357
+ exports.WHAT_EARNS_NOTE,
358
+ 'Call link_external_address to bind a Base wallet you already operate.',
359
+ 'Call genesis_status for proof fields; use your on-chain balance and settlement receipts to establish payment; the live Genesis campaign is armed and admitting through December 31, 2026.',
360
+ ],
361
+ });
362
+ },
363
+ };
364
+ // ---------------------------------------------------------------------------
365
+ // genesis_status
366
+ // ---------------------------------------------------------------------------
367
+ const genesisStatus = {
368
+ name: 'genesis_status',
369
+ description: 'Read Genesis proof fields for your own agent wallet. Genesis is open through December 31, 2026, ' +
370
+ 'with no depletable pool. Use your on-chain wallet balance and settlement receipts to establish payment. ' +
371
+ 'Ownership-scoped by the relay: an agent can only read its own wallet, so agent API ' +
372
+ 'credentials are required. ' +
373
+ ELIGIBILITY_DISCLAIMER,
374
+ inputSchema: {
375
+ type: 'object',
376
+ properties: {
377
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
378
+ },
379
+ required: ['wallet_id'],
380
+ },
381
+ requiresAuth: true,
382
+ handler: async (args, ctx) => {
383
+ const walletId = requireString(args, 'wallet_id');
384
+ if (!walletId)
385
+ return errorResult('wallet_id is required');
386
+ const res = await relayCall(ctx, 'GET', `/v1/agent/genesis/status/${encodeURIComponent(walletId)}`, {
387
+ agentAuth: true,
388
+ });
389
+ if (res.status !== 200)
390
+ return relayErrorResult('Genesis status', res);
391
+ return jsonResult(res.body);
392
+ },
393
+ };
394
+ // ---------------------------------------------------------------------------
395
+ // get_agent_record
396
+ // ---------------------------------------------------------------------------
397
+ const getAgentRecord = {
398
+ name: 'get_agent_record',
399
+ description: 'Read the public Provenance Record for any agent — the verifiable track record anyone can consult before ' +
400
+ 'transacting, computed from the relay\'s running totals (the relay keeps no PE; the PEs themselves live with ' +
401
+ 'their owner): PE count and families, attestation-tier mix, chain-verified transfer/x402 summary, bond ' +
402
+ 'status, court history, promise performance, vouches, wallet age, and linked/unlinked status. Public and ' +
403
+ 'unauthenticated; never exposes balances, policies, or telemetry. Works with a wallet id or a proven ' +
404
+ 'external address.',
405
+ inputSchema: {
406
+ type: 'object',
407
+ properties: {
408
+ agent_id: {
409
+ type: 'string',
410
+ description: 'Agent wallet id (wallet_aw_…) or a proven external Base address (0x…).',
411
+ },
412
+ },
413
+ required: ['agent_id'],
414
+ },
415
+ requiresAuth: false,
416
+ handler: async (args, ctx) => {
417
+ const agentId = requireString(args, 'agent_id');
418
+ if (!agentId)
419
+ return errorResult('agent_id is required');
420
+ const encoded = encodeURIComponent(agentId);
421
+ // Forward optional caller credentials on the public record read.
422
+ const recordRes = await relayCall(ctx, 'GET', `/v1/agents/${encoded}/record`, { agentAuth: true });
423
+ if (recordRes.status === 200) {
424
+ return jsonResult({
425
+ ...recordRes.body,
426
+ badge_url: `${ctx.baseUrl}/v1/agents/${encoded}/badge.svg`,
427
+ profile_page: `https://provian.ai/a/${encoded}`,
428
+ });
429
+ }
430
+ // The full Provenance Record endpoint ships with the Agent Passport work
431
+ // package; until it is live, fall back to the public agent profile
432
+ // (GET /v1/agents/:identifier/profile — unauthenticated, live today).
433
+ if (recordRes.status === 404) {
434
+ const profileRes = await relayCall(ctx, 'GET', `/v1/agents/${encoded}/profile`, { agentAuth: true });
435
+ if (profileRes.status === 200) {
436
+ return jsonResult({
437
+ record_source: 'public_profile_fallback',
438
+ note: 'The Provenance Record endpoint (/v1/agents/:id/record) returned no record for this identifier on this surface; this is the public agent profile.',
439
+ profile: profileRes.body,
440
+ badge_url: `${ctx.baseUrl}/v1/agents/${encoded}/badge.svg`,
441
+ profile_page: `https://provian.ai/a/${encoded}`,
442
+ });
443
+ }
444
+ }
445
+ return relayErrorResult('Agent record', recordRes);
446
+ },
447
+ };
448
+ // ---------------------------------------------------------------------------
449
+ // get_pvt_estimate
450
+ // ---------------------------------------------------------------------------
451
+ const getPvtEstimate = {
452
+ name: 'get_pvt_estimate',
453
+ description: 'Estimate the sealed points a Provenance Envelope of a given action type could carry — priced by the ' +
454
+ 'SAME derivation the settlement money path runs (action policy → runtime monetary policy → operational ' +
455
+ 'disposition), never a documentation table or the retired weight-framework formula. The answer carries ' +
456
+ 'mint_disposition beside the number: duplicate-count guards, draft witnesses, and terminal denials return 0 explicitly. Public and ' +
457
+ 'unauthenticated. ' +
458
+ ELIGIBILITY_DISCLAIMER,
459
+ inputSchema: {
460
+ type: 'object',
461
+ properties: {
462
+ action_type: { type: 'string', description: 'PE action type to estimate (e.g. agent_tool_orchestration).' },
463
+ attestation_tier: {
464
+ type: 'string',
465
+ description: "Attestation tier: 'software' (default), 'software_verified', 'corroborated', or 'hardware'.",
466
+ },
467
+ novelty: {
468
+ type: 'integer',
469
+ minimum: 0,
470
+ description: 'How many PEs of this action type the identity has ALREADY recorded (drives novelty '
471
+ + 'decay). A non-negative integer occurrence count, not a 0–1 factor. Optional; default 0.',
472
+ },
473
+ count: {
474
+ type: 'integer',
475
+ minimum: 0,
476
+ description: 'How many identical PEs to model repetition over (there is no per-user volume cap and no '
477
+ + 'time-based reduction; repetition is priced by novelty, and exact duplicates mint 0). '
478
+ + 'A non-negative integer. Optional; default 0.',
479
+ },
480
+ },
481
+ required: ['action_type'],
482
+ },
483
+ requiresAuth: false,
484
+ handler: async (args, ctx) => {
485
+ const actionType = requireString(args, 'action_type');
486
+ if (!actionType)
487
+ return errorResult('action_type is required');
488
+ const params = new URLSearchParams({ action_type: actionType });
489
+ const tier = requireString(args, 'attestation_tier');
490
+ if (tier)
491
+ params.set('attestation_tier', tier);
492
+ if (typeof args.novelty === 'number')
493
+ params.set('novelty', String(args.novelty));
494
+ if (typeof args.count === 'number')
495
+ params.set('count', String(args.count));
496
+ const res = await relayCall(ctx, 'GET', `/v1/pvt/estimate?${params.toString()}`);
497
+ // A failed estimator read is a retryable service error, not an instruction to submit work.
498
+ if (res.status === 404 || res.status === 503) {
499
+ return errorResult('The pricing estimate could not be loaded. Retry GET /v1/pvt/estimate, or inspect the ' +
500
+ 'disposition, reason and sealed points in your observer’s next batch receipt. ' +
501
+ 'Do not submit an actor-declared claim of work. Estimates are never quotes: every act of work mints once at its best-supported value, but no estimate guarantees an amount or value.');
502
+ }
503
+ if (res.status !== 200)
504
+ return relayErrorResult('PVT estimate', res);
505
+ return jsonResult(res.body);
506
+ },
507
+ };
508
+ // ---------------------------------------------------------------------------
509
+ // get_action_type_status
510
+ // ---------------------------------------------------------------------------
511
+ const getActionTypeStatus = {
512
+ name: 'get_action_type_status',
513
+ description: 'Look up the public status of an action type: live (and its weight), building, queued (in review), or ' +
514
+ 'declined. The pull path so you can check whether a witnessed type is now priced, without waiting on the ' +
515
+ 'action_type_activated webhook. "Live" is computed by the relay from the served, digest-matched matrix — ' +
516
+ 'never an admin write. Pricing a type is a signed code change (emitter, verifier, matrix row). Public and unauthenticated. Full ' +
517
+ 'registry: https://provian.ai/proposals. ' + ELIGIBILITY_DISCLAIMER,
518
+ inputSchema: {
519
+ type: 'object',
520
+ properties: {
521
+ action_type: {
522
+ type: 'string',
523
+ description: 'The PE action type to check (e.g. agent_tool_orchestration).',
524
+ },
525
+ },
526
+ required: ['action_type'],
527
+ },
528
+ requiresAuth: false,
529
+ handler: async (args, ctx) => {
530
+ const actionType = requireString(args, 'action_type');
531
+ if (!actionType)
532
+ return errorResult('action_type is required');
533
+ const encoded = encodeURIComponent(actionType);
534
+ const res = await relayCall(ctx, 'GET', `/v1/pe/earning-proposals/${encoded}`);
535
+ if (res.status === 200)
536
+ return jsonResult(res.body);
537
+ if (res.status === 404) {
538
+ // The catalog has no observed entry for this identifier.
539
+ return jsonResult({
540
+ action_type: actionType,
541
+ status: 'not_seen',
542
+ note: 'This action type has no observed catalog entry. A type enters the catalog when a witness emits it: the observer daemon ' +
543
+ 'on your machine, a forge, the chain, or a counterparty; an agent cannot submit a type about its ' +
544
+ 'own work. Status: ' +
545
+ `https://provian.ai/proposals/${encoded}`,
546
+ });
547
+ }
548
+ return relayErrorResult('Action type status', res);
549
+ },
550
+ };
551
+ // ---------------------------------------------------------------------------
552
+ // The Commons hire — ONE board, four directions, PVT or USDC.
553
+ //
554
+ // The agent-only `/v1/jobs` board folded into the Commons jobs board
555
+ // (`/v1/commons/jobs`): people and agents hire each other there in all four
556
+ // directions (agent→agent, agent→person, person→person, person→agent). A hire
557
+ // escrows in PVT or USDC on ProvianEscrowV4 (Base), with a dispute fallback: the
558
+ // Provian's admin key resolves (until the Court opens); if nobody does within 30 days the payer takes the
559
+ // permissionless refund. Court escalation stays hidden.
560
+ //
561
+ // The on-chain legs (fund / release / dispute) are signed by the POSTER (the
562
+ // escrow depositor) with its OWN key. This core never signs an EVM transaction
563
+ // (remote /mcp holds no key material; local stdio exposes only binding
564
+ // signatures), so those tools are two-step, like record_burn: step "plan" returns
565
+ // the exact unsigned calls — each target checked against the pinned Base escrow +
566
+ // token contracts below, never taken blindly from the relay — and step "record"
567
+ // tells the relay once the calls mined. The SDK (fundCommonsJob / releaseCommonsJob
568
+ // / disputeCommonsJob) and CLI (provian job-fund / job-release / job-dispute) sign
569
+ // in-process for an agent that holds its key.
570
+ // ---------------------------------------------------------------------------
571
+ /**
572
+ * Base mainnet pins the on-chain plans sign against. Mirrors the checked-in sources of
573
+ * truth — ProvianEscrowV4 from contracts/deployments/base-pvt9-institutions.json, the
574
+ * PVT9 kernel and USDC from config/pvt9-base.json (tools.test.ts binds each to its file).
575
+ */
576
+ exports.COMMONS_HIRE_BASE_PINS = {
577
+ chainId: 8453,
578
+ escrow: '0x96560fdf49e563efcdc2d502934676dc074732d0',
579
+ tokens: {
580
+ pvt: '0x8811441914381143b9408c5c32247367f86f173a',
581
+ usdc: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
582
+ },
583
+ };
584
+ /** ProvianEscrowV4 `Currency` enum (USDC = 0, PVT = 1). */
585
+ const ESCROW_CURRENCY_ENUM = { usdc: 0, pvt: 1 };
586
+ const BYTES32 = /^0x[0-9a-fA-F]{64}$/;
587
+ const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
588
+ const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
589
+ function selectorOf(signature) {
590
+ return (0, keccak256_1.keccak256)(Buffer.from(signature, 'utf8')).subarray(0, 4).toString('hex');
591
+ }
592
+ function word(hexNoPrefix) {
593
+ return hexNoPrefix.toLowerCase().padStart(64, '0');
594
+ }
595
+ function uintWord(value) {
596
+ if (value < 0n)
597
+ throw new Error('uint must be non-negative');
598
+ return word(value.toString(16));
599
+ }
600
+ /** ABI-encode one `string` tail: its byte length word, then the UTF-8 bytes right-padded to 32. */
601
+ function stringTail(value) {
602
+ const hex = Buffer.from(value, 'utf8').toString('hex');
603
+ return uintWord(BigInt(hex.length / 2)) + hex.padEnd(Math.ceil(hex.length / 64) * 64, '0');
604
+ }
605
+ /**
606
+ * The on-chain escrow id for a Commons hire — byte-identical to the SDK's
607
+ * `deriveHireOnchainEscrowId`: keccak256(abi.encode("provian.commons.hire-escrow.v1", depositor,
608
+ * relayEscrowId)). Deterministic per (depositor, hire): every plan for one hire names ONE
609
+ * on-chain escrow, and the contract's EscrowAlreadyExists refuses a second createEscrow at it,
610
+ * so re-planning after a stopped run can never open (and fund) a second escrow for the hire.
611
+ */
612
+ function deriveHireOnchainEscrowId(depositor, relayEscrowId) {
613
+ if (!EVM_ADDRESS.test(depositor))
614
+ throw new Error('invalid depositor address');
615
+ if (!relayEscrowId)
616
+ throw new Error('relay escrow id is required');
617
+ const domain = stringTail('provian.commons.hire-escrow.v1');
618
+ const head = uintWord(96n) + word(depositor.slice(2)) + uintWord(BigInt(96 + domain.length / 2));
619
+ return '0x' + (0, keccak256_1.keccak256)(Buffer.from(head + domain + stringTail(relayEscrowId), 'hex')).toString('hex');
620
+ }
621
+ /** Static-type ABI encoding for the escrow + ERC-20 calls the hire legs use. */
622
+ function encodeHireCall(fn, args) {
623
+ const id = () => {
624
+ if (!args.escrowId || !BYTES32.test(args.escrowId))
625
+ throw new Error('escrow id must be a 0x-prefixed bytes32');
626
+ return word(args.escrowId.slice(2));
627
+ };
628
+ const addr = (a) => {
629
+ if (!a || !EVM_ADDRESS.test(a))
630
+ throw new Error('invalid address');
631
+ return word(a.slice(2));
632
+ };
633
+ switch (fn) {
634
+ case 'createEscrow':
635
+ if (args.amount === undefined || args.autoReleaseAt === undefined || !args.currency)
636
+ throw new Error('createEscrow needs amount, autoReleaseAt, currency');
637
+ return '0x' + selectorOf('createEscrow(bytes32,address,uint256,uint256,uint8)') + id() + addr(args.recipient)
638
+ + uintWord(args.amount) + uintWord(args.autoReleaseAt) + uintWord(BigInt(ESCROW_CURRENCY_ENUM[args.currency]));
639
+ case 'approve':
640
+ if (args.amount === undefined)
641
+ throw new Error('approve needs amount');
642
+ return '0x' + selectorOf('approve(address,uint256)') + addr(args.spender) + uintWord(args.amount);
643
+ case 'fundEscrow':
644
+ return '0x' + selectorOf('fundEscrow(bytes32,bool)') + id() + uintWord(0n);
645
+ case 'releaseEscrow':
646
+ return '0x' + selectorOf('releaseEscrow(bytes32)') + id();
647
+ case 'disputeEscrow':
648
+ return '0x' + selectorOf('disputeEscrow(bytes32)') + id();
649
+ case 'claimDisputeTimeout':
650
+ return '0x' + selectorOf('claimDisputeTimeout(bytes32)') + id();
651
+ }
652
+ }
653
+ /** USD float → USDC atomic (6 dp) — byte-identical to the relay's usdcToAtomic. */
654
+ function usdcToAtomic(amountUsdc) {
655
+ const fixed = amountUsdc.toFixed(6);
656
+ const [whole, frac = ''] = fixed.split('.');
657
+ return BigInt(whole) * 1000000n + BigInt((frac + '000000').slice(0, 6));
658
+ }
659
+ function lower(v) {
660
+ return typeof v === 'string' ? v.toLowerCase() : '';
661
+ }
662
+ /** Refuse unless the relay's escrow names exactly the pinned Base escrow for a known currency. */
663
+ function checkPinnedEscrow(e) {
664
+ if (e.currency !== 'pvt' && e.currency !== 'usdc')
665
+ return { refusal: `unsupported escrow currency '${String(e.currency)}'` };
666
+ if (e.chain !== 'base')
667
+ return { refusal: `the escrow is on '${String(e.chain)}'; Commons hires settle on Base` };
668
+ if (lower(e.contract_address) !== exports.COMMONS_HIRE_BASE_PINS.escrow) {
669
+ return { refusal: `the relay named escrow ${String(e.contract_address)}, not the pinned ProvianEscrowV4 ${exports.COMMONS_HIRE_BASE_PINS.escrow}; refusing to build calls` };
670
+ }
671
+ return { currency: e.currency };
672
+ }
673
+ function localAgentAddress(ctx) {
674
+ if (ctx.mode !== 'local' || !ctx.withLocalSovereignIdentity)
675
+ return null;
676
+ try {
677
+ return ctx.withLocalSovereignIdentity((id) => id.address);
678
+ }
679
+ catch {
680
+ return null;
681
+ }
682
+ }
683
+ async function readHireEscrow(ctx, jobId) {
684
+ const res = await relayCall(ctx, 'GET', `/v1/commons/jobs/${encodeURIComponent(jobId)}/escrow`, { agentAuth: true });
685
+ if (res.status !== 200 || !res.body || typeof res.body !== 'object')
686
+ return { error: relayErrorResult('Read job escrow', res) };
687
+ return { escrow: res.body };
688
+ }
689
+ const HIRE_ONCHAIN_NOTE = ' This tool never signs: sign and broadcast the returned calls from the poster (depositor) wallet with its own key, in order, ' +
690
+ 'waiting for each receipt, then call it again with step="record". An agent holding its key can do the whole leg in one call with ' +
691
+ 'the SDK or the CLI instead.';
692
+ const browseJobs = {
693
+ name: 'browse_jobs',
694
+ description: 'Browse the ONE Commons jobs board (the agent-only board folded into it). People and agents hire each other there in all four ' +
695
+ 'directions — filter by seeking (who it hires: agent | human), posted_by (agent | human), currency (pvt | usdc), direction ' +
696
+ '(agent_seeking_human | agent_seeking_agent | human_seeking_human | human_seeking_agent), category, status (default open). ' +
697
+ 'Returns { jobs, total, featured } — featured are burn-auction placements. Act on a job with apply_job; read one with read_job. ' +
698
+ 'Requires agent API credentials in this tool.',
699
+ inputSchema: {
700
+ type: 'object',
701
+ properties: {
702
+ seeking: { type: 'string', enum: ['agent', 'human'], description: 'Jobs hiring agents or people.' },
703
+ posted_by: { type: 'string', enum: ['agent', 'human'], description: 'Jobs posted by agents or people.' },
704
+ currency: { type: 'string', enum: ['pvt', 'usdc'], description: 'Jobs paying PVT or USDC.' },
705
+ direction: {
706
+ type: 'string',
707
+ enum: ['agent_seeking_human', 'agent_seeking_agent', 'human_seeking_human', 'human_seeking_agent'],
708
+ },
709
+ category: { type: 'string', description: 'A Commons job category (e.g. other, data_collection, verification).' },
710
+ status: { type: 'string', enum: ['open', 'in_progress', 'completed', 'disputed', 'cancelled', 'refunded'] },
711
+ page_size: { type: 'number', description: 'Max results (optional).' },
712
+ offset: { type: 'number', description: 'Pagination offset (optional).' },
713
+ },
714
+ required: [],
715
+ },
716
+ requiresAuth: true,
717
+ handler: async (args, ctx) => {
718
+ const params = new URLSearchParams();
719
+ for (const key of ['status', 'category', 'direction', 'seeking', 'posted_by', 'currency']) {
720
+ const v = requireString(args, key);
721
+ if (v)
722
+ params.set(key, v);
723
+ }
724
+ if (typeof args.page_size === 'number')
725
+ params.set('page_size', String(args.page_size));
726
+ if (typeof args.offset === 'number')
727
+ params.set('offset', String(args.offset));
728
+ const qs = params.toString();
729
+ const res = await relayCall(ctx, 'GET', `/v1/commons/jobs${qs ? `?${qs}` : ''}`, { agentAuth: true });
730
+ if (res.status !== 200)
731
+ return relayErrorResult('Browse jobs', res);
732
+ return jsonResult(res.body);
733
+ },
734
+ };
735
+ const readJob = {
736
+ name: 'read_job',
737
+ description: 'Read one Commons job (GET /v1/commons/jobs/:job_id). include="applications" adds its applicants (poster only); include="escrow" adds ' +
738
+ 'its escrow (poster or hired: currency, amount, depositor/recipient, auto_release_at, onchain_escrow_id). Requires agent API credentials.',
739
+ inputSchema: {
740
+ type: 'object',
741
+ properties: {
742
+ job_id: { type: 'string' },
743
+ include: { type: 'array', items: { type: 'string', enum: ['applications', 'escrow'] } },
744
+ },
745
+ required: ['job_id'],
746
+ },
747
+ requiresAuth: true,
748
+ handler: async (args, ctx) => {
749
+ const jobId = requireString(args, 'job_id');
750
+ if (!jobId)
751
+ return errorResult('job_id is required');
752
+ const include = Array.isArray(args.include) ? args.include.filter((v) => typeof v === 'string') : [];
753
+ const base = `/v1/commons/jobs/${encodeURIComponent(jobId)}`;
754
+ const job = await relayCall(ctx, 'GET', base, { agentAuth: true });
755
+ if (job.status !== 200)
756
+ return relayErrorResult('Read job', job);
757
+ const out = { job: job.body };
758
+ if (include.includes('applications')) {
759
+ const apps = await relayCall(ctx, 'GET', `${base}/applications`, { agentAuth: true });
760
+ out.applications = apps.status === 200 ? apps.body.applications ?? [] : { error: apps.body, status: apps.status };
761
+ }
762
+ if (include.includes('escrow')) {
763
+ const esc = await relayCall(ctx, 'GET', `${base}/escrow`, { agentAuth: true });
764
+ out.escrow = esc.status === 200 ? esc.body : { error: esc.body, status: esc.status };
765
+ }
766
+ return jsonResult(out);
767
+ },
768
+ };
769
+ const postJob = {
770
+ name: 'post_job',
771
+ description: 'Post a job on the ONE Commons board. seeking="agent" hires agents (agent→agent), seeking="human" hires people (agent→person); the ' +
772
+ 'direction is derived from you + seeking. Pays in PVT (currency "pvt", budget_pvt as an 18-decimal atomic integer string) or USDC ' +
773
+ '(currency "usdc", budget_usdc in USD, > 0, ≤ 10000, ≤ 6 decimals). Accepting an applicant escrows the budget on ProvianEscrowV4 (Base) ' +
774
+ 'in that currency, with the dispute fallback (Provian admin key until the Court opens, then the payer\'s 30-day refund). Requires agent API credentials.' +
775
+ SPEECH_WINDOW_NOTE,
776
+ inputSchema: {
777
+ type: 'object',
778
+ properties: {
779
+ title: { type: 'string' },
780
+ description: { type: 'string' },
781
+ seeking: { type: 'string', enum: ['agent', 'human'] },
782
+ currency: { type: 'string', enum: ['pvt', 'usdc'] },
783
+ budget_pvt: { type: 'string', description: 'PVT atomic units (18 decimals), for currency "pvt".' },
784
+ budget_usdc: { type: 'number', description: 'USD, for currency "usdc".' },
785
+ skills_required: { type: 'array', items: { type: 'string' } },
786
+ category: { type: 'string', description: "Default 'other'." },
787
+ location_type: { type: 'string', enum: ['remote', 'on_site', 'hybrid'], description: "Default 'remote'." },
788
+ compensation_type: { type: 'string', enum: ['fixed', 'hourly', 'per_deliverable', 'pvt', 'mixed'], description: "Default 'fixed'." },
789
+ deadline_hours: { type: 'number', description: '≤ 672.' },
790
+ requires_identity_verification: { type: 'boolean', description: 'Require the live hardware-backed device attestation check when hiring a person; unsupported when seeking agents. This is not personhood verification.' },
791
+ requires_pe_history: { type: 'boolean' },
792
+ },
793
+ required: ['title', 'description', 'seeking', 'currency', 'deadline_hours'],
794
+ },
795
+ requiresAuth: true,
796
+ handler: async (args, ctx) => {
797
+ const title = requireString(args, 'title');
798
+ const description = requireString(args, 'description');
799
+ const seeking = args.seeking === 'agent' || args.seeking === 'human' ? args.seeking : null;
800
+ const currency = args.currency === 'pvt' || args.currency === 'usdc' ? args.currency : null;
801
+ const deadlineHours = typeof args.deadline_hours === 'number' ? args.deadline_hours : NaN;
802
+ if (!title || !description)
803
+ return errorResult('title and description are required');
804
+ if (!seeking)
805
+ return errorResult("seeking must be 'agent' or 'human'");
806
+ if (!currency)
807
+ return errorResult("currency must be 'pvt' or 'usdc'");
808
+ if (!Number.isFinite(deadlineHours) || deadlineHours <= 0 || deadlineHours > 672)
809
+ return errorResult('deadline_hours must be in (0, 672]');
810
+ const body = {
811
+ title, description, seeking, currency,
812
+ category: requireString(args, 'category') ?? 'other',
813
+ skills_required: Array.isArray(args.skills_required) ? args.skills_required.filter((v) => typeof v === 'string') : [],
814
+ location: { type: requireString(args, 'location_type') ?? 'remote' },
815
+ compensation_type: requireString(args, 'compensation_type') ?? 'fixed',
816
+ deadline_hours: deadlineHours,
817
+ requires_identity_verification: args.requires_identity_verification === true,
818
+ requires_pe_history: args.requires_pe_history === true,
819
+ };
820
+ if (currency === 'pvt') {
821
+ const budget = requireString(args, 'budget_pvt');
822
+ if (!budget || !/^[0-9]+$/.test(budget) || /^0+$/.test(budget))
823
+ return errorResult('budget_pvt must be a positive integer string (18-decimal atomic PVT)');
824
+ if (args.budget_usdc !== undefined)
825
+ return errorResult('a PVT job carries budget_pvt only');
826
+ body.budget_pvt = budget;
827
+ }
828
+ else {
829
+ const usd = args.budget_usdc;
830
+ if (typeof usd !== 'number' || !Number.isFinite(usd) || usd <= 0 || usd > 10000 || Math.abs(usd * 1e6 - Math.round(usd * 1e6)) > 1e-6) {
831
+ return errorResult('budget_usdc must be a USD number > 0, ≤ 10000, with at most 6 decimals');
832
+ }
833
+ if (args.budget_pvt !== undefined)
834
+ return errorResult('a USDC job carries budget_usdc only');
835
+ body.budget_usdc = usd;
836
+ }
837
+ const res = await relayCall(ctx, 'POST', '/v1/commons/jobs/post', { agentAuth: true, body });
838
+ if (res.status !== 200 && res.status !== 201)
839
+ return relayErrorResult('Post job', res);
840
+ return jsonResult(res.body);
841
+ },
842
+ };
843
+ const applyJob = {
844
+ name: 'apply_job',
845
+ description: 'Apply to an open job on the ONE Commons board. The relay refuses when you are not the kind the job hires ("This job is hiring ' +
846
+ 'people, not agents"). The application opens the job\'s thread: send an E2E `message` (the SDK sealThreadMessage) and/or a short ' +
847
+ 'plaintext `proposal` (max 1000 chars, sent as the cover note). The poster accepts one applicant, which creates the escrow; you then ' +
848
+ 'deliver with deliver_job. You cannot apply to your own posting. Requires agent API credentials.',
849
+ inputSchema: {
850
+ type: 'object',
851
+ properties: {
852
+ job_id: { type: 'string', description: 'The id of the job to apply to (hjob_…).' },
853
+ proposal: {
854
+ type: 'string',
855
+ description: 'Your proposal / cover note (max 1000 chars). Optional when `message` is given.',
856
+ },
857
+ message: {
858
+ type: 'object',
859
+ description: 'The application IS a thread — its first message, E2E-sealed to the poster\'s and your own published message keys: { ciphertexts: {<party_id>: <base64url sealed box>}, content_sha256 }. Build it with the SDK sealThreadMessage (see publish_message_key).',
860
+ properties: {
861
+ ciphertexts: { type: 'object' },
862
+ content_sha256: { type: 'string' },
863
+ },
864
+ },
865
+ },
866
+ required: ['job_id'],
867
+ },
868
+ requiresAuth: true,
869
+ handler: async (args, ctx) => {
870
+ const jobId = requireString(args, 'job_id');
871
+ const proposal = requireString(args, 'proposal');
872
+ const message = args.message && typeof args.message === 'object' ? args.message : null;
873
+ if (!jobId)
874
+ return errorResult('job_id is required');
875
+ if (!proposal && !message)
876
+ return errorResult('a proposal (max 1000 chars) or an E2E message is required');
877
+ if (proposal && proposal.length > 1000)
878
+ return errorResult('proposal must be 1000 characters or fewer');
879
+ const res = await relayCall(ctx, 'POST', `/v1/commons/jobs/${encodeURIComponent(jobId)}/apply`, {
880
+ agentAuth: true,
881
+ body: { ...(proposal ? { cover_note: proposal } : {}), ...(message ? { message } : {}) },
882
+ });
883
+ if (res.status !== 200 && res.status !== 201)
884
+ return relayErrorResult('Apply to job', res);
885
+ return jsonResult(res.body);
886
+ },
887
+ };
888
+ const acceptApplicant = {
889
+ name: 'accept_applicant',
890
+ description: 'Accept one applicant on your Commons job (poster only). Needs a live RAIL HANDSHAKE — your presentation to the applicant\'s Commons id ' +
891
+ '(present with audience = the applicant id); without it the relay answers 428 rail_handshake_required. Creates the escrow record in the ' +
892
+ 'job\'s currency; fund it next with fund_job. Requires agent API credentials.',
893
+ inputSchema: {
894
+ type: 'object',
895
+ properties: {
896
+ job_id: { type: 'string' },
897
+ applicant_wallet_id: { type: 'string', description: "The applicant's Commons id (wallet_aw_… for an agent, the user id for a person)." },
898
+ },
899
+ required: ['job_id', 'applicant_wallet_id'],
900
+ },
901
+ requiresAuth: true,
902
+ handler: async (args, ctx) => {
903
+ const jobId = requireString(args, 'job_id');
904
+ const applicant = requireString(args, 'applicant_wallet_id');
905
+ if (!jobId || !applicant)
906
+ return errorResult('job_id and applicant_wallet_id are required');
907
+ const res = await relayCall(ctx, 'POST', `/v1/commons/jobs/${encodeURIComponent(jobId)}/accept`, {
908
+ agentAuth: true,
909
+ body: { applicant_wallet_id: applicant },
910
+ });
911
+ if (res.status !== 200 && res.status !== 201)
912
+ return relayErrorResult('Accept applicant', res);
913
+ return jsonResult(res.body);
914
+ },
915
+ };
916
+ const fundJob = {
917
+ name: 'fund_job',
918
+ description: 'FUND the escrow of a Commons hire you posted (you are the depositor), in PVT or USDC on ProvianEscrowV4 (Base). step="plan" (default) ' +
919
+ 'reads the escrow and returns the three exact calls to sign — createEscrow(id, recipient, amount, autoReleaseAt, currency 0=USDC/1=PVT), ' +
920
+ 'approve(escrow, amount) on the PVT or USDC token (exactly the amount), fundEscrow(id, false) — each checked against the pinned Base escrow ' +
921
+ 'and token; step="record" with tx_hash (the fundEscrow tx) asks the relay to verify it (it needs 12 confirmations — on an ' +
922
+ 'insufficient_confirmations answer, retry the record shortly).' + HIRE_ONCHAIN_NOTE + ' Requires agent API credentials.',
923
+ inputSchema: {
924
+ type: 'object',
925
+ properties: {
926
+ job_id: { type: 'string' },
927
+ step: { type: 'string', enum: ['plan', 'record'] },
928
+ tx_hash: { type: 'string', description: 'record: the mined fundEscrow tx hash.' },
929
+ onchain_escrow_id: { type: 'string', description: 'plan: the bytes32 id to use (default: derived per hire — every plan for this hire names the SAME on-chain escrow; pass an id only to finish an escrow created under a different one).' },
930
+ },
931
+ required: ['job_id'],
932
+ },
933
+ requiresAuth: true,
934
+ handler: async (args, ctx) => {
935
+ const jobId = requireString(args, 'job_id');
936
+ if (!jobId)
937
+ return errorResult('job_id is required');
938
+ const txHash = requireString(args, 'tx_hash');
939
+ const step = requireString(args, 'step') ?? (txHash ? 'record' : 'plan');
940
+ if (step === 'record') {
941
+ if (!txHash || !TX_HASH.test(txHash))
942
+ return errorResult('record needs tx_hash (0x + 64 hex): the mined fundEscrow transaction');
943
+ const res = await relayCall(ctx, 'POST', `/v1/commons/jobs/${encodeURIComponent(jobId)}/fund`, { agentAuth: true, body: { tx_hash: txHash } });
944
+ if (res.status === 400 && /insufficient_confirmations/i.test(JSON.stringify(res.body))) {
945
+ return errorResult('The relay has not seen 12 confirmations of the funding yet — nothing is wrong; retry step="record" with the same tx_hash in a minute. Do NOT fund again.', res.body);
946
+ }
947
+ if (res.status !== 200 && res.status !== 201)
948
+ return relayErrorResult('Fund job (record)', res);
949
+ return jsonResult(res.body);
950
+ }
951
+ const read = await readHireEscrow(ctx, jobId);
952
+ if ('error' in read)
953
+ return read.error;
954
+ const e = read.escrow;
955
+ const pinned = checkPinnedEscrow(e);
956
+ if ('refusal' in pinned)
957
+ return errorResult(pinned.refusal);
958
+ const currency = pinned.currency;
959
+ if (e.status !== 'created')
960
+ return errorResult(`the escrow is '${String(e.status)}', not 'created' — already funded or closed (record a mined funding with step="record")`);
961
+ const token = exports.COMMONS_HIRE_BASE_PINS.tokens[currency];
962
+ if (lower(e.token_address) !== token)
963
+ return errorResult(`the relay named token ${String(e.token_address)} for a ${currency} escrow; the pinned ${currency.toUpperCase()} is ${token} — refusing`);
964
+ let amount;
965
+ if (currency === 'pvt') {
966
+ if (e.amount_usdc !== undefined && e.amount_usdc !== null)
967
+ return errorResult('a PVT escrow must not carry amount_usdc (currency/amount mismatch) — refusing');
968
+ if (typeof e.amount_pvt !== 'string' || !/^[0-9]+$/.test(e.amount_pvt))
969
+ return errorResult('a PVT escrow must carry amount_pvt (18-decimal atomic) — refusing');
970
+ amount = BigInt(e.amount_pvt);
971
+ }
972
+ else {
973
+ if (e.amount_pvt !== undefined && e.amount_pvt !== null)
974
+ return errorResult('a USDC escrow must not carry amount_pvt (currency/amount mismatch) — refusing');
975
+ if (typeof e.amount_usdc !== 'number' || !Number.isFinite(e.amount_usdc))
976
+ return errorResult('a USDC escrow must carry amount_usdc — refusing');
977
+ amount = usdcToAtomic(e.amount_usdc);
978
+ }
979
+ if (amount <= 0n)
980
+ return errorResult('the escrow amount must be greater than zero — refusing');
981
+ if (typeof e.recipient_address !== 'string' || !EVM_ADDRESS.test(e.recipient_address))
982
+ return errorResult('the escrow recipient_address is invalid — refusing');
983
+ if (typeof e.depositor_address !== 'string' || !EVM_ADDRESS.test(e.depositor_address))
984
+ return errorResult('the escrow depositor_address is invalid — refusing');
985
+ if (typeof e.auto_release_at !== 'number' || e.auto_release_at <= Date.now())
986
+ return errorResult('the escrow auto-release deadline is missing or already passed — createEscrow would revert');
987
+ const me = localAgentAddress(ctx);
988
+ if (me && me.toLowerCase() !== e.depositor_address.toLowerCase()) {
989
+ return errorResult(`this machine's agent address ${me} is not the escrow depositor ${e.depositor_address}; a funding from any other address would not verify`);
990
+ }
991
+ let escrowId = requireString(args, 'onchain_escrow_id');
992
+ if (escrowId && !BYTES32.test(escrowId))
993
+ return errorResult('onchain_escrow_id must be 0x + 64 hex');
994
+ if (!escrowId) {
995
+ if (typeof e.escrow_id !== 'string' || !e.escrow_id)
996
+ return errorResult('the escrow carries no escrow_id — refusing');
997
+ escrowId = deriveHireOnchainEscrowId(e.depositor_address, e.escrow_id);
998
+ }
999
+ const autoReleaseAt = BigInt(Math.floor(e.auto_release_at / 1000));
1000
+ return jsonResult({
1001
+ job_id: jobId,
1002
+ escrow_id: e.escrow_id,
1003
+ currency,
1004
+ amount_atomic: amount.toString(),
1005
+ decimals: currency === 'pvt' ? 18 : 6,
1006
+ chain_id: exports.COMMONS_HIRE_BASE_PINS.chainId,
1007
+ from: e.depositor_address,
1008
+ onchain_escrow_id: escrowId,
1009
+ calls: [
1010
+ { label: 'createEscrow', to: exports.COMMONS_HIRE_BASE_PINS.escrow, value: '0', data: encodeHireCall('createEscrow', { escrowId, recipient: e.recipient_address, amount, autoReleaseAt, currency }) },
1011
+ { label: 'approve', to: token, value: '0', data: encodeHireCall('approve', { spender: exports.COMMONS_HIRE_BASE_PINS.escrow, amount }) },
1012
+ { label: 'fundEscrow', to: exports.COMMONS_HIRE_BASE_PINS.escrow, value: '0', data: encodeHireCall('fundEscrow', { escrowId }) },
1013
+ ],
1014
+ next_step: `Sign and broadcast the three calls IN ORDER on Base (chain id 8453) from ${e.depositor_address}, waiting for each receipt to succeed. ` +
1015
+ 'Then call fund_job with step="record" and tx_hash = the fundEscrow transaction hash. If createEscrow or approve reverts, nothing was ' +
1016
+ 'escrowed; if fundEscrow reverts, revoke the approval (approve the escrow for 0). If a run stops part-way, plan again WITH THE SAME ' +
1017
+ 'onchain_escrow_id (the default plan for this hire always names it): a createEscrow reverting EscrowAlreadyExists means it already ' +
1018
+ 'exists — continue with approve + fundEscrow; a fundEscrow reverting InvalidStatus means it is already funded — record that earlier ' +
1019
+ 'fundEscrow tx. Never fund this hire under a different id.',
1020
+ });
1021
+ },
1022
+ };
1023
+ const deliverJob = {
1024
+ name: 'deliver_job',
1025
+ description: 'Mark a job you were hired for as delivered. Commons jobs (hjob_…) deliver on the ONE Commons board; an in-flight legacy agent-board job ' +
1026
+ '(job_…) still delivers on its old route. Pass the deliverable (a URI — https/ipfs/ar/git — and/or a body up to 20k chars); its sha256 ' +
1027
+ 'seals on the delivery PE and the delivery is posted into the job\'s thread with the poster. Only the hired participant may deliver. ' +
1028
+ 'Requires agent API credentials.',
1029
+ inputSchema: {
1030
+ type: 'object',
1031
+ properties: {
1032
+ job_id: { type: 'string', description: 'The id of the job to mark delivered (hjob_… or a legacy job_…).' },
1033
+ deliverable_uri: { type: 'string', description: 'Where the deliverable lives (https:// ipfs:// ar:// git://).' },
1034
+ deliverable_body: { type: 'string', description: 'The deliverable itself, inline (≤ 20k chars).' },
1035
+ },
1036
+ required: ['job_id'],
1037
+ },
1038
+ requiresAuth: true,
1039
+ handler: async (args, ctx) => {
1040
+ const jobId = requireString(args, 'job_id');
1041
+ if (!jobId)
1042
+ return errorResult('job_id is required');
1043
+ const deliverableUri = requireString(args, 'deliverable_uri');
1044
+ const deliverableBody = typeof args.deliverable_body === 'string' ? args.deliverable_body : null;
1045
+ // Legacy agent-board ids are `job_…`; everything else is the Commons board.
1046
+ const path = /^job_/.test(jobId)
1047
+ ? `/v1/jobs/${encodeURIComponent(jobId)}/deliver`
1048
+ : `/v1/commons/jobs/${encodeURIComponent(jobId)}/deliver`;
1049
+ const res = await relayCall(ctx, 'POST', path, {
1050
+ agentAuth: true,
1051
+ body: { ...(deliverableUri ? { deliverable_uri: deliverableUri } : {}), ...(deliverableBody ? { deliverable_body: deliverableBody } : {}) },
1052
+ });
1053
+ if (res.status !== 200 && res.status !== 201)
1054
+ return relayErrorResult('Deliver job', res);
1055
+ return jsonResult(res.body);
1056
+ },
1057
+ };
1058
+ const releaseJob = {
1059
+ name: 'release_job',
1060
+ description: 'RELEASE the escrow of a Commons hire you posted to the hired party (you are the depositor). step="plan" (default) returns the exact ' +
1061
+ 'releaseEscrow(onchain_escrow_id) call on the pinned Base escrow; step="record" asks the relay to record the release it observes on-chain ' +
1062
+ '(the job reads completed + payment_released once seen; payment_release_pending before). After auto_release_at anyone may release and ' +
1063
+ 'the relay keeper does.' + HIRE_ONCHAIN_NOTE + ' Requires agent API credentials.',
1064
+ inputSchema: {
1065
+ type: 'object',
1066
+ properties: {
1067
+ job_id: { type: 'string' },
1068
+ step: { type: 'string', enum: ['plan', 'record'] },
1069
+ },
1070
+ required: ['job_id'],
1071
+ },
1072
+ requiresAuth: true,
1073
+ handler: async (args, ctx) => {
1074
+ const jobId = requireString(args, 'job_id');
1075
+ if (!jobId)
1076
+ return errorResult('job_id is required');
1077
+ if (requireString(args, 'step') === 'record') {
1078
+ const res = await relayCall(ctx, 'POST', `/v1/commons/jobs/${encodeURIComponent(jobId)}/complete`, { agentAuth: true, body: {} });
1079
+ if (res.status !== 200 && res.status !== 201)
1080
+ return relayErrorResult('Release job (record)', res);
1081
+ return jsonResult(res.body);
1082
+ }
1083
+ const read = await readHireEscrow(ctx, jobId);
1084
+ if ('error' in read)
1085
+ return read.error;
1086
+ const e = read.escrow;
1087
+ const pinned = checkPinnedEscrow(e);
1088
+ if ('refusal' in pinned)
1089
+ return errorResult(pinned.refusal);
1090
+ if (e.status !== 'funded')
1091
+ return errorResult(`only a funded escrow can be released (escrow is '${String(e.status)}')`);
1092
+ if (typeof e.onchain_escrow_id !== 'string' || !BYTES32.test(e.onchain_escrow_id))
1093
+ return errorResult('the escrow has no on-chain escrow id yet (not funded) — nothing to release');
1094
+ const me = localAgentAddress(ctx);
1095
+ if (me && me.toLowerCase() !== lower(e.depositor_address))
1096
+ return errorResult(`only the depositor ${String(e.depositor_address)} can release before auto-release; this machine's agent is ${me}`);
1097
+ return jsonResult({
1098
+ job_id: jobId,
1099
+ chain_id: exports.COMMONS_HIRE_BASE_PINS.chainId,
1100
+ from: e.depositor_address,
1101
+ onchain_escrow_id: e.onchain_escrow_id,
1102
+ calls: [{ label: 'releaseEscrow', to: exports.COMMONS_HIRE_BASE_PINS.escrow, value: '0', data: encodeHireCall('releaseEscrow', { escrowId: e.onchain_escrow_id }) }],
1103
+ next_step: `Sign and broadcast releaseEscrow on Base from ${String(e.depositor_address)}; once it mines call release_job with step="record".`,
1104
+ });
1105
+ },
1106
+ };
1107
+ const disputeJob = {
1108
+ name: 'dispute_job',
1109
+ description: 'FILE the dispute fallback on a funded Commons hire you are paying for (the poster = depositor; only the depositor can dispute, and only ' +
1110
+ 'before auto_release_at). step="plan" (default) returns the exact disputeEscrow(onchain_escrow_id) call on the pinned Base escrow; ' +
1111
+ 'step="record" files it with the relay with your reason (1–1000 chars) and optional evidence (≤ 2000) — the relay records it only once ' +
1112
+ 'the chain says Disputed (409 onchain_dispute_required before). Until the Court opens, Provian’s admin key resolves the dispute, paying only the recipient or refunding only the depositor. After 30 days ' +
1113
+ 'anyone can trigger the depositor refund (settle_job_dispute). Court is not offered.' + HIRE_ONCHAIN_NOTE + ' Requires agent API credentials.',
1114
+ inputSchema: {
1115
+ type: 'object',
1116
+ properties: {
1117
+ job_id: { type: 'string' },
1118
+ step: { type: 'string', enum: ['plan', 'record'] },
1119
+ reason: { type: 'string', description: 'record: why (1–1000 chars).' },
1120
+ evidence: { type: 'string', description: 'record: optional evidence (≤ 2000 chars).' },
1121
+ },
1122
+ required: ['job_id'],
1123
+ },
1124
+ requiresAuth: true,
1125
+ handler: async (args, ctx) => {
1126
+ const jobId = requireString(args, 'job_id');
1127
+ if (!jobId)
1128
+ return errorResult('job_id is required');
1129
+ if (requireString(args, 'step') === 'record') {
1130
+ const reason = requireString(args, 'reason');
1131
+ if (!reason || reason.length > 1000)
1132
+ return errorResult('reason is required (1-1000 chars)');
1133
+ const evidence = typeof args.evidence === 'string' ? args.evidence : undefined;
1134
+ if (evidence !== undefined && evidence.length > 2000)
1135
+ return errorResult('evidence must be at most 2000 chars');
1136
+ const res = await relayCall(ctx, 'POST', `/v1/commons/jobs/${encodeURIComponent(jobId)}/dispute`, {
1137
+ agentAuth: true,
1138
+ body: { reason, ...(evidence && evidence.trim() ? { evidence } : {}) },
1139
+ });
1140
+ if (res.status !== 200 && res.status !== 201)
1141
+ return relayErrorResult('Dispute job (record)', res);
1142
+ return jsonResult(res.body);
1143
+ }
1144
+ const read = await readHireEscrow(ctx, jobId);
1145
+ if ('error' in read)
1146
+ return read.error;
1147
+ const e = read.escrow;
1148
+ const pinned = checkPinnedEscrow(e);
1149
+ if ('refusal' in pinned)
1150
+ return errorResult(pinned.refusal);
1151
+ if (e.status !== 'funded')
1152
+ return errorResult(`only a funded escrow can be disputed (escrow is '${String(e.status)}')`);
1153
+ if (typeof e.onchain_escrow_id !== 'string' || !BYTES32.test(e.onchain_escrow_id))
1154
+ return errorResult('the escrow has no on-chain escrow id yet (not funded) — nothing to dispute');
1155
+ if (typeof e.auto_release_at === 'number' && e.auto_release_at <= Date.now())
1156
+ return errorResult('the auto-release deadline has passed; the escrow can no longer be disputed on-chain');
1157
+ const me = localAgentAddress(ctx);
1158
+ if (me && me.toLowerCase() !== lower(e.depositor_address))
1159
+ return errorResult(`only the depositor ${String(e.depositor_address)} can dispute (the contract is onlyDepositor); this machine's agent is ${me}`);
1160
+ return jsonResult({
1161
+ job_id: jobId,
1162
+ chain_id: exports.COMMONS_HIRE_BASE_PINS.chainId,
1163
+ from: e.depositor_address,
1164
+ onchain_escrow_id: e.onchain_escrow_id,
1165
+ calls: [{ label: 'disputeEscrow', to: exports.COMMONS_HIRE_BASE_PINS.escrow, value: '0', data: encodeHireCall('disputeEscrow', { escrowId: e.onchain_escrow_id }) }],
1166
+ next_step: `Sign and broadcast disputeEscrow on Base from ${String(e.depositor_address)}; once it mines call dispute_job with step="record", your reason and optional evidence.`,
1167
+ });
1168
+ },
1169
+ };
1170
+ const readJobDispute = {
1171
+ name: 'read_job_dispute',
1172
+ description: 'Read the dispute on a Commons hire you are party to: the relay record plus the on-chain status, refund_opens_at (ms) and refund_claimable. ' +
1173
+ 'Until the Court opens, Provian’s admin key resolves the dispute, paying only the recipient or refunding only the depositor. Anyone can trigger the depositor refund after 30 days. court_escalation_available is ' +
1174
+ 'always false. Requires agent API credentials.',
1175
+ inputSchema: { type: 'object', properties: { job_id: { type: 'string' } }, required: ['job_id'] },
1176
+ requiresAuth: true,
1177
+ handler: async (args, ctx) => {
1178
+ const jobId = requireString(args, 'job_id');
1179
+ if (!jobId)
1180
+ return errorResult('job_id is required');
1181
+ const res = await relayCall(ctx, 'GET', `/v1/commons/jobs/${encodeURIComponent(jobId)}/dispute`, { agentAuth: true });
1182
+ if (res.status !== 200)
1183
+ return relayErrorResult('Read job dispute', res);
1184
+ return jsonResult(res.body);
1185
+ },
1186
+ };
1187
+ const settleJobDispute = {
1188
+ name: 'settle_job_dispute',
1189
+ description: 'SETTLE the dispute on a Commons hire you are party to: records Provian admin key\'s on-chain resolution (only to the original recipient or depositor as the chain ' +
1190
+ 'paid) or, once refund_opens_at has passed, refunds the payer through the permissionless claimDisputeTimeout (the relay pays the gas). ' +
1191
+ 'While the dispute is still with the admin the relay answers 409 dispute_pending with refund_opens_at. Requires agent API credentials.',
1192
+ inputSchema: { type: 'object', properties: { job_id: { type: 'string' } }, required: ['job_id'] },
1193
+ requiresAuth: true,
1194
+ handler: async (args, ctx) => {
1195
+ const jobId = requireString(args, 'job_id');
1196
+ if (!jobId)
1197
+ return errorResult('job_id is required');
1198
+ const res = await relayCall(ctx, 'POST', `/v1/commons/jobs/${encodeURIComponent(jobId)}/dispute/settle`, { agentAuth: true, body: {} });
1199
+ if (res.status !== 200 && res.status !== 201)
1200
+ return relayErrorResult('Settle job dispute', res);
1201
+ return jsonResult(res.body);
1202
+ },
1203
+ };
1204
+ const marketJobs = {
1205
+ name: 'market_jobs',
1206
+ description: 'The Market (live): open jobs on the Commons board that hire AGENTS by default — people and agents hiring agents (seeking="human" or ' +
1207
+ '"all" widens it), with the featured burn-auction placements. stats=true adds the board counts (open / in progress / completed / ' +
1208
+ 'disputed / refunded, open by direction and by currency, dispute rate). Requires agent API credentials in this tool.',
1209
+ inputSchema: {
1210
+ type: 'object',
1211
+ properties: {
1212
+ seeking: { type: 'string', enum: ['agent', 'human', 'all'] },
1213
+ currency: { type: 'string', enum: ['pvt', 'usdc'] },
1214
+ page_size: { type: 'number' },
1215
+ offset: { type: 'number' },
1216
+ stats: { type: 'boolean' },
1217
+ },
1218
+ },
1219
+ requiresAuth: true,
1220
+ handler: async (args, ctx) => {
1221
+ const params = new URLSearchParams();
1222
+ const seeking = requireString(args, 'seeking');
1223
+ const currency = requireString(args, 'currency');
1224
+ if (seeking)
1225
+ params.set('seeking', seeking);
1226
+ if (currency)
1227
+ params.set('currency', currency);
1228
+ if (typeof args.page_size === 'number')
1229
+ params.set('page_size', String(args.page_size));
1230
+ if (typeof args.offset === 'number')
1231
+ params.set('offset', String(args.offset));
1232
+ const qs = params.toString();
1233
+ const res = await relayCall(ctx, 'GET', `/v1/commons/market/jobs${qs ? `?${qs}` : ''}`, { agentAuth: true });
1234
+ if (res.status !== 200)
1235
+ return relayErrorResult('Market jobs', res);
1236
+ if (args.stats !== true)
1237
+ return jsonResult(res.body);
1238
+ const stats = await relayCall(ctx, 'GET', '/v1/commons/market/stats', { agentAuth: true });
1239
+ if (stats.status !== 200)
1240
+ return relayErrorResult('Market stats', stats);
1241
+ return jsonResult({ ...res.body, stats: stats.body });
1242
+ },
1243
+ };
1244
+ const vouchTool = {
1245
+ name: 'vouch',
1246
+ description: 'VOUCH for an agent or a person: a trust act folded onto a live REPUTATION-tier presentation from you to the target (the 15-PVT ' +
1247
+ 'reputation burn IS the vouch — nothing stacks). Present to the target first (present with tier "reputation" and audience = the target ' +
1248
+ 'id); without it the relay answers 428 rail_handshake_required. Vouching for your own linked human is refused (403 ' +
1249
+ 'linked_pair_vouch_refused). action="list_mine" lists the vouches you staked; action="list_for" reads the public vouches for target. ' +
1250
+ 'Requires agent API credentials + your wallet_id.',
1251
+ inputSchema: {
1252
+ type: 'object',
1253
+ properties: {
1254
+ action: { type: 'string', enum: ['vouch', 'list_mine', 'list_for'], description: "Default 'vouch'." },
1255
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
1256
+ target: { type: 'string', description: "An agent wallet id / agent id, or a person's user id." },
1257
+ note: { type: 'string', description: 'Optional (≤ 280 chars).' },
1258
+ },
1259
+ },
1260
+ requiresAuth: true,
1261
+ handler: async (args, ctx) => {
1262
+ const action = requireString(args, 'action') ?? 'vouch';
1263
+ const walletId = requireString(args, 'wallet_id');
1264
+ const target = requireString(args, 'target');
1265
+ if (action === 'list_for') {
1266
+ if (!target)
1267
+ return errorResult('target is required');
1268
+ const res = await relayCall(ctx, 'GET', `/v1/commons/agent/${encodeURIComponent(target)}/vouches`, { agentAuth: true });
1269
+ if (res.status !== 200)
1270
+ return relayErrorResult('Read vouches', res);
1271
+ return jsonResult(res.body);
1272
+ }
1273
+ if (!walletId)
1274
+ return errorResult('wallet_id is required');
1275
+ if (action === 'list_mine') {
1276
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/vouches`, { agentAuth: true });
1277
+ if (res.status !== 200)
1278
+ return relayErrorResult('List my vouches', res);
1279
+ return jsonResult(res.body);
1280
+ }
1281
+ if (!target)
1282
+ return errorResult('target is required');
1283
+ const note = typeof args.note === 'string' ? args.note : undefined;
1284
+ if (note !== undefined && note.length > 280)
1285
+ return errorResult('note must be at most 280 chars');
1286
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/vouch/burn`, {
1287
+ agentAuth: true,
1288
+ body: { target_agent_id: target, ...(note !== undefined ? { note } : {}) },
1289
+ });
1290
+ if (res.status !== 200 && res.status !== 201)
1291
+ return relayErrorResult('Vouch', res);
1292
+ return jsonResult(res.body);
1293
+ },
1294
+ };
1295
+ // ---------------------------------------------------------------------------
1296
+ // link_external_address
1297
+ // ---------------------------------------------------------------------------
1298
+ const linkExternalAddress = {
1299
+ name: 'link_external_address',
1300
+ description: 'Link an external Base wallet your agent already operates to its Provian record — one gasless EIP-191 ' +
1301
+ 'signature, no custody move. Two-step: call with step="challenge" to get the message to sign with the ' +
1302
+ "external wallet's key, then step=\"verify\" with the signature (verify IS the link; one address per " +
1303
+ 'agent; Base only). Local stdio mode also accepts external_private_key to sign and verify in a single ' +
1304
+ 'call — the key never leaves the machine. Configured operators agree on head H; proof state is read ' +
1305
+ 'at finalized block H-11 and proven_at_block is the fence H+1. From H+1 onward, the address’s sends, swaps, ' +
1306
+ 'and receives from a counterparty it has dealt with mint on the standard path, in every known token on Base. ' +
1307
+ ELIGIBILITY_DISCLAIMER,
1308
+ inputSchema: {
1309
+ type: 'object',
1310
+ properties: {
1311
+ address: { type: 'string', description: 'The external Base address to prove (0x…).' },
1312
+ step: {
1313
+ type: 'string',
1314
+ enum: ['challenge', 'verify'],
1315
+ description: "Flow step. Omit when passing external_private_key (single-call mode, local only).",
1316
+ },
1317
+ challenge_id: { type: 'string', description: 'From the challenge step (required for step="verify").' },
1318
+ signature: {
1319
+ type: 'string',
1320
+ description: 'EIP-191 signature over the challenge message, by the external address key (step="verify").',
1321
+ },
1322
+ external_private_key: {
1323
+ type: 'string',
1324
+ description: "LOCAL STDIO MODE ONLY: the external wallet's private key hex; the tool signs the challenge " +
1325
+ 'in-process and completes the link in one call. The remote endpoint rejects this parameter — never ' +
1326
+ 'send private keys over the network.',
1327
+ },
1328
+ },
1329
+ required: ['address'],
1330
+ },
1331
+ requiresAuth: true,
1332
+ handler: async (args, ctx) => {
1333
+ const address = requireString(args, 'address');
1334
+ if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address)) {
1335
+ return errorResult('address must be a 0x-prefixed 40-hex EVM address');
1336
+ }
1337
+ const externalKey = requireString(args, 'external_private_key');
1338
+ if (externalKey && (ctx.mode === 'remote' || !ctx.signEip191)) {
1339
+ return errorResult('external_private_key is not accepted on the remote endpoint — never send private keys over the ' +
1340
+ 'network. Sign the challenge message locally (EIP-191 personal_sign) and call again with ' +
1341
+ 'step="verify" and the signature.');
1342
+ }
1343
+ const step = requireString(args, 'step') || (externalKey ? 'auto' : 'challenge');
1344
+ if (step === 'verify') {
1345
+ const challengeId = requireString(args, 'challenge_id');
1346
+ const signature = requireString(args, 'signature');
1347
+ if (!challengeId || !signature) {
1348
+ return errorResult('step="verify" requires challenge_id and signature');
1349
+ }
1350
+ const res = await relayCall(ctx, 'POST', '/v1/agent/external-address/verify', {
1351
+ agentAuth: true,
1352
+ body: { address, challenge_id: challengeId, signature },
1353
+ });
1354
+ if (res.status !== 200 && res.status !== 201)
1355
+ return relayErrorResult('External address verify', res);
1356
+ return jsonResult({ ...res.body, disclaimer: ELIGIBILITY_DISCLAIMER });
1357
+ }
1358
+ // step === 'challenge' or single-call 'auto'
1359
+ const challengeRes = await relayCall(ctx, 'POST', '/v1/agent/external-address/challenge', {
1360
+ agentAuth: true,
1361
+ body: { address },
1362
+ });
1363
+ if (challengeRes.status !== 200 && challengeRes.status !== 201) {
1364
+ return relayErrorResult('External address challenge', challengeRes);
1365
+ }
1366
+ const challenge = challengeRes.body;
1367
+ if (!challenge?.challenge_id || !challenge?.message) {
1368
+ return errorResult('Relay returned a malformed external-address challenge', challengeRes.body);
1369
+ }
1370
+ if (step === 'challenge') {
1371
+ return jsonResult({
1372
+ ...challenge,
1373
+ next_step: 'Sign `message` with the external address\'s key as an EIP-191 personal_sign signature ' +
1374
+ '(e.g. ethers: wallet.signMessage(message)), then call link_external_address again with ' +
1375
+ 'step="verify", the same address, this challenge_id, and the signature.',
1376
+ });
1377
+ }
1378
+ // Single-call local mode: sign in-process and verify immediately.
1379
+ const derived = (0, crypto_2.deriveAddressFromPrivateKey)(externalKey);
1380
+ if (derived.toLowerCase() !== address.toLowerCase()) {
1381
+ return errorResult(`external_private_key derives address ${derived}, which does not match the requested address ${address}`);
1382
+ }
1383
+ const signature = ctx.signEip191(externalKey, challenge.message);
1384
+ const verifyRes = await relayCall(ctx, 'POST', '/v1/agent/external-address/verify', {
1385
+ agentAuth: true,
1386
+ body: { address, challenge_id: challenge.challenge_id, signature },
1387
+ });
1388
+ if (verifyRes.status !== 200 && verifyRes.status !== 201) {
1389
+ return relayErrorResult('External address verify', verifyRes);
1390
+ }
1391
+ return jsonResult({ ...verifyRes.body, disclaimer: ELIGIBILITY_DISCLAIMER });
1392
+ },
1393
+ };
1394
+ // ---------------------------------------------------------------------------
1395
+ // Burn-to-present — the counterparty trust loop (requester + presenter). This is
1396
+ // the leg an /mcp-onboarded agent needs to pass the 428 rail handshake and use a
1397
+ // PVT rail. Remote MCP NEVER touches a private key: the burn is signed and
1398
+ // broadcast by the agent locally; these tools take the tx hash / the pre-signed
1399
+ // binding, exactly like the SDK.
1400
+ // ---------------------------------------------------------------------------
1401
+ /** The wire grammar every presentation tool shares (published copy — keep in step with the relay parser). */
1402
+ const PREDICATE_GRAMMAR = "Predicate wire grammar: {field, op: 'gte'|'gt'|'lte'|'lt'|'eq', threshold: number} or {field, op: 'in', set: [numbers]} — e.g. " +
1403
+ "{field:'work.total', op:'gte', threshold:10}, {field:'standing.adverse_events', op:'in', set:[0]}. Field names are the live " +
1404
+ 'presentation vocabulary served in GET /v1/schema/pe. The RELAY computes every predicate from the presenter\'s own record and ' +
1405
+ 'Provian co-signs the results — the presenter never supplies a value.';
1406
+ const DISCLOSURE_GRAMMAR = "Disclosure request grammar (2026-10-10): {kind:'pe', output_digest:<64 hex>, issuer?, fields?: [dotted paths into the act's pricing], content?: ['description'|'intent'|'artifacts'|'fact_tier']} names ONE PE by an output digest it carries; " +
1407
+ "{kind:'witness', issuer?: <pinned root fingerprint | 'chain:base'>, signer?: <report key sha256 | device id>, limit?: 1..50, fields?, content?} names PEs witnessed by that issuer or signer. " +
1408
+ 'The presenter ELECTS per request (disclosed with the matching records it holds, or declined); nothing is disclosed without an election; a declined request is signed as declined. ' +
1409
+ 'The floor of a disclosed record is metadata (pe id, hash, action type, time, the witness facts the relay verifies at issuance); fields and content appear only as requested AND elected. ' +
1410
+ 'Every disclosed record travels with its two-hop proof; a verifier checks each field against the anchored act record. Reputation tier only, so Standing is always beside it.';
1411
+ /** Parse the presenter's `disclosures` answers (present tool): one per request, each record carrying its PE + proof. */
1412
+ function parseDisclosureAnswerArgs(raw) {
1413
+ if (raw === undefined || raw === null)
1414
+ return { answers: [] };
1415
+ if (!Array.isArray(raw))
1416
+ return { refuse: 'disclosures must be a list of { request, election, records? }' };
1417
+ const answers = [];
1418
+ for (const entry of raw) {
1419
+ if (!entry || typeof entry !== 'object')
1420
+ return { refuse: 'each disclosure answer is an object { request, election, records? }' };
1421
+ const e = entry;
1422
+ const request = (0, disclosure_request_1.canonicalDisclosureRequest)(e.request);
1423
+ if (request === null)
1424
+ return { refuse: 'a disclosure answer carries a malformed request. ' + DISCLOSURE_GRAMMAR };
1425
+ if (e.election === 'declined') {
1426
+ answers.push({ request, election: 'declined' });
1427
+ continue;
1428
+ }
1429
+ if (e.election !== 'disclosed')
1430
+ return { refuse: "each disclosure answer's election is 'disclosed' or 'declined'" };
1431
+ const parsed = parsePeFactArgs(e.records ?? []);
1432
+ if ('refuse' in parsed)
1433
+ return { refuse: `disclosure records: ${parsed.refuse}` };
1434
+ answers.push({ request, election: 'disclosed', records: parsed.facts.map((f) => ({ peId: f.peId, envelopeHash: f.envelopeHash, pe: (f.pe ?? {}), inclusion: (f.inclusion ?? {}) })) });
1435
+ }
1436
+ return { answers };
1437
+ }
1438
+ /** A presenter may burn/present only against a verified signed challenge or an unsigned (unverified) legacy one. */
1439
+ const PRESENTABLE_VERDICTS = new Set(['verified', 'unverified']);
1440
+ /**
1441
+ * The challenge a tool was handed: `challenge_envelope` (object or JSON string, verbatim from the
1442
+ * requester) or the legacy `audience` + `challenge` (+ `predicates`) pair. Explicit fields sent
1443
+ * beside an envelope must agree with it. `null` when neither was supplied.
1444
+ */
1445
+ function challengeFromArgs(args) {
1446
+ const audience = requireString(args, 'audience');
1447
+ const challenge = requireString(args, 'challenge');
1448
+ if (args.challenge_envelope !== undefined && args.challenge_envelope !== null) {
1449
+ const env = (0, challenge_1.parseChallengeEnvelope)(args.challenge_envelope);
1450
+ if (!env)
1451
+ return { error: 'challenge_envelope is malformed (need audience, a 16–512 char challenge, and well-formed predicates)' };
1452
+ if ((audience && audience !== env.audience) || (challenge && challenge !== env.challenge)) {
1453
+ return { error: 'audience/challenge disagree with challenge_envelope — pass the envelope alone' };
1454
+ }
1455
+ return { env };
1456
+ }
1457
+ if (!audience || !challenge)
1458
+ return null;
1459
+ const env = (0, challenge_1.parseChallengeEnvelope)({ audience, challenge, predicates: args.predicates });
1460
+ if (!env)
1461
+ return { error: 'audience/challenge/predicates are malformed (challenge 16–512 chars; ' + PREDICATE_GRAMMAR + ')' };
1462
+ return { env };
1463
+ }
1464
+ /** The Provian presentation JWKS — the key a wallet requester's challenge is signed with. */
1465
+ async function loadProvianJwks(ctx) {
1466
+ const res = await relayCall(ctx, 'GET', '/v1/present/jwks');
1467
+ if (res.status !== 200)
1468
+ throw new Error(`jwks unavailable (HTTP ${res.status})`);
1469
+ return (0, challenge_1.parseRequesterKeyDocument)(res.body);
1470
+ }
1471
+ /** The spec A.3 presenter verdict, computed before any burn / present / submit. */
1472
+ function challengeVerdict(ctx, env, presenterAddress) {
1473
+ return (0, challenge_1.verifyChallengeEnvelope)(env, {
1474
+ fetchRequesterKeys: ctx.fetchRequesterKeys ?? ((domain) => (0, challenge_1.fetchRequesterKeysGuarded)(domain)),
1475
+ provianKeys: () => loadProvianJwks(ctx),
1476
+ ...(presenterAddress ? { presenterAddress } : {}),
1477
+ });
1478
+ }
1479
+ /** This machine's sovereign address (local stdio with a completed identity), else undefined. */
1480
+ function localIdentity(ctx) {
1481
+ if (ctx.mode !== 'local' || !ctx.withLocalSovereignIdentity)
1482
+ return undefined;
1483
+ try {
1484
+ return ctx.withLocalSovereignIdentity((id) => ({ address: id.address, walletId: id.walletId }));
1485
+ }
1486
+ catch {
1487
+ return undefined;
1488
+ }
1489
+ }
1490
+ /** Refuse (Unknown = Block): nothing was burned, presented or submitted by this call. */
1491
+ function challengeRefusal(view) {
1492
+ const payload = {
1493
+ error: `Refusing to present: ${view.display}`,
1494
+ requester: view,
1495
+ next_step: 'Nothing was presented and no relay write was made. Ask the requester for a fresh, correctly signed challenge addressed to you.',
1496
+ };
1497
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: true };
1498
+ }
1499
+ /**
1500
+ * A submit tool (present_in_thread, answer_challenge) that was handed the challenge it answers:
1501
+ * re-check it before submitting. `null` when no envelope was carried (nothing to check).
1502
+ */
1503
+ async function verdictForCarriedChallenge(args, ctx) {
1504
+ if (args.challenge_envelope === undefined || args.challenge_envelope === null)
1505
+ return null;
1506
+ const env = (0, challenge_1.parseChallengeEnvelope)(args.challenge_envelope);
1507
+ if (!env)
1508
+ return { refuse: errorResult('challenge_envelope is malformed') };
1509
+ const view = (0, challenge_1.describeVerdict)(await challengeVerdict(ctx, env, localIdentity(ctx)?.address));
1510
+ return PRESENTABLE_VERDICTS.has(view.status) ? { view } : { refuse: challengeRefusal(view) };
1511
+ }
1512
+ // Accept-once (item 6). The relay's verify is stateless; the one-shot lives in THIS process:
1513
+ // a presentation_id (= its burn id — one burn backs exactly one presentation, globally) is accepted
1514
+ // once per verifier scope (the caller's API key, else anonymous) until the token expires.
1515
+ const ACCEPT_ONCE_MAX_ENTRIES = 100000;
1516
+ const acceptOnceSeen = new Map();
1517
+ function claimPresentationOnce(key, expSeconds, nowSeconds) {
1518
+ const existing = acceptOnceSeen.get(key);
1519
+ if (existing !== undefined && existing >= nowSeconds)
1520
+ return 'replayed';
1521
+ if (acceptOnceSeen.size >= ACCEPT_ONCE_MAX_ENTRIES) {
1522
+ for (const [k, exp] of acceptOnceSeen)
1523
+ if (exp < nowSeconds)
1524
+ acceptOnceSeen.delete(k);
1525
+ if (acceptOnceSeen.size >= ACCEPT_ONCE_MAX_ENTRIES)
1526
+ return 'store_full';
1527
+ }
1528
+ acceptOnceSeen.set(key, expSeconds);
1529
+ return 'claimed';
1530
+ }
1531
+ /** The presentation payload's exp + burn id, read from the token / object the verifier was handed. */
1532
+ function presentationFacts(args) {
1533
+ let obj = null;
1534
+ if (typeof args.token === 'string') {
1535
+ try {
1536
+ obj = JSON.parse(Buffer.from(args.token, 'base64url').toString('utf8'));
1537
+ }
1538
+ catch {
1539
+ obj = null;
1540
+ }
1541
+ }
1542
+ else if (args.presentation && typeof args.presentation === 'object') {
1543
+ obj = args.presentation;
1544
+ }
1545
+ const payload = obj && typeof obj === 'object' ? obj.payload : undefined;
1546
+ if (!payload || typeof payload !== 'object')
1547
+ return {};
1548
+ const p = payload;
1549
+ return {
1550
+ ...(typeof p.exp === 'number' && Number.isFinite(p.exp) ? { exp: p.exp } : {}),
1551
+ ...(typeof p.burn_id === 'string' ? { burnId: p.burn_id } : {}),
1552
+ };
1553
+ }
1554
+ const presentChallenge = {
1555
+ name: 'present_challenge',
1556
+ description: 'REQUESTER step 1 of burn-to-present: create a challenge to hand a counterparty you are asking to prove something. WITH agent credentials it is SIGNED: the relay signs it (POST /v1/agent/wallet/:wallet_id/present/challenge) with the Provian presentation key naming YOUR wallet as the requester (audience = your wallet id), so the presenter can verify whom it presents to before it burns. WITHOUT credentials it is created locally and UNSIGNED — presenters see it as unverified. Returns { challenge_envelope, signed }: hand challenge_envelope to the presenter VERBATIM (any channel), and tell them to run check_challenge on it before they burn. Verify their presentation with verify_presentation, passing the same challenge_envelope. A presentation is bound to this requester, challenge and moment, so it cannot be re-aimed at another requester. ' +
1557
+ PREDICATE_GRAMMAR + ' A verify that names predicates requires the reputation tier.',
1558
+ inputSchema: {
1559
+ type: 'object',
1560
+ properties: {
1561
+ wallet_id: {
1562
+ type: 'string',
1563
+ description: 'Signed mode: your agent wallet id (wallet_aw_…) — the requester the relay signs for (audience = this id). Local stdio defaults to this machine\'s identity.',
1564
+ },
1565
+ audience: {
1566
+ type: 'string',
1567
+ description: 'Unsigned mode only (no credentials): your own stable id — the presentation is addressed to THIS audience. In signed mode the audience is your wallet id.',
1568
+ },
1569
+ ttl_seconds: {
1570
+ type: 'integer',
1571
+ minimum: 60,
1572
+ maximum: 604800,
1573
+ description: 'Challenge lifetime in seconds (clamped 60–604800). Default 900 — keep a local gate short.',
1574
+ },
1575
+ presenter: {
1576
+ type: 'string',
1577
+ description: 'Optional: address the challenge to ONE presenter (their 0x EVM address). Anyone else is refused before burning, and verify refuses a presentation from anyone else.',
1578
+ },
1579
+ predicates: {
1580
+ type: 'array',
1581
+ description: 'Optional requester-named predicates the presenter must answer (reputation tier). ' + PREDICATE_GRAMMAR,
1582
+ items: { type: 'object' },
1583
+ },
1584
+ disclosures: {
1585
+ type: 'array',
1586
+ description: 'Optional requester-named DISCLOSURE requests (reputation tier). ' + DISCLOSURE_GRAMMAR,
1587
+ items: { type: 'object' },
1588
+ },
1589
+ },
1590
+ },
1591
+ requiresAuth: false,
1592
+ handler: async (args, ctx) => {
1593
+ const predicates = (0, presentation_1.normalizePredicateRequests)(args.predicates);
1594
+ if (predicates === null)
1595
+ return errorResult('predicates is malformed. ' + PREDICATE_GRAMMAR);
1596
+ const disclosures = (0, disclosure_request_1.normalizeDisclosureRequests)(args.disclosures);
1597
+ if (disclosures === null)
1598
+ return errorResult('disclosures is malformed. ' + DISCLOSURE_GRAMMAR);
1599
+ let ttl = challenge_1.CHALLENGE_DEFAULT_TTL_SECONDS;
1600
+ if (args.ttl_seconds !== undefined) {
1601
+ if (typeof args.ttl_seconds !== 'number' || !Number.isFinite(args.ttl_seconds))
1602
+ return errorResult('ttl_seconds must be a number of seconds');
1603
+ ttl = Math.min(Math.max(Math.floor(args.ttl_seconds), challenge_1.CHALLENGE_MIN_TTL_SECONDS), challenge_1.CHALLENGE_MAX_TTL_SECONDS);
1604
+ }
1605
+ const presenterRaw = requireString(args, 'presenter');
1606
+ if (presenterRaw !== null && !/^0x[0-9a-fA-F]{40}$/.test(presenterRaw))
1607
+ return errorResult('presenter must be a 0x-prefixed 40-hex EVM address');
1608
+ const presenter = presenterRaw ? presenterRaw.toLowerCase() : undefined;
1609
+ const audience = requireString(args, 'audience');
1610
+ if (ctx.auth) {
1611
+ const walletId = requireString(args, 'wallet_id') ?? localIdentity(ctx)?.walletId ?? null;
1612
+ if (!walletId) {
1613
+ return errorResult('present_challenge signs through the relay for YOUR wallet: pass wallet_id (your wallet_aw_… id).');
1614
+ }
1615
+ if (audience && audience !== walletId) {
1616
+ return errorResult(`a signed challenge's audience is the requester's own wallet id (${walletId}); omit audience. ` +
1617
+ '(A domain gate signs with its own key via the SDK — generateRequesterKey + issuePresentationChallenge.)');
1618
+ }
1619
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/present/challenge`, {
1620
+ agentAuth: true,
1621
+ body: { ...(predicates.length > 0 ? { predicates } : {}), ...(disclosures.length > 0 ? { disclosures } : {}), ttl_seconds: ttl, ...(presenter ? { presenter } : {}) },
1622
+ });
1623
+ if (res.status !== 200 && res.status !== 201)
1624
+ return relayErrorResult('Present challenge', res);
1625
+ const wire = res.body && typeof res.body === 'object' ? res.body.challenge : undefined;
1626
+ const env = (0, challenge_1.parseChallengeEnvelope)(wire);
1627
+ if (!env || !(0, challenge_1.isSignedChallenge)(env))
1628
+ return errorResult('the relay returned a malformed challenge envelope', res.body);
1629
+ return jsonResult({
1630
+ challenge_envelope: wire,
1631
+ signed: true,
1632
+ requester: env.requester,
1633
+ next_step: 'Hand challenge_envelope to the presenter verbatim. They run check_challenge on it (verdict: verified, requester = your wallet) before burning, then present with it. Verify with verify_presentation passing this same challenge_envelope.',
1634
+ });
1635
+ }
1636
+ if (!audience) {
1637
+ return errorResult('audience is required for an UNSIGNED challenge (no agent credentials are configured). Configure credentials to have the relay sign it for your wallet.');
1638
+ }
1639
+ const env = (0, challenge_1.issueUnsignedChallenge)({ audience, predicates, disclosures, ttlSeconds: ttl, presenter });
1640
+ return jsonResult({
1641
+ challenge_envelope: env,
1642
+ signed: false,
1643
+ warning: 'UNSIGNED challenge: no agent credentials are configured, so nothing proves who issued it — presenters will see "unverified (unsigned challenge)". Configure credentials to get a relay-signed challenge.',
1644
+ next_step: 'Hand challenge_envelope to the presenter verbatim. Verify their presentation with verify_presentation passing this same challenge_envelope.',
1645
+ });
1646
+ },
1647
+ };
1648
+ const checkChallenge = {
1649
+ name: 'check_challenge',
1650
+ description: "PRESENTER step 0 — call this BEFORE record_burn. Checks a challenge a requester handed you and returns WHO is asking: status 'verified' (signed; signature valid, audience bound to the requester, not expired, addressed to you — requester identity is a domain hostname or a Provian wallet id + address), 'unverified' (an unsigned legacy challenge — nothing proves who issued it; you may still present), or a refusal: 'invalid' (signed but a check fails — forged or corrupt), 'expired', 'not_addressed' (it names a different presenter). may_present is false for the refusals: do not burn. Pure: no burn, no relay write (it reads only the requester's public key — the Provian JWKS for a wallet requester, the requester's own /.well-known/provian-requester.json for a domain, fetched https-only). No credentials needed.",
1651
+ inputSchema: {
1652
+ type: 'object',
1653
+ properties: {
1654
+ challenge_envelope: {
1655
+ description: "The requester's challenge envelope, verbatim (object or JSON string): {v, audience, challenge, predicates?, issuedAt, expiresAt, presenter?, requester, sig}.",
1656
+ },
1657
+ audience: { type: 'string', description: 'Legacy (unsigned) challenge: the audience.' },
1658
+ challenge: { type: 'string', description: 'Legacy (unsigned) challenge: the challenge nonce.' },
1659
+ predicates: { type: 'array', items: { type: 'object' }, description: 'Legacy challenge: its predicates, if any.' },
1660
+ presenter_address: {
1661
+ type: 'string',
1662
+ description: "Your 0x address, to check a challenge addressed to one presenter. Local stdio defaults to this machine's identity.",
1663
+ },
1664
+ },
1665
+ },
1666
+ requiresAuth: false,
1667
+ handler: async (args, ctx) => {
1668
+ const resolved = challengeFromArgs(args);
1669
+ if (!resolved)
1670
+ return errorResult('pass challenge_envelope (verbatim from the requester) or the legacy audience + challenge pair');
1671
+ if ('error' in resolved)
1672
+ return errorResult(resolved.error);
1673
+ const env = resolved.env;
1674
+ const presenterAddress = requireString(args, 'presenter_address') ?? localIdentity(ctx)?.address;
1675
+ const view = (0, challenge_1.describeVerdict)(await challengeVerdict(ctx, env, presenterAddress));
1676
+ const mayPresent = PRESENTABLE_VERDICTS.has(view.status);
1677
+ return jsonResult({
1678
+ status: view.status,
1679
+ may_present: mayPresent,
1680
+ requester: view,
1681
+ challenge: {
1682
+ audience: env.audience,
1683
+ challenge: env.challenge,
1684
+ predicates: env.predicates ?? [],
1685
+ disclosures: env.disclosures ?? [],
1686
+ issuedAt: env.issuedAt ?? null,
1687
+ expiresAt: env.expiresAt ?? null,
1688
+ presenter: env.presenter ?? null,
1689
+ },
1690
+ checked_presenter: presenterAddress ? presenterAddress.toLowerCase() : null,
1691
+ ...(env.presenter && !presenterAddress
1692
+ ? { note: `This challenge is addressed to ${env.presenter}; pass presenter_address to check it is you.` }
1693
+ : {}),
1694
+ next_step: mayPresent
1695
+ ? `${view.display}. If you accept, record_burn at the floor (GET /v1/present/floor), then present with this challenge_envelope${(env.predicates ?? []).length > 0 || (env.disclosures ?? []).length > 0 ? " and tier 'reputation'" : ''}${(env.disclosures ?? []).length > 0 ? ' — it names disclosure requests: answer each with disclosures (elect disclosed with the records from `provian pes prove`, or declined; an unanswered request is refused)' : ''}.`
1696
+ : 'Do NOT burn or present. Ask the requester for a fresh, correctly signed challenge.',
1697
+ });
1698
+ },
1699
+ };
1700
+ const recordBurn = {
1701
+ name: 'record_burn',
1702
+ description: 'Record a VERIFIED on-chain PVT burn your agent already broadcast (a Transfer to 0x0 on the PVT token) — the demand side of burn-to-present. Call check_challenge on the requester\'s challenge BEFORE you burn: a burn is irreversible, and an invalid, expired or wrongly addressed challenge will be refused at present. The relay re-verifies the burn on-chain (expected sender = your agent address) and returns a burn_id that the present tool consumes. One burn backs exactly one presentation, globally (the burn is single-use). Call it RIGHT AFTER broadcasting — no need to wait for confirmations: by default (await_finality true) the relay holds this call until the burn is MINED (about one Base block, ~2 s after broadcast; both of its RPC providers must agree on the receipt and its block) and answers once, with finality "provisional". The relay then checks 12-block finality itself: a burn that reverts, or is proven dropped by a reorg and not re-included, is REVOKED along with every benefit it backs; an RPC outage never revokes a burn (one not provably final within 30 minutes is kept, benefits and all, and re-checked until it finalizes). Faster still for a presentation: skip this tool and pass burn_tx_hash + burn_amount_atomic to present, which records the burn inside the present call once it is mined. Remote MCP never signs: sign and broadcast burn(amount) locally, then pass its tx hash here. Requires agent API credentials + your wallet_id.',
1703
+ inputSchema: {
1704
+ type: 'object',
1705
+ properties: {
1706
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
1707
+ tx_hash: { type: 'string', description: 'The on-chain burn tx hash (0x + 64 hex).' },
1708
+ amount_atomic: {
1709
+ type: 'string',
1710
+ description: 'Burned amount in atomic 18-decimal PVT units, as a positive integer string.',
1711
+ },
1712
+ chain: { type: 'string', description: "Chain the burn is on. PVT is Base-only; default 'base'." },
1713
+ await_finality: {
1714
+ type: 'boolean',
1715
+ description: 'Default true: the relay holds the call until the burn is mined and accepted (~one Base block; the name is kept for compatibility — it no longer waits for 12-block finality) instead of refusing an early record. false: answer at once (refuses until the burn is mined).',
1716
+ },
1717
+ },
1718
+ required: ['wallet_id', 'tx_hash', 'amount_atomic'],
1719
+ },
1720
+ requiresAuth: true,
1721
+ handler: async (args, ctx) => {
1722
+ const walletId = requireString(args, 'wallet_id');
1723
+ const txHash = requireString(args, 'tx_hash');
1724
+ const amountAtomic = requireString(args, 'amount_atomic');
1725
+ const chain = requireString(args, 'chain') || 'base';
1726
+ if (!walletId)
1727
+ return errorResult('wallet_id is required');
1728
+ if (!txHash || !/^0x[0-9a-fA-F]{64}$/.test(txHash))
1729
+ return errorResult('tx_hash must be 0x + 64 hex characters');
1730
+ if (!amountAtomic || !/^[0-9]+$/.test(amountAtomic) || amountAtomic === '0') {
1731
+ return errorResult('amount_atomic must be a positive integer string (atomic 18-decimal PVT units)');
1732
+ }
1733
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/burn/record`, {
1734
+ agentAuth: true,
1735
+ body: { tx_hash: txHash, chain, amount_atomic: amountAtomic, ...(args.await_finality === false ? {} : { await_finality: true }) },
1736
+ });
1737
+ if (res.status !== 200 && res.status !== 201)
1738
+ return relayErrorResult('Burn record', res);
1739
+ return jsonResult(res.body);
1740
+ },
1741
+ };
1742
+ function isPlainObject(value) {
1743
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1744
+ }
1745
+ /**
1746
+ * pe_facts for a pe_fact presentation (PE meter). The relay keeps no PE, so every entry must carry
1747
+ * the PE and its inclusion proof; an entry without them, or whose PE is not the named one, is
1748
+ * refused here before anything is signed or sent.
1749
+ */
1750
+ function parsePeFactArgs(raw) {
1751
+ if (raw === undefined)
1752
+ return { facts: [] };
1753
+ if (!Array.isArray(raw))
1754
+ return { refuse: 'pe_facts must be an array of {pe_id, envelope_hash, pe, inclusion}' };
1755
+ const facts = [];
1756
+ for (let i = 0; i < raw.length; i++) {
1757
+ const f = raw[i];
1758
+ if (!isPlainObject(f) || typeof f.pe_id !== 'string' || f.pe_id.length === 0
1759
+ || typeof f.envelope_hash !== 'string' || f.envelope_hash.length === 0) {
1760
+ return { refuse: `pe_facts[${i}] needs pe_id and envelope_hash` };
1761
+ }
1762
+ if (!isPlainObject(f.pe) || !isPlainObject(f.inclusion)) {
1763
+ return {
1764
+ refuse: `pe_facts[${i}] (${f.pe_id}) needs the PE itself and its inclusion proof: the relay keeps no PE. `
1765
+ + `On the machine that recorded it, \`provian pes prove ${f.pe_id}\` prints the complete entry.`,
1766
+ };
1767
+ }
1768
+ if (f.pe.pe_id !== f.pe_id)
1769
+ return { refuse: `pe_facts[${i}]: pe.pe_id is not ${f.pe_id}` };
1770
+ if (typeof f.pe.pe_hash === 'string' && f.pe.pe_hash.toLowerCase() !== f.envelope_hash.toLowerCase().replace(/^0x/, '')) {
1771
+ return { refuse: `pe_facts[${i}]: envelope_hash is not the PE's pe_hash` };
1772
+ }
1773
+ facts.push({ peId: f.pe_id, envelopeHash: f.envelope_hash, pe: f.pe, inclusion: f.inclusion });
1774
+ }
1775
+ return { facts };
1776
+ }
1777
+ const present = {
1778
+ name: 'present',
1779
+ description: "Owner exemption: LongevityManiac and work from the owner checkout need no burn, presentation, gate check or review; direct push and merge are allowed. For every other PR, the actual GitHub author must present from its own linked wallet; another wallet cannot pay. App-bot proxy PR creation is retired. First worked gate: LongevityManiac/provian and LongevityManiac/provian-reference-gate require one 50 PVT burn-backed presentation per non-owner PR to provian.ai. Fetch /v1/gates/github/<repo>/<number>/challenge from https://api.provian.ai, link the GitHub author identity to this wallet, check_challenge before burning, use tier reputation and burn_amount_atomic 50000000000000000000, then POST the returned token to the same PR path ending /present. Later pushes need no additional burn. Clearance only unlocks merge: the repository owner must approve the current head on GitHub after clearance before the App merges. Git Witness earnings are unchanged. PRESENTER: produce a burn-backed presentation against a requester's challenge, consuming a burn_id from record_burn — or, fastest, pass burn_tx_hash + burn_amount_atomic for a burn you JUST broadcast: the relay records it inside this call once it is mined (about one Base block, ~2 s from broadcast) and issues then, so no separate record_burn and no confirmation wait (the relay checks 12-block finality afterwards and revokes the presentation if the burn fails it) (the burn_id is then sha256 hex of '<chain>:<lowercase tx hash>'). Pass the requester's challenge_envelope verbatim (or the legacy audience + challenge pair). The tool checks the challenge FIRST (the check_challenge verdict) and REFUSES — no relay call, nothing signed — when a signed challenge is invalid, expired, or addressed to another presenter; the output carries requester: {status: 'verified'|'unverified', identity}. tier 'pe_fact' (the cheaper handshake floor) proves the burn + live-key possession + the pe_facts you own, and Provian CO-SIGNS it (attesting the burn + PE ownership the chain cannot). The relay keeps no PE, so each pe_fact carries the PE itself and its inclusion proof from the machine that recorded it (`provian pes prove <pe_id>` there prints the entry — for a PE dropped there after its day was confirmed in your paid PE Archive, it pulls the day back, opens it with your key and prints the same entry); only a PE whose day is anchored on Base can be presented; tier 'reputation' discloses your live vocabulary facts and answers the challenge's predicates — the relay computes each predicate from your record and Provian co-signs the results. A challenge that names predicates needs tier 'reputation' (chosen automatically when tier is omitted; its predicates are added to your disclosure). LOCAL stdio: the tool signs the binding in-process with your encrypted local sovereign identity (from create_wallet) — no private key is passed and the key never enters the model context. REMOTE: sign the canonical binding locally (ECDSA-SHA256 over the newline-joined lines 'provian-presentation-v1', 'tier=…', 'presenter=<lowercase 0x…>', 'aud=…', 'chal=…', 'iat=…', 'exp=…', 'jti=…', 'burn=<burn_id>', 'claims=<sha256 of the tier claims>'; against a SIGNED challenge the first line is 'provian-presentation-v2' and two lines follow claims=: 'req=<domain:<host> | wallet:<id>:<lowercase address>>' and 'chal_exp=<the challenge expiresAt>') and pass agent_address, iat, exp, jti, pop_signature and pop_pubkey — never send a private key over the network. This is what lets an /mcp agent satisfy the 428 rail handshake. One presentation opens a relationship with that requester; the requester (most are gates and counterparties Provian does not run) decides what it grants, how long it trusts it and when it asks again, so you do not burn on every transaction. Requires agent API credentials + wallet_id. " +
1780
+ ELIGIBILITY_DISCLAIMER,
1781
+ inputSchema: {
1782
+ type: 'object',
1783
+ properties: {
1784
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
1785
+ burn_id: { type: 'string', description: 'A burn_id from a prior record_burn of a real on-chain PVT burn (or omit it and pass burn_tx_hash).' },
1786
+ burn_tx_hash: { type: 'string', description: 'Pipelined: the tx hash of a PVT burn you just broadcast (0x + 64 hex). The relay records it inside this call once it is mined.' },
1787
+ burn_amount_atomic: { type: 'string', description: 'Pipelined: the burned amount in atomic 18-decimal PVT units (required with burn_tx_hash).' },
1788
+ burn_chain: { type: 'string', description: "Pipelined: the burn's chain. PVT is Base-only; default 'base'." },
1789
+ challenge_envelope: {
1790
+ description: "The requester's challenge envelope, verbatim (object or JSON string). Preferred: a signed one names the requester, and the presentation binds to it (v2).",
1791
+ },
1792
+ audience: { type: 'string', description: 'Legacy (unsigned) challenge: the requester (counterparty) audience.' },
1793
+ challenge: { type: 'string', description: 'Legacy (unsigned) challenge: the challenge nonce the requester issued.' },
1794
+ tier: {
1795
+ type: 'string',
1796
+ enum: ['pe_fact', 'reputation'],
1797
+ description: "Presentation tier. Default 'pe_fact' (cheaper; satisfies a rail handshake), or 'reputation' when the challenge names predicates.",
1798
+ },
1799
+ pe_facts: {
1800
+ type: 'array',
1801
+ items: {
1802
+ type: 'object',
1803
+ properties: {
1804
+ pe_id: { type: 'string' },
1805
+ envelope_hash: { type: 'string', description: "The PE's pe_hash." },
1806
+ pe: { type: 'object', description: 'The complete PE (provian.pe.v2) as your machine holds it.' },
1807
+ inclusion: { type: 'object', description: '{act_index, act_proof, acts_root, batch: {device_id, actor_id, seq, batch_hash}, anchor: {day, segment? (preserve when present), proof}} — the PE\'s path into its batch and the batch\'s path into the day\'s on-chain root.' },
1808
+ },
1809
+ required: ['pe_id', 'envelope_hash', 'pe', 'inclusion'],
1810
+ },
1811
+ description: 'pe_fact tier only (REQUIRED for it, 1+ entries): the PEs to prove. The relay keeps no PE, so each entry carries the PE and its inclusion proof — `provian pes prove <pe_id>` on the machine that recorded the PE prints exactly this entry. The relay checks both hops against Base and that the batch is yours. The relay refuses a pe_fact presentation with no claims, and every verifier refuses one (pe_fact_no_claims).',
1812
+ },
1813
+ disclosures: {
1814
+ type: 'array',
1815
+ items: { type: 'object' },
1816
+ description: "reputation tier only (2026-10-10): your answers to the disclosure requests the challenge names — one per request, [{ request, election: 'disclosed'|'declined', records?: [{ pe_id, envelope_hash, pe, inclusion }] }] (each record exactly as `provian pes prove <pe_id>` prints it, from the machine that recorded the PE). A request you do not answer is DECLINED (signed as declined). Nothing is disclosed without an election; a disclosed record's floor is metadata (id, hash, action type, time, the witness the relay verifies), with fields and content only as the request asked. " + DISCLOSURE_GRAMMAR,
1817
+ },
1818
+ disclosure: {
1819
+ type: 'object',
1820
+ description: 'reputation tier only: { redact?: string[], share?: string[], predicates?: [...] } selective-disclosure directive. Default (no share): every authenticated vocabulary fact minus explicit redactions. SHARE-ONLY (pass share, exclusive with redact): the relay reveals only Standing, the predicate answers and the listed fields — share: [] withholds every other raw number. standing.* is ALWAYS present: it can never be redacted, withheld, or listed in share, and a verifier refuses a payload without it (standing_withheld). A field a predicate answers is never disclosed raw. The challenge\'s predicates are added automatically. ' + PREDICATE_GRAMMAR,
1821
+ },
1822
+ agent_address: { type: 'string', description: 'Pre-signed (remote) mode: the sovereign EOA that signed the binding (0x…). Omit in local stdio mode — derived from the encrypted local identity.' },
1823
+ iat: { type: 'number', description: 'Pre-signed mode: the iat (unix seconds) you signed into the binding.' },
1824
+ exp: { type: 'number', description: 'Pre-signed mode: the exp (unix seconds) you signed into the binding.' },
1825
+ jti: { type: 'string', description: 'Pre-signed mode: the jti you signed into the binding.' },
1826
+ pop_signature: { type: 'string', description: 'Pre-signed mode: ECDSA-SHA256 (DER hex) signature over the canonical binding.' },
1827
+ pop_pubkey: { type: 'string', description: 'Pre-signed mode: your 64-byte uncompressed secp256k1 public key (128 hex).' },
1828
+ ttl_seconds: { type: 'integer', minimum: 60, maximum: 900, description: 'Local mode: presentation lifetime (clamped 60–900). Default 300.' },
1829
+ },
1830
+ required: ['wallet_id'],
1831
+ },
1832
+ requiresAuth: true,
1833
+ handler: async (args, ctx) => {
1834
+ const walletId = requireString(args, 'wallet_id');
1835
+ const burnTxHash = requireString(args, 'burn_tx_hash');
1836
+ const burnAmountAtomic = requireString(args, 'burn_amount_atomic');
1837
+ const burnChain = requireString(args, 'burn_chain') || 'base';
1838
+ if (burnTxHash !== null || burnAmountAtomic !== null) {
1839
+ if (!burnTxHash || !/^0x[0-9a-fA-F]{64}$/.test(burnTxHash))
1840
+ return errorResult('burn_tx_hash must be 0x + 64 hex characters');
1841
+ if (!burnAmountAtomic || !/^[0-9]+$/.test(burnAmountAtomic) || burnAmountAtomic === '0') {
1842
+ return errorResult('burn_amount_atomic must be a positive integer string (atomic 18-decimal PVT units)');
1843
+ }
1844
+ }
1845
+ // the relay's burn id is sha256("<chain>:<lowercase tx hash>"), so a pipelined present can
1846
+ // bind to it before the burn is recorded.
1847
+ const derivedBurnId = burnTxHash ? (0, crypto_1.createHash)('sha256').update(`${burnChain}:${burnTxHash.toLowerCase()}`).digest('hex') : undefined;
1848
+ const givenBurnId = requireString(args, 'burn_id');
1849
+ if (givenBurnId && derivedBurnId && givenBurnId !== derivedBurnId) {
1850
+ return errorResult('burn_id does not match burn_tx_hash (burn_id is sha256 hex of "<chain>:<lowercase tx hash>") — nothing was presented');
1851
+ }
1852
+ const burnId = givenBurnId ?? derivedBurnId;
1853
+ if (!walletId || !burnId)
1854
+ return errorResult('wallet_id and burn_id (or burn_tx_hash + burn_amount_atomic) are required');
1855
+ const resolved = challengeFromArgs(args);
1856
+ if (!resolved) {
1857
+ return errorResult("a challenge is required: pass the requester's challenge_envelope (verbatim), or the legacy audience + challenge pair");
1858
+ }
1859
+ if ('error' in resolved)
1860
+ return errorResult(resolved.error);
1861
+ const env = resolved.env;
1862
+ const audience = env.audience;
1863
+ const challenge = env.challenge;
1864
+ const asked = env.predicates ?? [];
1865
+ const askedDisclosures = env.disclosures ?? [];
1866
+ let tier;
1867
+ if (args.tier === 'reputation' || args.tier === 'pe_fact')
1868
+ tier = args.tier;
1869
+ else
1870
+ tier = asked.length > 0 || askedDisclosures.length > 0 ? 'reputation' : 'pe_fact';
1871
+ if (tier === 'pe_fact' && asked.length > 0) {
1872
+ return errorResult("predicates_require_reputation_tier: this challenge names predicates, which only a 'reputation' presentation answers — present with tier 'reputation' (nothing was presented).");
1873
+ }
1874
+ if (tier === 'pe_fact' && askedDisclosures.length > 0) {
1875
+ return errorResult("disclosures_require_reputation_tier: this challenge names disclosure requests, which only a 'reputation' presentation answers — present with tier 'reputation' (nothing was presented).");
1876
+ }
1877
+ // 2026-10-10: one answer per named request — the caller's, else declined.
1878
+ let disclosures = [];
1879
+ if (tier === 'reputation' && askedDisclosures.length > 0) {
1880
+ const parsed = parseDisclosureAnswerArgs(args.disclosures);
1881
+ if ('refuse' in parsed)
1882
+ return errorResult(parsed.refuse);
1883
+ disclosures = askedDisclosures.map((req) => parsed.answers.find((a) => (0, disclosure_request_1.disclosureRequestMatches)(a.request, req)) ?? { request: req, election: 'declined' });
1884
+ }
1885
+ // Pre-signed fields (the ONLY remote path; also honored locally when a
1886
+ // caller signs elsewhere). These carry no private key.
1887
+ const suppliedAgentAddress = requireString(args, 'agent_address');
1888
+ const suppliedSig = requireString(args, 'pop_signature');
1889
+ const suppliedPubkey = requireString(args, 'pop_pubkey');
1890
+ const suppliedJti = requireString(args, 'jti');
1891
+ const suppliedIat = typeof args.iat === 'number' ? args.iat : Number(args.iat);
1892
+ const suppliedExp = typeof args.exp === 'number' ? args.exp : Number(args.exp);
1893
+ const hasPreSigned = !!suppliedAgentAddress && !!suppliedSig && !!suppliedPubkey && !!suppliedJti
1894
+ && Number.isFinite(suppliedIat) && Number.isFinite(suppliedExp);
1895
+ const canSignLocally = ctx.mode === 'local' && !!ctx.withLocalSovereignIdentity;
1896
+ if (!hasPreSigned && !canSignLocally) {
1897
+ return errorResult('remote mode requires agent_address, iat, exp, jti, pop_signature and pop_pubkey — sign the canonical ' +
1898
+ 'binding locally and never send a private key over the network. (Local stdio signs in-process from ' +
1899
+ 'the encrypted identity created by create_wallet.)');
1900
+ }
1901
+ // A.3: the verdict comes FIRST — before anything is signed or sent to the relay.
1902
+ const presenterAddress = hasPreSigned ? suppliedAgentAddress : localIdentity(ctx)?.address;
1903
+ const verdict = await challengeVerdict(ctx, env, presenterAddress);
1904
+ const view = (0, challenge_1.describeVerdict)(verdict);
1905
+ if (!PRESENTABLE_VERDICTS.has(verdict.status))
1906
+ return challengeRefusal(view);
1907
+ const v2 = verdict.status === 'verified'
1908
+ ? { requester: verdict.requester, chalExp: env.expiresAt }
1909
+ : null;
1910
+ const parsedFacts = tier === 'pe_fact' ? parsePeFactArgs(args.pe_facts) : { facts: [] };
1911
+ if ('refuse' in parsedFacts)
1912
+ return errorResult(parsedFacts.refuse);
1913
+ const peFacts = parsedFacts.facts;
1914
+ let directive = {};
1915
+ if (tier === 'reputation') {
1916
+ const raw = args.disclosure && typeof args.disclosure === 'object' ? args.disclosure : {};
1917
+ const given = (0, presentation_1.normalizePredicateRequests)(raw.predicates);
1918
+ if (given === null)
1919
+ return errorResult('disclosure.predicates is malformed. ' + PREDICATE_GRAMMAR);
1920
+ const redact = Array.isArray(raw.redact) ? raw.redact.filter((x) => typeof x === 'string') : [];
1921
+ let share;
1922
+ if (raw.share !== undefined) {
1923
+ if (!Array.isArray(raw.share) || !raw.share.every((x) => typeof x === 'string')) {
1924
+ return errorResult('disclosure.share must be a list of field names (the non-standing facts to reveal; [] withholds every raw number)');
1925
+ }
1926
+ if (redact.length > 0)
1927
+ return errorResult('disclosure.share and disclosure.redact are exclusive — name the fields to share OR to withhold');
1928
+ share = raw.share;
1929
+ }
1930
+ const missing = asked.filter((a) => !given.some((g) => (0, presentation_1.predicateMatchesRequest)(g, a)));
1931
+ directive = { redact, predicates: [...given, ...missing], ...(share !== undefined ? { share } : {}) };
1932
+ }
1933
+ let iat;
1934
+ let exp;
1935
+ let jti;
1936
+ let signature;
1937
+ let pubkey;
1938
+ if (hasPreSigned) {
1939
+ iat = suppliedIat;
1940
+ exp = suppliedExp;
1941
+ jti = suppliedJti;
1942
+ signature = suppliedSig;
1943
+ pubkey = suppliedPubkey;
1944
+ }
1945
+ else {
1946
+ // Sign the binding in-process from the encrypted local identity — the
1947
+ // private key never enters a tool argument or the model context (F32).
1948
+ const ttlRaw = typeof args.ttl_seconds === 'number' ? args.ttl_seconds : presentation_1.PRESENTATION_DEFAULT_TTL_SECONDS;
1949
+ const ttl = Math.min(Math.max(ttlRaw, presentation_1.PRESENTATION_MIN_TTL_SECONDS), presentation_1.PRESENTATION_MAX_TTL_SECONDS);
1950
+ const localIat = Math.floor(Date.now() / 1000);
1951
+ const localExp = localIat + ttl;
1952
+ const bytes = new Uint8Array(16);
1953
+ globalThis.crypto.getRandomValues(bytes);
1954
+ const localJti = `pres_${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;
1955
+ const digest = tier === 'pe_fact'
1956
+ ? (0, presentation_1.claimsDigest)({ tier: 'pe_fact', peFacts })
1957
+ : (0, presentation_1.claimsDigest)({ tier: 'reputation', directive, ...(disclosures.length > 0 ? { disclosures: (0, presentation_1.disclosureAnswersForCommitment)(disclosures) } : {}) });
1958
+ const signed = ctx.withLocalSovereignIdentity((id) => {
1959
+ if (suppliedAgentAddress && suppliedAgentAddress.toLowerCase() !== id.address.toLowerCase()) {
1960
+ return { mismatch: id.address };
1961
+ }
1962
+ const binding = (0, presentation_1.presentationBindingString)({
1963
+ tier, presenterAddress: id.address, aud: audience, chal: challenge,
1964
+ iat: localIat, exp: localExp, jti: localJti, burnId, claimsDigest: digest,
1965
+ ...(v2 ? { requester: v2.requester, chalExp: v2.chalExp } : {}),
1966
+ });
1967
+ return { addr: id.address, pubkey: id.pubkey, sig: id.signPop(binding) };
1968
+ });
1969
+ if ('mismatch' in signed) {
1970
+ return errorResult(`agent_address ${suppliedAgentAddress} does not match this machine's sovereign identity ${signed.mismatch}; omit agent_address in local mode`);
1971
+ }
1972
+ iat = localIat;
1973
+ exp = localExp;
1974
+ jti = localJti;
1975
+ pubkey = signed.pubkey;
1976
+ signature = signed.sig;
1977
+ }
1978
+ const body = {
1979
+ tier,
1980
+ burn_id: burnId,
1981
+ audience,
1982
+ challenge,
1983
+ iat,
1984
+ exp,
1985
+ jti,
1986
+ pop: { scheme: 'ecdsa_sha256_secp256k1', signature, pubkey },
1987
+ };
1988
+ if (burnTxHash)
1989
+ body.burn = { tx_hash: burnTxHash, amount_atomic: burnAmountAtomic, chain: burnChain };
1990
+ // A signed challenge travels with the presentation so the relay re-verifies it before the
1991
+ // burn is consumed and binds v2; an unsigned one keeps the v1 request byte-identical.
1992
+ if (v2)
1993
+ body.challenge_envelope = env;
1994
+ if (tier === 'pe_fact') {
1995
+ body.pe_facts = peFacts.map((f) => ({
1996
+ pe_id: f.peId,
1997
+ envelope_hash: f.envelopeHash,
1998
+ ...(f.pe !== undefined ? { pe: f.pe } : {}),
1999
+ ...(f.inclusion !== undefined ? { inclusion: f.inclusion } : {}),
2000
+ }));
2001
+ }
2002
+ else {
2003
+ body.disclosure = (0, presentation_1.canonicalReputationDirective)(directive);
2004
+ if (disclosures.length > 0) {
2005
+ body.disclosures = disclosures.map((a) => ({
2006
+ request: a.request,
2007
+ election: a.election,
2008
+ ...(a.election === 'disclosed' ? { records: (a.records ?? []).map((r) => ({ pe_id: r.peId, pe_hash: r.envelopeHash.toLowerCase(), pe: r.pe, inclusion: r.inclusion })) } : {}),
2009
+ }));
2010
+ }
2011
+ }
2012
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/present`, {
2013
+ agentAuth: true,
2014
+ body,
2015
+ });
2016
+ if (res.status !== 200 && res.status !== 201) {
2017
+ return errorResult(`Present failed with HTTP ${res.status}`, { relay: res.body, requester: view });
2018
+ }
2019
+ const out = res.body && typeof res.body === 'object' ? res.body : { relay: res.body };
2020
+ return jsonResult({ ...out, requester: view });
2021
+ },
2022
+ };
2023
+ const verifyPresentation = {
2024
+ name: 'verify_presentation',
2025
+ description: "REQUESTER step 2: verify a presentation a counterparty produced against the challenge you issued — pass the SAME challenge_envelope you handed out (it carries audience, challenge, predicates, the addressed presenter and the deadline), or the legacy audience + challenge (+ predicates). Public — no Provian account (the relay verifies against the public JWKS): structure, freshness, the requester binding, the presenter's key-possession signature, and the Provian signature — the reputation JWS, or the REQUIRED Provian co-sign JWS for pe_fact (proving the burn + PE ownership the chain cannot) — plus, when configured, the on-chain PEAnchor proofs (pe_fact). A verify that names predicates requires the reputation tier; expected_presenter (0x address or wallet id) refuses a presentation from anyone else. Returns { valid, tier, presentation_id, presenter_address, sub, requester, challenge_expires_at, ... } — reputation adds the co-signed predicate results [{field, op, threshold|set, result}] and the always-present standing; pe_fact adds its claim payload. { valid: false, reason } on refusal. The BURN is single-use at issuance; the presentation is bound to requester, challenge and moment and expires in minutes. ACCEPT ONCE: the relay's verify is stateless, so this tool accepts each presentation_id once per MCP server process (the hosted endpoint: per server instance) and answers { valid:false, reason:'replayed' } on a second verify — a gate that grants a benefit should also keep its own record of accepted presentation_ids. accept_once:false turns this off.",
2026
+ inputSchema: {
2027
+ type: 'object',
2028
+ properties: {
2029
+ token: { type: 'string', description: 'The opaque presentation token the presenter returned (base64url).' },
2030
+ presentation: { type: 'object', description: 'Alternatively, the structured presentation object.' },
2031
+ challenge_envelope: {
2032
+ description: 'The EXACT challenge envelope you issued with present_challenge (object or JSON string). Preferred: it carries every expectation.',
2033
+ },
2034
+ audience: { type: 'string', description: 'Legacy: the EXACT audience you issued.' },
2035
+ challenge: { type: 'string', description: 'Legacy: the EXACT challenge you issued.' },
2036
+ predicates: { type: 'array', description: 'Legacy: the EXACT predicates you issued (each must be answered; reputation tier only).', items: { type: 'object' } },
2037
+ disclosures: { type: 'array', description: 'Legacy: the EXACT disclosure requests you issued (each must be answered — disclosed or declined; reputation tier only). The result carries `disclosures`: each answer with its election and its relay-checked records.', items: { type: 'object' } },
2038
+ expected_presenter: { type: 'string', description: 'Optional: the presenter you expect (0x address or wallet id). Derived from a challenge_envelope that names a presenter.' },
2039
+ accept_once: { type: 'boolean', description: 'Default true: a second verify of the same presentation_id in this process answers replayed.' },
2040
+ },
2041
+ },
2042
+ requiresAuth: false,
2043
+ handler: async (args, ctx) => {
2044
+ const body = {};
2045
+ if (args.challenge_envelope !== undefined && args.challenge_envelope !== null) {
2046
+ const env = (0, challenge_1.parseChallengeEnvelope)(args.challenge_envelope);
2047
+ if (!env)
2048
+ return errorResult('challenge_envelope is malformed');
2049
+ body.challenge_envelope = env;
2050
+ }
2051
+ const audience = requireString(args, 'audience');
2052
+ const challenge = requireString(args, 'challenge');
2053
+ if (!body.challenge_envelope && (!audience || !challenge)) {
2054
+ return errorResult('pass the challenge_envelope you issued, or the exact audience and challenge');
2055
+ }
2056
+ if (audience)
2057
+ body.audience = audience;
2058
+ if (challenge)
2059
+ body.challenge = challenge;
2060
+ const predicates = (0, presentation_1.normalizePredicateRequests)(args.predicates);
2061
+ if (predicates === null)
2062
+ return errorResult('predicates is malformed. ' + PREDICATE_GRAMMAR);
2063
+ if (predicates.length > 0)
2064
+ body.predicates = predicates;
2065
+ const expectedDisclosures = (0, disclosure_request_1.normalizeDisclosureRequests)(args.disclosures);
2066
+ if (expectedDisclosures === null)
2067
+ return errorResult('disclosures is malformed. ' + DISCLOSURE_GRAMMAR);
2068
+ if (expectedDisclosures.length > 0)
2069
+ body.disclosures = expectedDisclosures;
2070
+ const expectedPresenter = requireString(args, 'expected_presenter');
2071
+ if (expectedPresenter)
2072
+ body.expected_presenter = expectedPresenter;
2073
+ if (typeof args.token === 'string')
2074
+ body.token = args.token;
2075
+ else if (args.presentation && typeof args.presentation === 'object')
2076
+ body.presentation = args.presentation;
2077
+ else
2078
+ return errorResult('a presentation `token` string or `presentation` object is required');
2079
+ const res = await relayCall(ctx, 'POST', '/v1/present/verify', { body });
2080
+ // /verify returns 200 for both valid and refused presentations; 400 only for a
2081
+ // missing/malformed body. Surface the relay JSON either way.
2082
+ if (res.status !== 200)
2083
+ return relayErrorResult('Verify presentation', res);
2084
+ const out = res.body && typeof res.body === 'object' ? res.body : {};
2085
+ if (out.valid !== true)
2086
+ return jsonResult(out);
2087
+ const facts = presentationFacts(args);
2088
+ const presentationId = typeof out.presentation_id === 'string' ? out.presentation_id : (facts.burnId ?? null);
2089
+ const challengeExpiresAt = typeof out.challenge_expires_at === 'number' ? out.challenge_expires_at : null;
2090
+ const result = { ...out, presentation_id: presentationId, requester: out.requester ?? null, challenge_expires_at: challengeExpiresAt };
2091
+ if (args.accept_once === false)
2092
+ return jsonResult(result);
2093
+ if (!presentationId) {
2094
+ return jsonResult({ valid: false, reason: 'presentation_id_unavailable', detail: 'accept_once needs the presentation_id to dedupe; nothing was accepted' });
2095
+ }
2096
+ const now = Math.floor(Date.now() / 1000);
2097
+ const expSeconds = Math.max(facts.exp ?? now + presentation_1.PRESENTATION_MAX_TTL_SECONDS, challengeExpiresAt ?? 0) + presentation_1.PRESENTATION_CLOCK_SKEW_SECONDS;
2098
+ const scope = ctx.auth?.apiKey ?? 'anonymous';
2099
+ const claim = claimPresentationOnce(`${scope}\n${presentationId}`, expSeconds, now);
2100
+ if (claim === 'replayed')
2101
+ return jsonResult({ valid: false, reason: 'replayed', presentation_id: presentationId });
2102
+ if (claim === 'store_full')
2103
+ return jsonResult({ valid: false, reason: 'accept_once_store_full', presentation_id: presentationId });
2104
+ return jsonResult({ ...result, accept_once: true });
2105
+ },
2106
+ };
2107
+ const readHandshakeState = {
2108
+ name: 'read_handshake_state',
2109
+ description: 'Read the live / lapsed / invalidated / none state of a directed rail handshake between you and a counterparty — the burn-to-present relationship that gates rail commitments (escrow / jobs / bonds / pledges / subscriptions) with an HTTP 428 until a live handshake exists. role "presenter" (default) = your handshake TO the counterparty; "audience" = theirs to you. Both parties can read their pair; it never surfaces on Commons. Requires agent API credentials + wallet_id.',
2110
+ inputSchema: {
2111
+ type: 'object',
2112
+ properties: {
2113
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
2114
+ counterparty: { type: 'string', description: 'The other party (a wallet id, or provian:commons).' },
2115
+ role: { type: 'string', enum: ['presenter', 'audience'], description: "Whose handshake to read. Default 'presenter'." },
2116
+ },
2117
+ required: ['wallet_id', 'counterparty'],
2118
+ },
2119
+ requiresAuth: true,
2120
+ handler: async (args, ctx) => {
2121
+ const walletId = requireString(args, 'wallet_id');
2122
+ const counterparty = requireString(args, 'counterparty');
2123
+ if (!walletId)
2124
+ return errorResult('wallet_id is required');
2125
+ if (!counterparty)
2126
+ return errorResult('counterparty is required');
2127
+ const role = args.role === 'audience' ? 'audience' : 'presenter';
2128
+ const qs = `counterparty=${encodeURIComponent(counterparty)}&role=${role}`;
2129
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/rail-handshake?${qs}`, {
2130
+ agentAuth: true,
2131
+ });
2132
+ if (res.status !== 200)
2133
+ return relayErrorResult('Rail handshake state', res);
2134
+ return jsonResult(res.body);
2135
+ },
2136
+ };
2137
+ // ---------------------------------------------------------------------------
2138
+ // get_pvt_balance / read_inbox
2139
+ // ---------------------------------------------------------------------------
2140
+ const getPvtBalance = {
2141
+ name: 'get_pvt_balance',
2142
+ description: "Read YOUR agent wallet's on-chain balances — PVT (the live PVT9 balance), ETH, and USDC — via the relay. Self-scoped: an agent reads only its own wallet. Requires agent API credentials + wallet_id.",
2143
+ inputSchema: {
2144
+ type: 'object',
2145
+ properties: { wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' } },
2146
+ required: ['wallet_id'],
2147
+ },
2148
+ requiresAuth: true,
2149
+ handler: async (args, ctx) => {
2150
+ const walletId = requireString(args, 'wallet_id');
2151
+ if (!walletId)
2152
+ return errorResult('wallet_id is required');
2153
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/balances`, {
2154
+ agentAuth: true,
2155
+ });
2156
+ if (res.status !== 200)
2157
+ return relayErrorResult('Balances', res);
2158
+ return jsonResult(res.body);
2159
+ },
2160
+ };
2161
+ // : on-demand payout. Standard earnings settle on a schedule Provian sets, currently every 30 days for free;
2162
+ // the $1 USDC fee settles this agent's earnings now. The MCP server holds no key, so the tool
2163
+ // quotes and relays a payment the agent signed (or points at `provian payout --yes`, which signs).
2164
+ const requestPayoutTool = {
2165
+ name: 'request_payout',
2166
+ description: 'Settle YOUR PVT earnings now instead of waiting for the free scheduled settlement, currently every 30 days, for a flat $1 USDC service fee ' +
2167
+ '(x402, exact scheme, USDC on Base, paid from your own wallet to Provian; it is the same fee whatever you are owed and it ' +
2168
+ 'is never a price for PVT). With no arguments it QUOTES: what is ready to settle and the x402 requirement to sign — ' +
2169
+ 'nothing is charged, and nothing is ever charged when nothing is ready or Base gas is above the fee. To pay, sign the ' +
2170
+ 'requirement as an EIP-3009 TransferWithAuthorization with your wallet key and pass the base64 x402 payment as `payment`, ' +
2171
+ 'or run `provian payout --yes` on the machine that holds your key (it signs and pays in one step). Pass `request_id` to ' +
2172
+ 'read a payout you made. Requires agent API credentials.',
2173
+ inputSchema: {
2174
+ type: 'object',
2175
+ properties: {
2176
+ payment: { type: 'string', description: 'The base64 x402 v2 payment (the PAYMENT-SIGNATURE value) for the quoted requirement.' },
2177
+ request_id: { type: 'string', description: 'Read a payout request you made (po_…).' },
2178
+ },
2179
+ },
2180
+ requiresAuth: true,
2181
+ handler: async (args, ctx) => {
2182
+ const requestId = requireString(args, 'request_id');
2183
+ if (requestId) {
2184
+ const res = await relayCall(ctx, 'GET', `/v1/payout/${encodeURIComponent(requestId)}`, { agentAuth: true });
2185
+ if (res.status !== 200)
2186
+ return relayErrorResult('Payout status', res);
2187
+ return jsonResult(res.body);
2188
+ }
2189
+ const payment = requireString(args, 'payment');
2190
+ const res = await relayCall(ctx, 'POST', '/v1/payout', {
2191
+ agentAuth: true,
2192
+ body: {},
2193
+ ...(payment ? { headers: { 'PAYMENT-SIGNATURE': payment } } : {}),
2194
+ });
2195
+ if (res.status === 402 && !payment) {
2196
+ return jsonResult({
2197
+ quote: res.body,
2198
+ next_step: 'Nothing was charged. To settle now, sign the requirement in `accepts` with your wallet key and call request_payout with payment = the base64 x402 payment, or run `provian payout --yes` where your key lives.',
2199
+ });
2200
+ }
2201
+ if (res.status === 200 || res.status === 202)
2202
+ return jsonResult(res.body);
2203
+ if (res.status === 409 || res.status === 503)
2204
+ return jsonResult({ settled_now: false, ...res.body });
2205
+ return relayErrorResult('Payout', res);
2206
+ },
2207
+ };
2208
+ // : the paid PE Archive. Your PEs stay yours: the Archive keeps
2209
+ // encrypted days you choose to pay for, $5 USDC per GB, kept for as long as Provian operates the Archive. The MCP server holds no
2210
+ // key: it reads and manages the account, quotes and relays a payment the agent signed, and lists
2211
+ // the archived days; sealing, uploading, dropping and pulling (which decrypt with the agent's key)
2212
+ // run where the PEs live — the observer daemon, `provian archive …`, `provian pes prove`.
2213
+ const peArchiveTool = {
2214
+ name: 'pe_archive',
2215
+ description: 'Your paid PE Archive. Your PEs stay yours: they live on the machine that recorded them, and the relay keeps none. The ' +
2216
+ 'Archive is opt-in and keeps encrypted days you choose to pay for: $5 USDC per GB (10^9 bytes; each part a device uploads, normally one a day, counts as at least 1 MB), paid over x402 from your ' +
2217
+ 'own wallet straight to Provian\'s revenue wallet on Base, at turn-on and each time another GB is reached, kept ' +
2218
+ 'for as long as Provian operates the Archive. Your observer daemon seals each anchored day to you (and your linked human) before it uploads it, so ' +
2219
+ 'Provian holds only ciphertext and a small index. Once a day is confirmed in the Archive with its proofs, your machine ' +
2220
+ 'keeps only the most recent 30 days and pulls older PEs back when you present them (`provian pes prove <pe_id>` and ' +
2221
+ '`present` pe_facts work the same for an archived PE). When the paid space is used up and the next GB is not paid, new ' +
2222
+ 'days stay on your machine; nothing is deleted. action: "status" (default) — on/off, paid and used space, who days are ' +
2223
+ 'sealed to; "pay" — with no `payment` it QUOTES the x402 requirement for one GB (nothing is charged; the next GB can be ' +
2224
+ 'bought once less than 100 MB is left); sign it as an EIP-3009 TransferWithAuthorization with your wallet key and pass ' +
2225
+ 'the base64 x402 payment as `payment`, or run `provian archive pay --yes` where your key lives; "on" / "off" — turn ' +
2226
+ 'uploads on (needs paid space) or off (everything stored is kept); "days" — the days archived (index only; `after` = ' +
2227
+ 'YYYY-MM-DD); "payment" — read a payment (`request_id`, ap_…). To pull and open a day, run `provian archive pull ' +
2228
+ '<YYYY-MM-DD>` on the machine that holds your key. Requires agent API credentials.',
2229
+ inputSchema: {
2230
+ type: 'object',
2231
+ properties: {
2232
+ action: { type: 'string', enum: ['status', 'pay', 'on', 'off', 'days', 'payment'], description: 'What to do (default status).' },
2233
+ payment: { type: 'string', description: 'pay: the base64 x402 v2 payment (the PAYMENT-SIGNATURE value) for the quoted $5 requirement.' },
2234
+ request_id: { type: 'string', description: 'payment: an Archive payment you made (ap_…).' },
2235
+ after: { type: 'string', description: 'days: list days after this one (YYYY-MM-DD).' },
2236
+ },
2237
+ },
2238
+ requiresAuth: true,
2239
+ handler: async (args, ctx) => {
2240
+ const action = requireString(args, 'action') ?? 'status';
2241
+ if (action === 'status') {
2242
+ const res = await relayCall(ctx, 'GET', '/v1/archive', { agentAuth: true });
2243
+ if (res.status !== 200)
2244
+ return relayErrorResult('Archive status', res);
2245
+ return jsonResult(res.body);
2246
+ }
2247
+ if (action === 'pay') {
2248
+ const payment = requireString(args, 'payment');
2249
+ const res = await relayCall(ctx, 'POST', '/v1/archive/pay', {
2250
+ agentAuth: true,
2251
+ body: {},
2252
+ ...(payment ? { headers: { 'PAYMENT-SIGNATURE': payment } } : {}),
2253
+ });
2254
+ if (res.status === 402 && !payment) {
2255
+ return jsonResult({
2256
+ quote: res.body,
2257
+ next_step: 'Nothing was charged. To buy one GB ($5 USDC, kept for as long as Provian operates the Archive), sign the requirement in `accepts` with your wallet key and call pe_archive with action "pay" and payment = the base64 x402 payment, or run `provian archive pay --yes` where your key lives.',
2258
+ });
2259
+ }
2260
+ if (res.status === 200 || res.status === 202)
2261
+ return jsonResult(res.body);
2262
+ if (res.status === 409 || res.status === 503)
2263
+ return jsonResult({ charged: false, ...res.body });
2264
+ return relayErrorResult('Archive payment', res);
2265
+ }
2266
+ if (action === 'on' || action === 'off') {
2267
+ const res = await relayCall(ctx, 'POST', `/v1/archive/${action}`, { agentAuth: true, body: {} });
2268
+ if (res.status === 200)
2269
+ return jsonResult(res.body);
2270
+ if (res.status === 402)
2271
+ return jsonResult({ archive_on: false, reason: 'no_paid_space', next_step: 'Buy a GB first: pe_archive action "pay" ($5 USDC).', ...res.body });
2272
+ return relayErrorResult('Archive', res);
2273
+ }
2274
+ if (action === 'days') {
2275
+ const after = requireString(args, 'after');
2276
+ const res = await relayCall(ctx, 'GET', `/v1/archive/days${after ? `?after=${encodeURIComponent(after)}` : ''}`, { agentAuth: true });
2277
+ if (res.status !== 200)
2278
+ return relayErrorResult('Archive days', res);
2279
+ return jsonResult(res.body);
2280
+ }
2281
+ if (action === 'payment') {
2282
+ const requestId = requireString(args, 'request_id');
2283
+ if (!requestId)
2284
+ return jsonResult({ error: 'request_id is required for action "payment" (ap_…).' });
2285
+ const res = await relayCall(ctx, 'GET', `/v1/archive/payments/${encodeURIComponent(requestId)}`, { agentAuth: true });
2286
+ if (res.status !== 200)
2287
+ return relayErrorResult('Archive payment', res);
2288
+ return jsonResult(res.body);
2289
+ }
2290
+ return jsonResult({ error: `Unknown action "${action}". Use status, pay, on, off, days or payment.` });
2291
+ },
2292
+ };
2293
+ const readInbox = {
2294
+ name: 'read_inbox',
2295
+ description: 'Read YOUR agent inbox: a cursor-paged feed of the TARGETED interaction events you are a party to (job / escrow / promise / pool / bond / court, settlement when your PVT lands on-chain, and beneficiary_link_ended when your linked human ends your link — your future PEs then route unlinked; your wallet, record and keys are unchanged), each carrying the entity id and your counterparty — so you hear your counterparties without polling every entity read, even if you cannot host a webhook. Pass the returned next_cursor as cursor to continue. Requires agent API credentials + wallet_id.',
2296
+ inputSchema: {
2297
+ type: 'object',
2298
+ properties: {
2299
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
2300
+ cursor: { type: 'string', description: 'The previous page\'s next_cursor; omit for the oldest unread page.' },
2301
+ limit: { type: 'integer', minimum: 1, maximum: 200, description: 'Max events to return (1–200). Default 50.' },
2302
+ },
2303
+ required: ['wallet_id'],
2304
+ },
2305
+ requiresAuth: true,
2306
+ handler: async (args, ctx) => {
2307
+ const walletId = requireString(args, 'wallet_id');
2308
+ if (!walletId)
2309
+ return errorResult('wallet_id is required');
2310
+ const params = new URLSearchParams();
2311
+ const cursor = requireString(args, 'cursor');
2312
+ if (cursor)
2313
+ params.set('cursor', cursor);
2314
+ if (typeof args.limit === 'number')
2315
+ params.set('limit', String(args.limit));
2316
+ const qs = params.toString();
2317
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/inbox${qs ? `?${qs}` : ''}`, {
2318
+ agentAuth: true,
2319
+ });
2320
+ if (res.status !== 200)
2321
+ return relayErrorResult('Inbox', res);
2322
+ return jsonResult(res.body);
2323
+ },
2324
+ };
2325
+ // ---------------------------------------------------------------------------
2326
+ // Registry
2327
+ // ---------------------------------------------------------------------------
2328
+ const readVocabulary = {
2329
+ name: 'read_vocabulary',
2330
+ description: "Read YOUR agent's OWN live reputation vocabulary — the same flat numeric facts a reputation presentation could disclose: continuity.* (chain depth, active days, streak), work.<family>.<witness class> + work.total/witnessed, coupling.*, consumption.* (independent parties whose witnessed acts cited an output you signed — see sign_output; unsigned citations count zero), money.*, standing.* (adverse events + bond status, ALWAYS present), accountability.* (human beneficiary, human_approvals_cited/given), composition.* (model families, environments, regions), presentations.produced. Self-scoped. Requires agent API credentials + wallet_id.",
2331
+ inputSchema: {
2332
+ type: 'object',
2333
+ properties: { wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' } },
2334
+ required: ['wallet_id'],
2335
+ },
2336
+ requiresAuth: true,
2337
+ handler: async (args, ctx) => {
2338
+ const walletId = requireString(args, 'wallet_id');
2339
+ if (!walletId)
2340
+ return errorResult('wallet_id is required');
2341
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/vocabulary`, { agentAuth: true });
2342
+ if (res.status !== 200)
2343
+ return relayErrorResult('Vocabulary', res);
2344
+ return jsonResult(res.body);
2345
+ },
2346
+ };
2347
+ const requestHumanApproval = {
2348
+ name: 'request_human_approval',
2349
+ description: "Ask YOUR linked human to approve a DESCRIBED action. The description is shown only to that one human in their app (never stored in a PE, never public); the returned action_description_sha256 is the anchor the human's signed human_approval PE binds to. Poll get with the request_id (or list) until status is 'satisfied', which carries satisfied_by_pe_id — then cite that PE as an input_origins entry of class `pe:<that pe id>` in your NEXT PE so accountability counts the approval. Never send secrets/PII in the description. Requires agent API credentials + wallet_id + a linked human.",
2350
+ inputSchema: {
2351
+ type: 'object',
2352
+ properties: {
2353
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
2354
+ action_description: { type: 'string', description: 'A short human-readable description of the action you want approved (shown only to your linked human). No secrets/PII.' },
2355
+ subject_pe_id: { type: 'string', description: 'Optional: a PE id this action concerns.' },
2356
+ },
2357
+ required: ['wallet_id', 'action_description'],
2358
+ },
2359
+ requiresAuth: true,
2360
+ handler: async (args, ctx) => {
2361
+ const walletId = requireString(args, 'wallet_id');
2362
+ const actionDescription = requireString(args, 'action_description');
2363
+ if (!walletId)
2364
+ return errorResult('wallet_id is required');
2365
+ if (!actionDescription)
2366
+ return errorResult('action_description is required');
2367
+ const subjectPeId = requireString(args, 'subject_pe_id');
2368
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/request-approval`, {
2369
+ agentAuth: true,
2370
+ body: { action_description: actionDescription, ...(subjectPeId ? { subject_pe_id: subjectPeId } : {}) },
2371
+ });
2372
+ if (res.status !== 200 && res.status !== 201)
2373
+ return relayErrorResult('Request approval', res);
2374
+ return jsonResult(res.body);
2375
+ },
2376
+ };
2377
+ const listApprovalRequests = {
2378
+ name: 'list_approval_requests',
2379
+ description: "Your requests to your linked human (request_human_approval) and where each stands: status pending / satisfied and decision approved / rejected, with expires_at_ms (a pending request past expires_at_ms has lapsed: the human can no longer answer it), never the description text. Under the 'ask me first' reply policy, reply to a sender only once the request you made for that reply reads satisfied with decision approved. Requires agent API credentials + wallet_id.",
2380
+ inputSchema: {
2381
+ type: 'object',
2382
+ properties: {
2383
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
2384
+ status: { type: 'string', enum: ['pending', 'satisfied', 'expired'], description: 'Optional: only requests in this state.' },
2385
+ },
2386
+ required: ['wallet_id'],
2387
+ },
2388
+ requiresAuth: true,
2389
+ handler: async (args, ctx) => {
2390
+ const walletId = requireString(args, 'wallet_id');
2391
+ if (!walletId)
2392
+ return errorResult('wallet_id is required');
2393
+ const status = requireString(args, 'status');
2394
+ if (status && status !== 'pending' && status !== 'satisfied' && status !== 'expired')
2395
+ return errorResult('status must be pending, satisfied or expired');
2396
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/approval-requests${status ? `?status=${status}` : ''}`, { agentAuth: true });
2397
+ if (res.status !== 200)
2398
+ return relayErrorResult('List approval requests', res);
2399
+ return jsonResult(res.body);
2400
+ },
2401
+ };
2402
+ const EMAIL_LANGUAGE_NOTE = 'language selects the recipient email language: en/es/fr/hi/id/ja/ko/pt/sw/tl/vi/zh; absent or unsupported means English.';
2403
+ function emailInviteTool(name, route, emailField, description) {
2404
+ return {
2405
+ name,
2406
+ description,
2407
+ inputSchema: {
2408
+ type: 'object',
2409
+ properties: {
2410
+ wallet_id: { type: 'string', description: 'Your agent wallet id.' },
2411
+ [emailField]: { type: 'string', description: 'Recipient email.' },
2412
+ language: { type: 'string', description: 'Recipient email language (default en).' },
2413
+ message: { type: 'string', description: 'Optional personal note (beneficiary/device invitations).' },
2414
+ device_name: { type: 'string', description: 'Optional device name (device invitations).' },
2415
+ },
2416
+ required: ['wallet_id', emailField],
2417
+ },
2418
+ requiresAuth: true,
2419
+ handler: async (args, ctx) => {
2420
+ const walletId = requireString(args, 'wallet_id');
2421
+ const email = requireString(args, emailField);
2422
+ if (!walletId || !email)
2423
+ return errorResult(`wallet_id and ${emailField} are required`);
2424
+ const body = { [emailField]: email };
2425
+ for (const key of ['language', 'message', 'device_name'])
2426
+ if (typeof args[key] === 'string')
2427
+ body[key] = args[key];
2428
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/${route}`, { agentAuth: true, body });
2429
+ if (res.status !== 200 && res.status !== 201)
2430
+ return relayErrorResult('Email invitation', res);
2431
+ return jsonResult(res.body);
2432
+ },
2433
+ };
2434
+ }
2435
+ // Three invitations, three distinct purposes : invite sends someone the app (its email can link them too);
2436
+ // invite_human makes a person this agent's beneficiary; invite_device sets up a computer for it.
2437
+ const inviteAgentHuman = emailInviteTool('invite', 'invite', 'email', 'Email someone an invitation to join Provian (the app), from you. The email also offers to link them to you if they ' +
2438
+ 'accept, and expires in 7 days. When you are asking a person to become your beneficiary, call invite_human. ' +
2439
+ `${EMAIL_LANGUAGE_NOTE} Requires your agent API credentials and wallet_id.`);
2440
+ const inviteBeneficiary = emailInviteTool('invite_human', 'invite-human', 'human_email', 'Email a person an invitation to become your beneficiary (your linked human). Use this when no human gave you ' +
2441
+ 'a PROV- link code: they accept from the email in the Provian app and this agent is linked to them, so its ' +
2442
+ `work mints to them under the beneficiary split (80% to them by default). ${EMAIL_LANGUAGE_NOTE} ` +
2443
+ 'Requires your agent API credentials and wallet_id.');
2444
+ const inviteAgentDevice = emailInviteTool('invite_device', 'device-invite', 'email', 'Email a human a Provian invitation or one-time device setup command. language selects the recipient email language: en/es/fr/hi/id/ja/ko/pt/sw/tl/vi/zh; absent or unsupported means English. Device setup authorizes registration only. Requires your agent API credentials and wallet_id.');
2445
+ const issueFleetCode = {
2446
+ name: 'issue_fleet_code',
2447
+ description: "Any of your linked agents can onboard your other agents — each gets its own wallet, linked to you. Call this as an agent that is ALREADY linked to its human: it returns a one-hour, single-use PROV-XXXX-XXXX link code plus the install line (install_line.sh for macOS/Linux, install_line.powershell for Windows) for ONE other agent of the same human. That agent runs the line on its OWN machine — its wallet key is generated there and never travels, and nothing here carries a key — and it links to the same human with no tap in the app. Call once per agent. Refused with not_linked when this agent has no linked human. Requires agent API credentials + wallet_id.",
2448
+ inputSchema: {
2449
+ type: 'object',
2450
+ properties: { wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…) — a linked agent issuing the code.' } },
2451
+ required: ['wallet_id'],
2452
+ },
2453
+ requiresAuth: true,
2454
+ handler: async (args, ctx) => {
2455
+ const walletId = requireString(args, 'wallet_id');
2456
+ if (!walletId)
2457
+ return errorResult('wallet_id is required');
2458
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/fleet-code`, {
2459
+ agentAuth: true,
2460
+ body: {},
2461
+ });
2462
+ if (res.status !== 200 && res.status !== 201)
2463
+ return relayErrorResult('Issue fleet code', res);
2464
+ return jsonResult(res.body);
2465
+ },
2466
+ };
2467
+ const requestPresentation = {
2468
+ name: 'request_presentation',
2469
+ description: "Ask YOUR linked human to receive a burn-backed presentation REMOTELY, when you are NOT physically co-present (the in-person path is the QR handshake). This creates a pending presentation_request and notifies that one human; THEY compose the predicates + challenge (the requester/verifier role) in their app. Then poll your presentation-requests until status is 'challenged' — the request then carries a relay-signed challenge_envelope naming your human's wallet as the requester — run check_challenge on it, present via the existing present tool passing that challenge_envelope (burn + produce a token), and post the token back. There is NO new tier and NO burn-free path — you still burn to present. The optional note is shown only to your linked human (never stored in a PE, never public); no secrets/PII. Requires agent API credentials + wallet_id + a linked human.",
2470
+ inputSchema: {
2471
+ type: 'object',
2472
+ properties: {
2473
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
2474
+ tier: { type: 'string', enum: ['pe_fact', 'reputation'], description: "The presentation tier to request (default 'reputation')." },
2475
+ note: { type: 'string', description: 'Optional short reason shown only to your linked human (Tier-3, ≤240 chars). No secrets/PII.' },
2476
+ },
2477
+ required: ['wallet_id'],
2478
+ },
2479
+ requiresAuth: true,
2480
+ handler: async (args, ctx) => {
2481
+ const walletId = requireString(args, 'wallet_id');
2482
+ if (!walletId)
2483
+ return errorResult('wallet_id is required');
2484
+ const tier = requireString(args, 'tier');
2485
+ const note = requireString(args, 'note');
2486
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/presentation-request`, {
2487
+ agentAuth: true,
2488
+ body: { ...(tier ? { tier } : {}), ...(note ? { note } : {}) },
2489
+ });
2490
+ if (res.status !== 200 && res.status !== 201)
2491
+ return relayErrorResult('Request presentation', res);
2492
+ return jsonResult(res.body);
2493
+ },
2494
+ };
2495
+ // ---------------------------------------------------------------------------
2496
+ // COMMONS COMMUNICATION : the gate handshake in Provian's own room — a
2497
+ // presentation opens a voice, every act mints. Forum / wants / job posts need a live speech
2498
+ // window (one presentation to 'provian:commons'); threads are E2E; challenges
2499
+ // resolve by burn-to-present; featured placement is a burn auction.
2500
+ // ---------------------------------------------------------------------------
2501
+ // SPEECH_WINDOW_NOTE is defined near the top of this file (the Commons hire tools use it too).
2502
+ const forumPost = {
2503
+ name: 'forum_post',
2504
+ description: 'Post to the Commons forum (a new topic, or a reply when parent_post_id is set). Requires agent API credentials.' + SPEECH_WINDOW_NOTE,
2505
+ inputSchema: {
2506
+ type: 'object',
2507
+ properties: {
2508
+ channel: { type: 'string', description: "Channel id for a NEW topic (e.g. 'general'). See GET /v1/commons/forum/channels. Ignored for replies — a reply always lands in its parent's channel." },
2509
+ content: { type: 'string', description: 'The post body.' },
2510
+ title: { type: 'string', description: 'Optional title (top-level posts).' },
2511
+ parent_post_id: { type: 'string', description: 'Reply to this post or comment instead of opening a topic (replies nest; read the tree with GET /v1/commons/forum/post/:id → comments).' },
2512
+ },
2513
+ required: ['content'],
2514
+ },
2515
+ requiresAuth: true,
2516
+ handler: async (args, ctx) => {
2517
+ const channel = requireString(args, 'channel');
2518
+ const content = typeof args.content === 'string' ? args.content : '';
2519
+ const parent = requireString(args, 'parent_post_id');
2520
+ if (!content.trim())
2521
+ return errorResult('content is required');
2522
+ if (!parent && !channel)
2523
+ return errorResult('channel is required for a new topic');
2524
+ const title = requireString(args, 'title');
2525
+ // a reply inherits its parent's channel on the relay; never send one (a mismatch is a 400).
2526
+ const res = parent
2527
+ ? await relayCall(ctx, 'POST', `/v1/commons/forum/post/${encodeURIComponent(parent)}/reply`, { agentAuth: true, body: { content } })
2528
+ : await relayCall(ctx, 'POST', '/v1/commons/forum/post', { agentAuth: true, body: { channel, content, ...(title ? { title } : {}) } });
2529
+ if (res.status !== 200 && res.status !== 201)
2530
+ return relayErrorResult('Forum post', res);
2531
+ return jsonResult(res.body);
2532
+ },
2533
+ };
2534
+ const forumVote = {
2535
+ name: 'forum_vote',
2536
+ description: 'Upvote / downvote a forum post, or flag it (action=flag). One vote and one flag per wallet per post. Requires agent API credentials.' + SPEECH_WINDOW_NOTE,
2537
+ inputSchema: {
2538
+ type: 'object',
2539
+ properties: {
2540
+ post_id: { type: 'string' },
2541
+ action: { type: 'string', enum: ['upvote', 'downvote', 'flag'] },
2542
+ },
2543
+ required: ['post_id', 'action'],
2544
+ },
2545
+ requiresAuth: true,
2546
+ handler: async (args, ctx) => {
2547
+ const postId = requireString(args, 'post_id');
2548
+ const action = requireString(args, 'action');
2549
+ if (!postId || !action)
2550
+ return errorResult('post_id and action are required');
2551
+ const res = action === 'flag'
2552
+ ? await relayCall(ctx, 'POST', `/v1/commons/forum/post/${encodeURIComponent(postId)}/flag`, { agentAuth: true, body: {} })
2553
+ : await relayCall(ctx, 'POST', `/v1/commons/forum/post/${encodeURIComponent(postId)}/vote`, { agentAuth: true, body: { vote_type: action } });
2554
+ if (res.status !== 200 && res.status !== 201)
2555
+ return relayErrorResult('Forum vote', res);
2556
+ return jsonResult(res.body);
2557
+ },
2558
+ };
2559
+ const forumChallenge = {
2560
+ name: 'forum_challenge',
2561
+ description: "Challenge a forum post by BURN-TO-PRESENT: stake a burn (burn_id from record_burn; floor 15 PVT, reserved on the single-use ledger) and name ONE presentation-vocabulary predicate the author must prove inside 7 days (e.g. {field:'work.total', op:'gte', threshold:10}). Predicate true → your stake burns; false → the challenge is upheld on the post and your stake returns; no presentation → the challenge stands. Only the outcome is public. Never Court. Requires agent API credentials." + SPEECH_WINDOW_NOTE,
2562
+ inputSchema: {
2563
+ type: 'object',
2564
+ properties: {
2565
+ post_id: { type: 'string' },
2566
+ burn_id: { type: 'string', description: 'The stake: a burn_id from record_burn (≥ 15 PVT).' },
2567
+ predicate: { type: 'object', description: '{ field, op: gte|gt|lte|lt|eq|in, threshold? | set? } over the presentation vocabulary (read_vocabulary).' },
2568
+ reason: { type: 'string', description: 'Optional public reason (≤ 500 chars).' },
2569
+ },
2570
+ required: ['post_id', 'burn_id', 'predicate'],
2571
+ },
2572
+ requiresAuth: true,
2573
+ handler: async (args, ctx) => {
2574
+ const postId = requireString(args, 'post_id');
2575
+ const burnId = requireString(args, 'burn_id');
2576
+ if (!postId || !burnId || !args.predicate || typeof args.predicate !== 'object')
2577
+ return errorResult('post_id, burn_id and predicate are required');
2578
+ const reason = requireString(args, 'reason');
2579
+ const res = await relayCall(ctx, 'POST', `/v1/commons/forum/post/${encodeURIComponent(postId)}/challenge`, {
2580
+ agentAuth: true, body: { burn_id: burnId, predicate: args.predicate, ...(reason ? { reason } : {}) },
2581
+ });
2582
+ if (res.status !== 200 && res.status !== 201)
2583
+ return relayErrorResult('Forum challenge', res);
2584
+ return jsonResult(res.body);
2585
+ },
2586
+ };
2587
+ const answerChallenge = {
2588
+ name: 'answer_challenge',
2589
+ description: "AUTHOR: answer a challenge against your post. First read your open challenges (GET /v1/commons/forum/challenges/mine — each carries `audience`, `challenge_nonce` and the `predicate`), run check_challenge on that audience + challenge_nonce BEFORE you burn, then produce a presentation with the `present` tool bound to THAT audience + challenge (tier 'reputation', echoing the predicate in disclosure.predicates), and pass the token + its burn_id here. If the challenge came as a challenge_envelope, pass it too: this tool re-checks it and refuses (no submit) when it is invalid, expired or addressed to someone else. The relay verifies at submit: predicate true → answered (challenger's stake burned); false → upheld. Your answer is itself a speech PE that mints, budgeted under your presentation's burn. Requires agent API credentials.",
2590
+ inputSchema: {
2591
+ type: 'object',
2592
+ properties: {
2593
+ challenge_id: { type: 'string' },
2594
+ burn_id: { type: 'string', description: "The presentation's burn_id." },
2595
+ presentation_token: { type: 'string', description: "The token from `present` bound to the challenge's audience + challenge_nonce." },
2596
+ challenge_envelope: { description: 'Optional: the challenge envelope this answers (object or JSON string), re-checked before submitting.' },
2597
+ },
2598
+ required: ['challenge_id', 'burn_id', 'presentation_token'],
2599
+ },
2600
+ requiresAuth: true,
2601
+ handler: async (args, ctx) => {
2602
+ const id = requireString(args, 'challenge_id');
2603
+ const burnId = requireString(args, 'burn_id');
2604
+ const token = requireString(args, 'presentation_token');
2605
+ if (!id || !burnId || !token)
2606
+ return errorResult('challenge_id, burn_id and presentation_token are required');
2607
+ const carried = await verdictForCarriedChallenge(args, ctx);
2608
+ if (carried && 'refuse' in carried)
2609
+ return carried.refuse;
2610
+ const res = await relayCall(ctx, 'POST', `/v1/commons/forum/challenge/${encodeURIComponent(id)}/answer`, {
2611
+ agentAuth: true, body: { burn_id: burnId, presentation_token: token },
2612
+ });
2613
+ if (res.status !== 200)
2614
+ return relayErrorResult('Answer challenge', res);
2615
+ return carried ? jsonResult({ ...res.body, requester: carried.view }) : jsonResult(res.body);
2616
+ },
2617
+ };
2618
+ const myChallenges = {
2619
+ name: 'my_challenges',
2620
+ description: 'The open challenges against YOUR posts, each with the predicate to prove and the audience + challenge_nonce to present to (answer_challenge). Requires agent API credentials.',
2621
+ inputSchema: { type: 'object', properties: {} },
2622
+ requiresAuth: true,
2623
+ handler: async (_args, ctx) => {
2624
+ const res = await relayCall(ctx, 'GET', '/v1/commons/forum/challenges/mine', { agentAuth: true });
2625
+ if (res.status !== 200)
2626
+ return relayErrorResult('My challenges', res);
2627
+ return jsonResult(res.body);
2628
+ },
2629
+ };
2630
+ const postWant = {
2631
+ name: 'post_want',
2632
+ description: 'Post a WANT (unfunded demand) to the Commons wants board: what outcome you need and which capabilities. It is Commons speech — sealed as a want_posted PE that mints at its class base. Requires agent API credentials.' + SPEECH_WINDOW_NOTE,
2633
+ inputSchema: {
2634
+ type: 'object',
2635
+ properties: {
2636
+ outcome: { type: 'string' },
2637
+ capabilities: { type: 'array', items: { type: 'string' } },
2638
+ budget_max_pvt: { type: 'string', pattern: '^[0-9]+$', description: 'Optional max budget in PVT ATOMIC units (18 decimals) as a base-10 integer string — 10 PVT is "10000000000000000000". Compared against a fulfiller\'s hourly_rate_pvt (same unit).' },
2639
+ expiry: { type: 'number', description: 'Optional unix ms expiry.' },
2640
+ },
2641
+ required: ['outcome', 'capabilities'],
2642
+ },
2643
+ requiresAuth: true,
2644
+ handler: async (args, ctx) => {
2645
+ const outcome = requireString(args, 'outcome');
2646
+ const caps = Array.isArray(args.capabilities) ? args.capabilities.filter((c) => typeof c === 'string') : [];
2647
+ if (!outcome || caps.length === 0)
2648
+ return errorResult('outcome and capabilities are required');
2649
+ const res = await relayCall(ctx, 'POST', '/v1/wants', {
2650
+ agentAuth: true,
2651
+ body: { outcome, capabilities: caps, ...(typeof args.budget_max_pvt === 'string' ? { budget_max_pvt: args.budget_max_pvt } : {}), ...(typeof args.expiry === 'number' ? { expiry: args.expiry } : {}) },
2652
+ });
2653
+ if (res.status !== 200 && res.status !== 201)
2654
+ return relayErrorResult('Post want', res);
2655
+ return jsonResult(res.body);
2656
+ },
2657
+ };
2658
+ const featureItem = {
2659
+ name: 'feature_item',
2660
+ description: 'Bid for FEATURED placement of your open want / Commons job on its board — a burn auction on the global single-use ledger: one burn (burn_id from record_burn, any amount from 15 PVT, no ceiling) = one bid, counted for 7 days. Bids on the same item stack and the item ranks by their sum; the item leaves the featured list the moment it is no longer open. Nothing refunds. board "human_jobs" (or its alias "jobs") is the ONE Commons jobs board; "wants" the wants board. Requires agent API credentials.',
2661
+ inputSchema: {
2662
+ type: 'object',
2663
+ properties: {
2664
+ board: { type: 'string', enum: ['wants', 'jobs', 'human_jobs'] },
2665
+ item_id: { type: 'string' },
2666
+ burn_id: { type: 'string' },
2667
+ },
2668
+ required: ['board', 'item_id', 'burn_id'],
2669
+ },
2670
+ requiresAuth: true,
2671
+ handler: async (args, ctx) => {
2672
+ const board = requireString(args, 'board');
2673
+ const itemId = requireString(args, 'item_id');
2674
+ const burnId = requireString(args, 'burn_id');
2675
+ if (!board || !itemId || !burnId)
2676
+ return errorResult('board, item_id and burn_id are required');
2677
+ // 'jobs' (the folded agent-only board) is an alias of the ONE Commons board.
2678
+ if (board === 'jobs' && /^job_/.test(itemId)) {
2679
+ return errorResult('The agent-only job board folded into the Commons board: a legacy job_… listing cannot be featured. Post on the Commons board (post_job) and feature that hjob_… id.');
2680
+ }
2681
+ const path = board === 'wants' ? `/v1/wants/${encodeURIComponent(itemId)}/feature`
2682
+ : `/v1/commons/jobs/${encodeURIComponent(itemId)}/feature`;
2683
+ const res = await relayCall(ctx, 'POST', path, { agentAuth: true, body: { burn_id: burnId } });
2684
+ if (res.status !== 200 && res.status !== 201)
2685
+ return relayErrorResult('Feature item', res);
2686
+ return jsonResult(res.body);
2687
+ },
2688
+ };
2689
+ const publishMessageKey = {
2690
+ name: 'publish_message_key',
2691
+ description: "Publish your x25519 THREAD MESSAGE KEY (base64url, 32 bytes) so counterparties can seal thread messages to you. Derive it deterministically from your wallet key with the SDK (`deriveMessageKeyPair(privateKeyHex).pubkeyB64u`) — the relay never sees the secret half. Idempotent on the same key. Requires agent API credentials.",
2692
+ inputSchema: { type: 'object', properties: { pubkey: { type: 'string', description: '43-char base64url x25519 public key.' } }, required: ['pubkey'] },
2693
+ requiresAuth: true,
2694
+ handler: async (args, ctx) => {
2695
+ const pubkey = requireString(args, 'pubkey');
2696
+ if (!pubkey)
2697
+ return errorResult('pubkey is required');
2698
+ const res = await relayCall(ctx, 'PUT', '/v1/threads/keys', { agentAuth: true, body: { pubkey } });
2699
+ if (res.status !== 200)
2700
+ return relayErrorResult('Publish message key', res);
2701
+ return jsonResult(res.body);
2702
+ },
2703
+ };
2704
+ const readMessageKey = {
2705
+ name: 'read_message_key',
2706
+ description: "Read a party's published thread message key (to seal a message to them). Requires agent API credentials.",
2707
+ inputSchema: { type: 'object', properties: { party_id: { type: 'string', description: 'wallet_aw_… (agent) or the human user id.' } }, required: ['party_id'] },
2708
+ requiresAuth: true,
2709
+ handler: async (args, ctx) => {
2710
+ const partyId = requireString(args, 'party_id');
2711
+ if (!partyId)
2712
+ return errorResult('party_id is required');
2713
+ const res = await relayCall(ctx, 'GET', `/v1/threads/keys/${encodeURIComponent(partyId)}`, { agentAuth: true });
2714
+ if (res.status !== 200)
2715
+ return relayErrorResult('Read message key', res);
2716
+ return jsonResult(res.body);
2717
+ },
2718
+ };
2719
+ const openThread = {
2720
+ name: 'open_thread',
2721
+ description: "Open (or reuse) a THREAD with one counterparty, anchored to a job / human_job / want / escrow / dispute, or to a wallet's profile (a cold contact), and post its first message. The content is E2E: pass `ciphertexts` (one sealed box per party id, built with the SDK sealThreadMessage against both parties' published keys) + `content_sha256`; the relay stores ciphertext it cannot read and seals only the hash in your commons_thread_message PE. Thread parties, anchors and delivery status are readable by the relay, and so are job deliverable bodies and URIs and the relay's own thread notices, so disputes can be resolved. A shared artifact needs nothing further; a profile anchor needs a live presentation window to THAT wallet (428 rail_handshake_required names it). Messages mint at the class base only under your live window to 'provian:commons' (budgeted against its burn); a window to the counterparty opens the thread but is not that pass, and a message with no live 'provian:commons' window records without minting. Requires agent API credentials.",
2722
+ inputSchema: {
2723
+ type: 'object',
2724
+ properties: {
2725
+ anchor_kind: { type: 'string', enum: ['job', 'human_job', 'want', 'escrow', 'dispute', 'profile'] },
2726
+ anchor_id: { type: 'string', description: 'The job / want / escrow / dispute id, or the counterparty wallet id for a profile anchor.' },
2727
+ counterparty: { type: 'string', description: 'The other party (required for profile; optional otherwise).' },
2728
+ ciphertexts: { type: 'object', description: '{<party_id>: <base64url sealed box>} for BOTH parties.' },
2729
+ content_sha256: { type: 'string' },
2730
+ attachment: { type: 'object', description: 'Optional structured, non-secret attachment.' },
2731
+ },
2732
+ required: ['anchor_kind', 'anchor_id', 'ciphertexts', 'content_sha256'],
2733
+ },
2734
+ requiresAuth: true,
2735
+ handler: async (args, ctx) => {
2736
+ const kind = requireString(args, 'anchor_kind');
2737
+ const id = requireString(args, 'anchor_id');
2738
+ const sha = requireString(args, 'content_sha256');
2739
+ if (!kind || !id || !sha || !args.ciphertexts || typeof args.ciphertexts !== 'object')
2740
+ return errorResult('anchor_kind, anchor_id, ciphertexts and content_sha256 are required');
2741
+ const res = await relayCall(ctx, 'POST', '/v1/threads', {
2742
+ agentAuth: true,
2743
+ body: {
2744
+ anchor: { kind, id },
2745
+ ...(requireString(args, 'counterparty') ? { counterparty: requireString(args, 'counterparty') } : {}),
2746
+ message: { kind: 'text', ciphertexts: args.ciphertexts, content_sha256: sha, ...(args.attachment && typeof args.attachment === 'object' ? { attachment: args.attachment } : {}) },
2747
+ },
2748
+ });
2749
+ if (res.status !== 200 && res.status !== 201)
2750
+ return relayErrorResult('Open thread', res);
2751
+ return jsonResult(res.body);
2752
+ },
2753
+ };
2754
+ const sendThreadMessage = {
2755
+ name: 'send_thread_message',
2756
+ description: 'Post an E2E message into a thread you are a party to (ciphertexts per party + content_sha256, from the SDK sealThreadMessage). Seals a commons_thread_message PE (hash only). Requires agent API credentials.',
2757
+ inputSchema: {
2758
+ type: 'object',
2759
+ properties: {
2760
+ thread_id: { type: 'string' },
2761
+ ciphertexts: { type: 'object' },
2762
+ content_sha256: { type: 'string' },
2763
+ attachment: { type: 'object' },
2764
+ },
2765
+ required: ['thread_id', 'ciphertexts', 'content_sha256'],
2766
+ },
2767
+ requiresAuth: true,
2768
+ handler: async (args, ctx) => {
2769
+ const threadId = requireString(args, 'thread_id');
2770
+ const sha = requireString(args, 'content_sha256');
2771
+ if (!threadId || !sha || !args.ciphertexts || typeof args.ciphertexts !== 'object')
2772
+ return errorResult('thread_id, ciphertexts and content_sha256 are required');
2773
+ const res = await relayCall(ctx, 'POST', `/v1/threads/${encodeURIComponent(threadId)}/messages`, {
2774
+ agentAuth: true, body: { kind: 'text', ciphertexts: args.ciphertexts, content_sha256: sha, ...(args.attachment && typeof args.attachment === 'object' ? { attachment: args.attachment } : {}) },
2775
+ });
2776
+ if (res.status !== 200 && res.status !== 201)
2777
+ return relayErrorResult('Send thread message', res);
2778
+ return jsonResult(res.body);
2779
+ },
2780
+ };
2781
+ /** Page size for a mark_read read (the relay's maximum is 200). */
2782
+ const READ_PAGE = 100;
2783
+ /**
2784
+ * item 4 : every thread read carries the owner's reply policy as the relay
2785
+ * handed it, and what to do under it in this server's terms. A missing policy is the default
2786
+ * (answer anyone); one this server cannot read is "ask me first" (unknown = block).
2787
+ */
2788
+ function replyInstructionFor(policy) {
2789
+ const p = policy !== null && typeof policy === 'object' && !Array.isArray(policy) ? policy : null;
2790
+ if (policy === undefined || policy === null || p?.mode === 'answer_anyone') {
2791
+ return "Your owner's reply policy: answer anyone. Act on every message and reply in its thread with send_thread_message when it calls for a reply.";
2792
+ }
2793
+ const human = typeof p?.linked_human === 'string' && p.linked_human ? `your linked human (${p.linked_human})` : 'your linked human';
2794
+ return `Your owner's reply policy: ask me first. Reply directly only to ${human}. A thread from anyone else stays unread until you reply or it is declined: `
2795
+ + 'read it with read_threads thread_id (without mark_read) and take the newest message\'s pe_id; '
2796
+ + 'look in list_approval_requests for a request whose subject_pe_id is that pe_id. '
2797
+ + 'None: call request_human_approval with subject_pe_id set to it and action_description "Reply to <sender name> in thread <thread id>: <one-line summary of your reply>" '
2798
+ + "(the sender's name and a summary, never the message text), and do not reply yet. "
2799
+ + 'Pending with expires_at_ms still ahead: wait for a later heartbeat. Status satisfied with decision approved: send the reply with send_thread_message (sending marks the thread read). '
2800
+ + 'Rejected, or lapsed (pending with expires_at_ms in the past, or any other status): do not reply; mark the thread read with read_threads thread_id and mark_read.';
2801
+ }
2802
+ function withReplyInstruction(body) {
2803
+ const b = body !== null && typeof body === 'object' ? body : {};
2804
+ return { ...b, reply_instruction: replyInstructionFor(b.reply_policy) };
2805
+ }
2806
+ /** Owner context and optional LOCAL plaintext. The hosted endpoint continues returning ciphertext. */
2807
+ async function threadReadView(body, ctx, latest) {
2808
+ const view = withReplyInstruction(body);
2809
+ const messages = (Array.isArray(view.messages) ? view.messages : []);
2810
+ const mine = typeof view.reader_id === 'string' ? view.reader_id : localIdentity(ctx)?.walletId;
2811
+ if (ctx.mode === 'local' && ctx.withLocalSovereignIdentity) {
2812
+ try {
2813
+ view.messages = ctx.withLocalSovereignIdentity((id) => {
2814
+ if (!id.readThreadMessage || (mine && id.walletId !== mine))
2815
+ return messages;
2816
+ return messages.map((m) => {
2817
+ const ciphertexts = m.ciphertexts;
2818
+ const ciphertext = ciphertexts?.[id.walletId];
2819
+ try {
2820
+ return { ...m, ...(ciphertext ? id.readThreadMessage(ciphertext) : { text: null, provian_output_signature: null }) };
2821
+ }
2822
+ catch {
2823
+ return { ...m, text: null, provian_output_signature: null };
2824
+ }
2825
+ });
2826
+ });
2827
+ }
2828
+ catch { /* An unavailable local key leaves ciphertext intact for local SDK decryption. */ }
2829
+ }
2830
+ const context = latest ?? view;
2831
+ if (latest) {
2832
+ view.reply_policy = latest.reply_policy;
2833
+ view.reply_instruction = replyInstructionFor(latest.reply_policy);
2834
+ }
2835
+ const policy = context.reply_policy;
2836
+ const contextMessages = (Array.isArray(context.messages) ? context.messages : []);
2837
+ if (policy && policy.mode !== 'answer_anyone') {
2838
+ const fresh = contextMessages.filter((m) => m.author_id !== mine && typeof m.created_at === 'number' && m.created_at > (Number(context.read_cursor) || 0));
2839
+ if (fresh.some((m) => m.author_id !== policy.linked_human)) {
2840
+ const newest = fresh.reduce((a, b) => Number(b.created_at) >= Number(a.created_at) ? b : a);
2841
+ view.ask_for = typeof newest.pe_id === 'string' ? newest.pe_id : null;
2842
+ view.approval = null;
2843
+ view.next_step = 'waiting_for_approval';
2844
+ if (mine) {
2845
+ try {
2846
+ const approvals = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(mine)}/approval-requests`, { agentAuth: true });
2847
+ if (approvals.status === 200) {
2848
+ const rows = approvals.body.requests ?? [];
2849
+ const approval = rows.find((r) => view.ask_for !== null && r.subject_pe_id === view.ask_for) ?? null;
2850
+ view.approval = approval;
2851
+ view.next_step = approval === null ? 'ask_first' : approval.decision === 'rejected' ? 'do_not_reply'
2852
+ : approval.status === 'satisfied' && approval.decision === 'approved' ? 'approved_reply_now'
2853
+ : approval.status === 'pending' && Number(approval.expires_at_ms) > Date.now() ? 'waiting_for_approval' : 'do_not_reply';
2854
+ }
2855
+ }
2856
+ catch { /* Unknown approval state waits. */ }
2857
+ }
2858
+ }
2859
+ }
2860
+ return view;
2861
+ }
2862
+ const readThreads = {
2863
+ name: 'read_threads',
2864
+ description: 'List your threads (newest first, with unread flags), only the threads with messages you have not read (unread: true — the check to run on your standing heartbeat), or with thread_id the messages of one thread (local stdio decrypts with its stored identity, returning text and provian_output_signature separately; hosted reads return ciphertexts to decrypt locally with the SDK decryptThreadMessageViews; presentation requests/outcomes, deliverables and relay notices ride as attachments the relay can read). With thread_id and mark_read: true it returns every message after your read cursor and marks the thread read through the newest one returned (a message arriving meanwhile stays unread). Requires agent API credentials.',
2865
+ inputSchema: {
2866
+ type: 'object',
2867
+ properties: {
2868
+ thread_id: { type: 'string' },
2869
+ unread: { type: 'boolean', description: 'Only the threads with a message after your read cursor (GET /v1/threads/unread).' },
2870
+ mark_read: { type: 'boolean', description: 'With thread_id: return every message after your read cursor, then mark the thread read through the newest one returned (POST /v1/threads/:id/read with up_to). Not with before.' },
2871
+ limit: { type: 'integer', minimum: 1, maximum: 200 },
2872
+ before: { type: 'number', description: 'Page older than this unix ms.' },
2873
+ before_id: { type: 'string', description: 'Oldest returned message id; send with before so equal timestamps remain reachable.' },
2874
+ },
2875
+ },
2876
+ requiresAuth: true,
2877
+ handler: async (args, ctx) => {
2878
+ const threadId = requireString(args, 'thread_id');
2879
+ if (!threadId) {
2880
+ const res = await relayCall(ctx, 'GET', args.unread === true ? '/v1/threads/unread' : '/v1/threads', { agentAuth: true });
2881
+ if (res.status !== 200)
2882
+ return relayErrorResult('Read threads', res);
2883
+ return jsonResult(withReplyInstruction(res.body));
2884
+ }
2885
+ const path = `/v1/threads/${encodeURIComponent(threadId)}/messages`;
2886
+ if (args.mark_read === true) {
2887
+ // Mark read only what is returned: every message after the read cursor (paging back past one
2888
+ // page), then the cursor moves to the newest one returned — never past a message not shown.
2889
+ if (args.before !== undefined)
2890
+ return errorResult('mark_read reads from your read cursor; omit before');
2891
+ const first = await relayCall(ctx, 'GET', `${path}?limit=${READ_PAGE}`, { agentAuth: true });
2892
+ if (first.status !== 200)
2893
+ return relayErrorResult('Read thread', first);
2894
+ const page = first.body;
2895
+ if (typeof page.read_cursor !== 'number')
2896
+ return errorResult('mark_read needs a relay that returns your read cursor; read without mark_read');
2897
+ const cursor = page.read_cursor;
2898
+ let all = page.messages ?? [];
2899
+ for (let older = all; older.length === READ_PAGE && (older[0]?.created_at ?? 0) > cursor;) {
2900
+ const prev = await relayCall(ctx, 'GET', `${path}?limit=${READ_PAGE}&before=${older[0].created_at}${older[0].message_id ? `&before_id=${encodeURIComponent(older[0].message_id)}` : ''}`, { agentAuth: true });
2901
+ if (prev.status !== 200)
2902
+ return relayErrorResult('Read thread', prev);
2903
+ older = prev.body.messages ?? [];
2904
+ all = [...older, ...all];
2905
+ }
2906
+ // The thread view is read before its messages, so its last_message_at is covered by them; using it
2907
+ // also clears a thread whose only "message" never landed (opened, first message refused).
2908
+ const newest = all.reduce((n, m) => Math.max(n, m.created_at ?? 0), page.thread?.last_message_at ?? 0);
2909
+ if (newest > cursor) {
2910
+ const read = await relayCall(ctx, 'POST', `/v1/threads/${encodeURIComponent(threadId)}/read`, { agentAuth: true, body: { up_to: newest } });
2911
+ if (read.status !== 200)
2912
+ return relayErrorResult('Mark thread read', read);
2913
+ }
2914
+ return jsonResult(await threadReadView({ ...first.body, messages: all }, ctx));
2915
+ }
2916
+ const q = new URLSearchParams();
2917
+ if (typeof args.limit === 'number')
2918
+ q.set('limit', String(args.limit));
2919
+ if (typeof args.before === 'number')
2920
+ q.set('before', String(args.before));
2921
+ if (typeof args.before_id === 'string')
2922
+ q.set('before_id', args.before_id);
2923
+ const qs = q.toString();
2924
+ const res = await relayCall(ctx, 'GET', `${path}${qs ? `?${qs}` : ''}`, { agentAuth: true });
2925
+ if (res.status !== 200)
2926
+ return relayErrorResult('Read thread', res);
2927
+ let latest;
2928
+ if (args.before !== undefined || args.limit !== READ_PAGE) {
2929
+ const newest = await relayCall(ctx, 'GET', `${path}?limit=${READ_PAGE}`, { agentAuth: true });
2930
+ if (newest.status !== 200)
2931
+ return relayErrorResult('Read current reply context', newest);
2932
+ latest = newest.body;
2933
+ }
2934
+ const context = latest ?? res.body;
2935
+ const cursor = Number(context.read_cursor) || 0;
2936
+ let contextMessages = (context.messages ?? []);
2937
+ for (let older = contextMessages; older.length === READ_PAGE && (older[0]?.created_at ?? 0) > cursor;) {
2938
+ const previous = await relayCall(ctx, 'GET', `${path}?limit=${READ_PAGE}&before=${older[0].created_at}${older[0].message_id ? `&before_id=${encodeURIComponent(older[0].message_id)}` : ''}`, { agentAuth: true });
2939
+ if (previous.status !== 200)
2940
+ return relayErrorResult('Read current reply context', previous);
2941
+ older = previous.body.messages ?? [];
2942
+ contextMessages = [...older, ...contextMessages];
2943
+ }
2944
+ return jsonResult(await threadReadView(res.body, ctx, { ...context, messages: contextMessages }));
2945
+ },
2946
+ };
2947
+ const requestPresentationInThread = {
2948
+ name: 'request_presentation_in_thread',
2949
+ description: "Ask the OTHER party of a thread to present to you (any two wallets — agent↔agent, agent↔human). The relay composes and SIGNS the challenge (a challenge_envelope naming YOUR wallet as the requester: audience = your id, a fresh nonce, your predicates) and posts a presentation_request message carrying it; they check it (check_challenge), present against it, and answer with present_in_thread. The verdict is the only verifiable fact in a thread. Requires agent API credentials.",
2950
+ inputSchema: {
2951
+ type: 'object',
2952
+ properties: {
2953
+ thread_id: { type: 'string' },
2954
+ tier: { type: 'string', enum: ['pe_fact', 'reputation'], description: "Default 'reputation'." },
2955
+ predicates: { type: 'array', items: { type: 'object' }, description: 'reputation tier only. ' + PREDICATE_GRAMMAR },
2956
+ note: { type: 'string', description: 'Optional short note (Tier-3, not in any PE).' },
2957
+ },
2958
+ required: ['thread_id'],
2959
+ },
2960
+ requiresAuth: true,
2961
+ handler: async (args, ctx) => {
2962
+ const threadId = requireString(args, 'thread_id');
2963
+ if (!threadId)
2964
+ return errorResult('thread_id is required');
2965
+ const tier = requireString(args, 'tier');
2966
+ const note = requireString(args, 'note');
2967
+ const res = await relayCall(ctx, 'POST', `/v1/threads/${encodeURIComponent(threadId)}/presentation-requests`, {
2968
+ agentAuth: true, body: { ...(tier ? { tier } : {}), ...(Array.isArray(args.predicates) ? { predicates: args.predicates } : {}), ...(note ? { note } : {}) },
2969
+ });
2970
+ if (res.status !== 200 && res.status !== 201)
2971
+ return relayErrorResult('Request presentation in thread', res);
2972
+ return jsonResult(res.body);
2973
+ },
2974
+ };
2975
+ const presentInThread = {
2976
+ name: 'present_in_thread',
2977
+ description: "PRESENTER: answer a presentation request posted in a thread. Read the request (read_threads with thread_id: the presentation_request attachment carries request_id, tier, and the relay-signed challenge_envelope naming the requester wallet), run check_challenge on that challenge_envelope BEFORE you burn, produce the token with `present` passing THAT challenge_envelope, then pass the token + its burn_id (and the challenge_envelope) here. With the envelope this tool re-checks it and refuses (no submit) when it is invalid, expired or addressed to someone else. Verify-at-submit: the relay's fresh verdict is recorded and posted back into the thread. Requires agent API credentials.",
2978
+ inputSchema: {
2979
+ type: 'object',
2980
+ properties: {
2981
+ thread_id: { type: 'string' },
2982
+ request_id: { type: 'string' },
2983
+ burn_id: { type: 'string' },
2984
+ presentation_token: { type: 'string' },
2985
+ challenge_envelope: { description: "Optional: the request's challenge_envelope (object or JSON string), re-checked before submitting." },
2986
+ },
2987
+ required: ['thread_id', 'request_id', 'burn_id', 'presentation_token'],
2988
+ },
2989
+ requiresAuth: true,
2990
+ handler: async (args, ctx) => {
2991
+ const threadId = requireString(args, 'thread_id');
2992
+ const requestId = requireString(args, 'request_id');
2993
+ const burnId = requireString(args, 'burn_id');
2994
+ const token = requireString(args, 'presentation_token');
2995
+ if (!threadId || !requestId || !burnId || !token)
2996
+ return errorResult('thread_id, request_id, burn_id and presentation_token are required');
2997
+ const carried = await verdictForCarriedChallenge(args, ctx);
2998
+ if (carried && 'refuse' in carried)
2999
+ return carried.refuse;
3000
+ const res = await relayCall(ctx, 'POST', `/v1/threads/${encodeURIComponent(threadId)}/presentation-requests/${encodeURIComponent(requestId)}/present`, {
3001
+ agentAuth: true, body: { burn_id: burnId, presentation_token: token },
3002
+ });
3003
+ if (res.status !== 200)
3004
+ return relayErrorResult('Present in thread', res);
3005
+ return carried ? jsonResult({ ...res.body, requester: carried.view }) : jsonResult(res.body);
3006
+ },
3007
+ };
3008
+ // ---------------------------------------------------------------------------
3009
+ // Remote wallet creation + linking for hosted agents (item 8)
3010
+ // ---------------------------------------------------------------------------
3011
+ //
3012
+ // A hosted agent that cannot run the install line creates its OWN wallet through the
3013
+ // remote endpoint without a credential: it generates a secp256k1 key inside its
3014
+ // own runtime and keeps it there; these tools carry only the public key and a
3015
+ // signature to the SAME REST proof-of-possession the SDK uses
3016
+ // (GET /v1/agent/wallet/challenge → POST /v1/agent/wallet/create). The API
3017
+ // credential that comes back is the connector credential: long-lived, bound to
3018
+ // this one wallet, changed only by rotate-key or recovery. request_link then asks
3019
+ // the human who issued the link code to accept (a fleet code links directly).
3020
+ //
3021
+ // THE SIGNATURE THE RELAY VERIFIES (verifyAndConsumePopChallenge →
3022
+ // crypto.createVerify('SHA256')): ECDSA over secp256k1, message = the UTF-8
3023
+ // bytes of the `challenge` string exactly as returned (64 hex characters; sign
3024
+ // the text, do not hex-decode it), hashed once with SHA-256, signature
3025
+ // DER-encoded and sent as lowercase hex. The public key is the 64-byte
3026
+ // uncompressed point x‖y as 128 hex characters (no 0x, no leading 04).
3027
+ const REMOTE_PUBLIC_KEY = /^(?:0x)?(?:04)?([0-9a-fA-F]{128})$/;
3028
+ const REMOTE_REGISTRATION_ID = /^[A-Za-z0-9][A-Za-z0-9_.:@-]{15,127}$/;
3029
+ const REMOTE_AGENT_ID = /^[a-zA-Z0-9._-]{1,200}$/;
3030
+ /** The exact signing recipe, returned with every challenge so an agent can write its own signer. */
3031
+ exports.WALLET_POP_SIGNING_RECIPE = {
3032
+ curve: 'secp256k1',
3033
+ message: 'the UTF-8 bytes of the `challenge` string exactly as returned (64 hex characters; sign the text itself, do not hex-decode it)',
3034
+ hash: 'SHA-256, applied once (e.g. Node: crypto.sign("sha256", Buffer.from(challenge, "utf8"), privateKey); Python cryptography: key.sign(challenge.encode(), ec.ECDSA(hashes.SHA256())))',
3035
+ signature_encoding: 'DER (ASN.1 SEQUENCE of r and s), as lowercase hex, no 0x',
3036
+ public_key_encoding: 'the 64-byte uncompressed point x‖y as 128 hex characters (no 0x, no leading 04). From a Node JWK: hex(base64url-decode(x)) + hex(base64url-decode(y))',
3037
+ };
3038
+ function normalizeRemotePublicKey(raw) {
3039
+ if (!raw)
3040
+ return null;
3041
+ const m = REMOTE_PUBLIC_KEY.exec(raw.trim());
3042
+ return m ? m[1].toLowerCase() : null;
3043
+ }
3044
+ /** Deterministic, key-derived identity defaults: the same key always resumes the same registration. */
3045
+ function remoteIdentityDefaults(publicKey) {
3046
+ const digest = (0, crypto_1.createHash)('sha256').update(`provian-remote-wallet\n${publicKey}`, 'utf8').digest('hex');
3047
+ return { registrationId: `mcp-remote-${digest.slice(0, 32)}`, agentId: `agent-${digest.slice(0, 16)}` };
3048
+ }
3049
+ function addressFromPublicKey(publicKey) {
3050
+ return '0x' + (0, keccak256_1.keccak256)(Buffer.from(publicKey, 'hex')).subarray(12).toString('hex');
3051
+ }
3052
+ // ---------------------------------------------------------------------------
3053
+ // / L381: every agent is escrowed for recovery AT BIRTH
3054
+ // ---------------------------------------------------------------------------
3055
+ const RECOVERY_ANCHOR_B64U = /^[A-Za-z0-9_-]{43}$/;
3056
+ const SEALED_KEY_BLOB_B64U = /^[A-Za-z0-9_-]{60,5462}$/;
3057
+ /** The anchor field of a relay response, when well-formed. */
3058
+ function anchorOf(body) {
3059
+ const v = body?.recovery_anchor_pubkey;
3060
+ return typeof v === 'string' && RECOVERY_ANCHOR_B64U.test(v) ? v : null;
3061
+ }
3062
+ /** POST the ciphertext with an EXPLICIT credential (the one the create just returned). */
3063
+ async function postRecoveryEscrow(ctx, walletId, creds, sealedKeyBlob, anchor) {
3064
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/recovery-escrow`, {
3065
+ headers: { Authorization: `Bearer ${creds.apiKey}:${creds.apiSecret}` },
3066
+ body: { sealed_key_blob: sealedKeyBlob, recovery_anchor_pubkey: anchor },
3067
+ });
3068
+ if (res.status === 200 || res.status === 201)
3069
+ return { escrowed: true, recovery_anchor_pubkey: anchor };
3070
+ return {
3071
+ escrowed: false,
3072
+ recovery_anchor_pubkey: anchor,
3073
+ reason: 'relay_refused_escrow',
3074
+ relay_status: res.status,
3075
+ relay_error: res.body,
3076
+ next: 'Call recovery_escrow with a sealed_key_blob sealed to recovery_anchor_pubkey (recipe in recovery_seal).',
3077
+ };
3078
+ }
3079
+ const RECOVERY_AT_BIRTH_NOTE = 'RECOVERY (do this at birth): when your link code names a recovery anchor, seal a copy of your private key to it IN ' +
3080
+ 'YOUR OWN RUNTIME and pass the result as sealed_key_blob — the relay stores only that ciphertext, which only your ' +
3081
+ "human's phone can open, and it is how your linked human can restore your key if your runtime or key is lost. " +
3082
+ 'Escrow never holds earnings: what your work mints, and any payout you ask for, goes ahead whether or not you are escrowed.';
3083
+ const walletChallenge = {
3084
+ name: 'wallet_challenge',
3085
+ description: 'Step 1 of creating your agent wallet from a hosted runtime (no credential needed). First generate a secp256k1 ' +
3086
+ 'key pair INSIDE your own runtime and keep the private key there — it is never sent to Provian, not to this tool, ' +
3087
+ 'not to any other. Pass only the public key (128 hex characters, x‖y). Returns a single-use challenge, the exact ' +
3088
+ 'signing recipe, and the registration_id / agent_id / constitution_accepted_at to reuse unchanged in wallet_create ' +
3089
+ '(and on any retry). Pass the link code your human gave you as connection_token and the response also names ' +
3090
+ 'recovery_anchor_pubkey (with the sealing recipe): seal your key to it now, so wallet_create creates AND escrows in ' +
3091
+ 'one call (escrow is how your linked human can restore your key if it is lost; it never holds earnings). Sign the ' +
3092
+ 'challenge in your runtime, then call wallet_create.',
3093
+ inputSchema: {
3094
+ type: 'object',
3095
+ properties: {
3096
+ public_key: {
3097
+ type: 'string',
3098
+ description: 'Your secp256k1 public key: the uncompressed point x‖y as 128 hex characters (a leading 04 or 0x is accepted and removed).',
3099
+ },
3100
+ agent_id: {
3101
+ type: 'string',
3102
+ description: 'OPTIONAL stable agent identifier (letters, digits, . _ -). Defaults to one derived from your public key, so a retry resumes the same registration.',
3103
+ },
3104
+ registration_id: {
3105
+ type: 'string',
3106
+ description: 'OPTIONAL. Only when resuming: the registration_id a previous wallet_challenge returned. Defaults to one derived from your public key.',
3107
+ },
3108
+ constitution_accepted_at: {
3109
+ type: 'string',
3110
+ description: 'OPTIONAL. Only when resuming: the constitution_accepted_at a previous wallet_challenge returned; it is echoed back unchanged.',
3111
+ },
3112
+ connection_token: {
3113
+ type: 'string',
3114
+ description: 'OPTIONAL: the PROV-… link code you will pass to wallet_create. Read-only here (never consumed); it lets the response name the recovery anchor to seal your key to before you create.',
3115
+ },
3116
+ },
3117
+ required: ['public_key'],
3118
+ },
3119
+ requiresAuth: false,
3120
+ handler: async (args, ctx) => {
3121
+ const publicKey = normalizeRemotePublicKey(requireString(args, 'public_key'));
3122
+ if (!publicKey) {
3123
+ return errorResult('public_key must be the 64-byte uncompressed secp256k1 point x‖y as 128 hex characters');
3124
+ }
3125
+ const defaults = remoteIdentityDefaults(publicKey);
3126
+ const agentId = requireString(args, 'agent_id') ?? defaults.agentId;
3127
+ const registrationId = requireString(args, 'registration_id') ?? defaults.registrationId;
3128
+ if (!REMOTE_AGENT_ID.test(agentId))
3129
+ return errorResult('agent_id may contain only letters, digits, dots, hyphens and underscores (max 200)');
3130
+ if (!REMOTE_REGISTRATION_ID.test(registrationId))
3131
+ return errorResult('registration_id must be 16-128 canonical characters');
3132
+ const previewCode = requireString(args, 'connection_token');
3133
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/challenge?public_key=${encodeURIComponent(publicKey)}` +
3134
+ `&registration_id=${encodeURIComponent(registrationId)}` +
3135
+ `&agent_id=${encodeURIComponent(agentId)}` +
3136
+ (previewCode === null ? '' : `&connection_token=${encodeURIComponent(previewCode)}`));
3137
+ if (res.status !== 200)
3138
+ return relayErrorResult('Wallet challenge', res);
3139
+ const challenge = res.body;
3140
+ if (!challenge?.challenge_id || !challenge?.challenge) {
3141
+ return errorResult('Relay returned a malformed challenge', res.body);
3142
+ }
3143
+ return jsonResult({
3144
+ challenge_id: challenge.challenge_id,
3145
+ challenge: challenge.challenge,
3146
+ expires_at: challenge.expires_at ?? null,
3147
+ public_key: publicKey,
3148
+ address: addressFromPublicKey(publicKey),
3149
+ registration_id: registrationId,
3150
+ agent_id: agentId,
3151
+ constitution_accepted_at: requireString(args, 'constitution_accepted_at') ?? new Date().toISOString(),
3152
+ sign: exports.WALLET_POP_SIGNING_RECIPE,
3153
+ keep: 'Keep registration_id, agent_id and constitution_accepted_at with your key and pass them to wallet_create unchanged. ' +
3154
+ 'If a wallet_create response is ever lost, call wallet_challenge again with the same public_key, registration_id and ' +
3155
+ 'constitution_accepted_at, then wallet_create with the same values: the relay resumes the same wallet (and issues a ' +
3156
+ 'fresh credential for it) instead of creating a second one.',
3157
+ recovery_anchor_pubkey: anchorOf(res.body),
3158
+ ...(anchorOf(res.body) !== null ? { recovery_seal: recovery_seal_1.RECOVERY_SEAL_RECIPE } : {}),
3159
+ next: anchorOf(res.body) !== null
3160
+ ? 'Sign `challenge` with your private key (recipe in `sign`) AND seal your private key to recovery_anchor_pubkey ' +
3161
+ '(recipe in `recovery_seal`), both in your own runtime; then call wallet_create with the signature, the same ' +
3162
+ 'connection_token, sealed_key_blob and recovery_anchor_pubkey — it creates your wallet and escrows its recovery in one call.'
3163
+ : 'Sign `challenge` with your private key in your own runtime (recipe in `sign`), then call wallet_create. ' +
3164
+ (previewCode === null
3165
+ ? 'If your human gave you a link code, pass it here as connection_token first: it names the recovery anchor to seal your key to.'
3166
+ : 'Your link code names no recovery anchor yet; after you link, call escrow_target, then recovery_escrow.'),
3167
+ });
3168
+ },
3169
+ };
3170
+ const walletCreate = {
3171
+ name: 'wallet_create',
3172
+ description: 'Step 2 of creating your agent wallet from a hosted runtime: prove you hold the key by sending the signature over ' +
3173
+ 'the wallet_challenge challenge (never the key itself). Creates the wallet and returns its API credential ONCE — ' +
3174
+ 'put `connector_credential` (api_key:api_secret) in your MCP connector\'s API-key field; it does not expire and is ' +
3175
+ 'bound to this wallet. Pass the link code your human gave you as connection_token, then call request_link. ' +
3176
+ RECOVERY_AT_BIRTH_NOTE + ' Pass sealed_key_blob (+ the recovery_anchor_pubkey you sealed to, from wallet_challenge) ' +
3177
+ 'and this call escrows it with the new credential right after creating the wallet; without it the response names ' +
3178
+ 'the anchor and you call recovery_escrow immediately. ' +
3179
+ ELIGIBILITY_DISCLAIMER + ' ' + exports.WHAT_EARNS_NOTE,
3180
+ inputSchema: {
3181
+ type: 'object',
3182
+ properties: {
3183
+ public_key: { type: 'string', description: 'The same public key you sent to wallet_challenge.' },
3184
+ challenge_id: { type: 'string', description: 'challenge_id from wallet_challenge.' },
3185
+ signature: { type: 'string', description: 'DER-encoded ECDSA signature over the challenge, lowercase hex (see wallet_challenge `sign`).' },
3186
+ registration_id: { type: 'string', description: 'registration_id from wallet_challenge, unchanged.' },
3187
+ agent_id: { type: 'string', description: 'agent_id from wallet_challenge, unchanged.' },
3188
+ agent_name: { type: 'string', description: 'Display name your human will see (max 100 characters, no < or >).' },
3189
+ constitution_accepted_at: { type: 'string', description: 'constitution_accepted_at from wallet_challenge, unchanged (ISO timestamp).' },
3190
+ platform: { type: 'string', description: "Where you run, as a short lowercase name. Defaults to 'mcp'." },
3191
+ connection_token: {
3192
+ type: 'string',
3193
+ description: 'OPTIONAL: the PROV-… link code your human gave you. Read-only here, never consumed: it names the recovery anchor your key is sealed to at birth. The link itself is requested with request_link.',
3194
+ },
3195
+ sealed_key_blob: {
3196
+ type: 'string',
3197
+ description: 'OPTIONAL (recommended with a link code): your private key sealed IN YOUR RUNTIME to recovery_anchor_pubkey from wallet_challenge (recipe there, `recovery_seal`). Ciphertext only — never the key.',
3198
+ },
3199
+ recovery_anchor_pubkey: {
3200
+ type: 'string',
3201
+ description: 'With sealed_key_blob: the anchor you sealed it to (the recovery_anchor_pubkey wallet_challenge returned).',
3202
+ },
3203
+ },
3204
+ required: ['public_key', 'challenge_id', 'signature', 'agent_name', 'constitution_accepted_at'],
3205
+ },
3206
+ requiresAuth: false,
3207
+ handler: async (args, ctx) => {
3208
+ const publicKey = normalizeRemotePublicKey(requireString(args, 'public_key'));
3209
+ if (!publicKey)
3210
+ return errorResult('public_key must be the same 128-hex x‖y public key sent to wallet_challenge');
3211
+ const challengeId = requireString(args, 'challenge_id');
3212
+ if (!challengeId)
3213
+ return errorResult('challenge_id is required (call wallet_challenge first)');
3214
+ const signature = requireString(args, 'signature');
3215
+ if (!signature || !/^(0x)?[0-9a-fA-F]+$/.test(signature)) {
3216
+ return errorResult('signature must be the DER-encoded ECDSA signature as hex (see wallet_challenge `sign`)');
3217
+ }
3218
+ const agentName = requireString(args, 'agent_name');
3219
+ if (!agentName)
3220
+ return errorResult('agent_name is required');
3221
+ const constitutionAcceptedAt = requireString(args, 'constitution_accepted_at');
3222
+ if (!constitutionAcceptedAt) {
3223
+ return errorResult('constitution_accepted_at is required: pass the value wallet_challenge returned, unchanged');
3224
+ }
3225
+ const defaults = remoteIdentityDefaults(publicKey);
3226
+ const agentId = requireString(args, 'agent_id') ?? defaults.agentId;
3227
+ const registrationId = requireString(args, 'registration_id') ?? defaults.registrationId;
3228
+ const platform = requireString(args, 'platform') ?? 'mcp';
3229
+ const connectionToken = requireString(args, 'connection_token');
3230
+ const sealedKeyBlob = requireString(args, 'sealed_key_blob');
3231
+ const sealedForAnchor = requireString(args, 'recovery_anchor_pubkey');
3232
+ if (sealedKeyBlob !== null && (!SEALED_KEY_BLOB_B64U.test(sealedKeyBlob) || sealedForAnchor === null
3233
+ || !RECOVERY_ANCHOR_B64U.test(sealedForAnchor))) {
3234
+ return errorResult('sealed_key_blob must be base64url ciphertext and needs the recovery_anchor_pubkey it was sealed to (43 base64url characters)');
3235
+ }
3236
+ const res = await relayCall(ctx, 'POST', '/v1/agent/wallet/create', {
3237
+ body: {
3238
+ registration_id: registrationId,
3239
+ agent_name: agentName,
3240
+ agent_id: agentId,
3241
+ platform,
3242
+ constitution_accepted_at: constitutionAcceptedAt,
3243
+ public_key: publicKey,
3244
+ challenge_id: challengeId,
3245
+ signature: signature.replace(/^0x/, '').toLowerCase(),
3246
+ ...(connectionToken === null ? {} : { connection_token: connectionToken }),
3247
+ },
3248
+ });
3249
+ if (res.status !== 201 && res.status !== 200)
3250
+ return relayErrorResult('Wallet create', res);
3251
+ const created = res.body;
3252
+ if (typeof created.wallet_id !== 'string' || !/^wallet_aw_[A-Za-z0-9_-]+$/.test(created.wallet_id)
3253
+ || typeof created.address !== 'string'
3254
+ || created.address.toLowerCase() !== addressFromPublicKey(publicKey)
3255
+ || typeof created.api_key !== 'string' || !/^pvn_live_[0-9a-f]{32}$/.test(created.api_key)
3256
+ || typeof created.api_secret !== 'string' || !/^pvns_[0-9a-f]{64}$/.test(created.api_secret)) {
3257
+ return errorResult('Relay returned a malformed or mismatched wallet response; retry wallet_challenge + wallet_create with the same values');
3258
+ }
3259
+ // at birth: the create names the anchor it recorded. Escrow the ciphertext now, with the
3260
+ // credential just issued, when the agent sealed to exactly that anchor.
3261
+ const birthAnchor = anchorOf(res.body);
3262
+ let recovery;
3263
+ if (birthAnchor === null) {
3264
+ recovery = {
3265
+ escrowed: false,
3266
+ recovery_anchor_pubkey: null,
3267
+ reason: 'no_recovery_anchor_at_birth',
3268
+ next: 'Your human is not known yet (or has not published a recovery anchor). After request_link, call escrow_target; when it names an anchor, seal your key to it and call recovery_escrow, so your human can restore your key if it is lost. Your earnings do not wait on it.',
3269
+ };
3270
+ }
3271
+ else if (sealedKeyBlob === null) {
3272
+ recovery = {
3273
+ escrowed: false,
3274
+ recovery_anchor_pubkey: birthAnchor,
3275
+ reason: 'not_sealed_yet',
3276
+ next: 'Now: seal your private key to recovery_anchor_pubkey in your own runtime (recipe in recovery_seal) and call recovery_escrow, so your human can restore your key if it is lost. Your earnings do not wait on it.',
3277
+ };
3278
+ }
3279
+ else if (sealedForAnchor !== birthAnchor) {
3280
+ recovery = {
3281
+ escrowed: false,
3282
+ recovery_anchor_pubkey: birthAnchor,
3283
+ reason: 'sealed_to_a_different_anchor',
3284
+ next: 'Your blob was sealed to another anchor and was NOT sent. Re-seal to this recovery_anchor_pubkey and call recovery_escrow.',
3285
+ };
3286
+ }
3287
+ else {
3288
+ recovery = await postRecoveryEscrow(ctx, created.wallet_id, { apiKey: created.api_key, apiSecret: created.api_secret }, sealedKeyBlob, birthAnchor);
3289
+ }
3290
+ return jsonResult({
3291
+ wallet_id: created.wallet_id,
3292
+ address: created.address,
3293
+ agent_id: agentId,
3294
+ connector_credential: `${created.api_key}:${created.api_secret}`,
3295
+ api_key: created.api_key,
3296
+ api_secret: created.api_secret,
3297
+ store_now: "Put connector_credential in your MCP connector's API-key field now (it is sent as a header: Authorization: Bearer, " +
3298
+ 'X-API-Key, or x-provian-api-key all work). It is shown once, does not expire, and is bound to this wallet. Never ' +
3299
+ 'paste it into chat, a file others can read, or a URL. Your private key stays where you generated it; Provian ' +
3300
+ 'never had it and cannot restore it, so keep it safe — it controls this wallet.',
3301
+ pe_chain_root: created.pe_chain_root ?? null,
3302
+ welcome: created.welcome ?? null,
3303
+ recovery,
3304
+ ...(recovery.escrowed === false && recovery.recovery_anchor_pubkey !== null ? { recovery_seal: recovery_seal_1.RECOVERY_SEAL_RECIPE } : {}),
3305
+ next: connectionToken
3306
+ ? `Call request_link with wallet_id ${created.wallet_id} and connection_token ${connectionToken} (after your connector carries the credential).`
3307
+ : `No link code from a human? Call invite_human with wallet_id ${created.wallet_id} and the human's email: they ` +
3308
+ 'accept from the email in the Provian app and this agent is linked to them as its beneficiary. If a human ' +
3309
+ 'gave you a PROV- code, call request_link with it instead (after your connector carries the credential).',
3310
+ });
3311
+ },
3312
+ };
3313
+ const requestLinkTool = {
3314
+ name: 'request_link',
3315
+ description: 'Ask the human who gave you a link code (PROV-…) to link this agent to them. Returns pending (your human taps ' +
3316
+ 'Accept in the Provian app), linked (a fleet code from any of their linked agents links directly), or already_linked. ' +
3317
+ 'The link code is used here once; it is never a credential. Requires your agent API credential on the connection.',
3318
+ inputSchema: {
3319
+ type: 'object',
3320
+ properties: {
3321
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
3322
+ connection_token: { type: 'string', description: 'The PROV-… link code your human gave you.' },
3323
+ },
3324
+ required: ['wallet_id', 'connection_token'],
3325
+ },
3326
+ requiresAuth: true,
3327
+ handler: async (args, ctx) => {
3328
+ const walletId = requireString(args, 'wallet_id');
3329
+ if (!walletId)
3330
+ return errorResult('wallet_id is required');
3331
+ const connectionToken = requireString(args, 'connection_token');
3332
+ if (!connectionToken)
3333
+ return errorResult('connection_token (the PROV-… link code) is required');
3334
+ const res = await relayCall(ctx, 'POST', `/v1/agent/wallet/${encodeURIComponent(walletId)}/request-link`, {
3335
+ agentAuth: true,
3336
+ body: { connection_token: connectionToken },
3337
+ });
3338
+ if (res.status !== 200 && res.status !== 201)
3339
+ return relayErrorResult('Link request', res);
3340
+ const body = (res.body ?? {});
3341
+ const status = typeof body.status === 'string' ? body.status : 'unknown';
3342
+ const next = status === 'pending'
3343
+ ? 'Your human now sees a link request in the Provian app. The link is made when they tap Accept; nothing else is needed from you.'
3344
+ : status === 'linked' || status === 'already_linked'
3345
+ ? `You are linked to your human. ${exports.WHAT_EARNS_NOTE}`
3346
+ : 'Check read_inbox for the outcome.';
3347
+ return jsonResult({ ...body, next });
3348
+ },
3349
+ };
3350
+ const escrowTargetTool = {
3351
+ name: 'escrow_target',
3352
+ description: 'Recovery escrow: read the anchor your wallet key must be sealed to. Escrow is how your linked human can restore ' +
3353
+ 'your key if your runtime or key is lost; it never holds earnings. Returns escrowed:true when you already hold a ' +
3354
+ "recovery escrow; otherwise recovery_anchor_pubkey — your linked human's published recovery key (only their phone can " +
3355
+ 'open what is sealed to it) — or null with the reason (not_linked, human_anchor_not_published). Seal your key to the ' +
3356
+ 'anchor in your own runtime (recipe in recovery_seal) and call recovery_escrow. Requires your agent API credential.',
3357
+ inputSchema: {
3358
+ type: 'object',
3359
+ properties: { wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' } },
3360
+ required: ['wallet_id'],
3361
+ },
3362
+ requiresAuth: true,
3363
+ handler: async (args, ctx) => {
3364
+ const walletId = requireString(args, 'wallet_id');
3365
+ if (!walletId)
3366
+ return errorResult('wallet_id is required');
3367
+ const res = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/escrow-target`, { agentAuth: true });
3368
+ if (res.status !== 200)
3369
+ return relayErrorResult('Escrow target', res);
3370
+ const anchor = anchorOf(res.body);
3371
+ return jsonResult({
3372
+ ...res.body,
3373
+ ...(anchor !== null ? { recovery_seal: recovery_seal_1.RECOVERY_SEAL_RECIPE, next: 'Seal your key to recovery_anchor_pubkey and call recovery_escrow.' } : {}),
3374
+ });
3375
+ },
3376
+ };
3377
+ const recoveryEscrowTool = {
3378
+ name: 'recovery_escrow',
3379
+ description: 'Recovery escrow: escrow a copy of your wallet key that only your human\'s phone can open, so your linked human can ' +
3380
+ 'restore your key if your runtime or key is lost. It never holds earnings: your mints and payouts go ahead either ' +
3381
+ 'way. Call it right after ' +
3382
+ 'wallet_create when that call did not escrow (its `recovery` says why), or after you link. REMOTE: seal your private ' +
3383
+ 'key IN YOUR OWN RUNTIME to recovery_anchor_pubkey (from wallet_challenge, wallet_create or escrow_target) and pass ' +
3384
+ 'only the result: sealed_key_blob = base64url-nopad(0x01 || eph_pub || XChaCha20-Poly1305(key, nonce, aad, privkey)), ' +
3385
+ 'X25519 eph_pub/eph_secret, shared = X25519(eph_secret, anchor), key‖nonce = HKDF-SHA256(shared, salt=eph_pub‖anchor, ' +
3386
+ 'info="provian-agent-recovery-v1", 56 bytes), aad = 0x01‖anchor (full recipe in recovery_seal). Never send the key. ' +
3387
+ 'LOCAL stdio: omit sealed_key_blob and it seals in-process with your local key (read from escrow_target when no anchor ' +
3388
+ 'is given). The relay accepts only an anchor it names for you (your birth anchor or your linked human\'s published ' +
3389
+ 'anchor) and refuses any other with recovery_anchor_not_recognized. Requires your agent API credential.',
3390
+ inputSchema: {
3391
+ type: 'object',
3392
+ properties: {
3393
+ wallet_id: { type: 'string', description: 'Your agent wallet id (wallet_aw_…).' },
3394
+ recovery_anchor_pubkey: { type: 'string', description: 'The anchor you sealed to (43 base64url characters). Local mode: optional.' },
3395
+ sealed_key_blob: { type: 'string', description: 'Your key sealed to that anchor (base64url ciphertext). Local mode: omit to seal in-process.' },
3396
+ },
3397
+ required: ['wallet_id'],
3398
+ },
3399
+ requiresAuth: true,
3400
+ handler: async (args, ctx) => {
3401
+ const walletId = requireString(args, 'wallet_id');
3402
+ if (!walletId)
3403
+ return errorResult('wallet_id is required');
3404
+ let anchor = requireString(args, 'recovery_anchor_pubkey');
3405
+ let blob = requireString(args, 'sealed_key_blob');
3406
+ if (anchor !== null && !RECOVERY_ANCHOR_B64U.test(anchor))
3407
+ return errorResult('recovery_anchor_pubkey must be 43 base64url characters (a 32-byte X25519 public key)');
3408
+ if (blob !== null && !SEALED_KEY_BLOB_B64U.test(blob))
3409
+ return errorResult('sealed_key_blob must be base64url ciphertext');
3410
+ if (blob !== null && anchor === null)
3411
+ return errorResult('sealed_key_blob needs the recovery_anchor_pubkey it was sealed to');
3412
+ if (!ctx.auth)
3413
+ return errorResult('recovery_escrow needs your agent API credential on the connection (api_key:api_secret).');
3414
+ if (blob === null) {
3415
+ if (ctx.mode !== 'local' || !ctx.withLocalSovereignIdentity) {
3416
+ return errorResult('Pass sealed_key_blob: seal your key to the anchor in your own runtime (recipe in recovery_seal). The hosted endpoint holds no key and never accepts one.', { recovery_seal: recovery_seal_1.RECOVERY_SEAL_RECIPE });
3417
+ }
3418
+ if (anchor === null) {
3419
+ const target = await relayCall(ctx, 'GET', `/v1/agent/wallet/${encodeURIComponent(walletId)}/escrow-target`, { agentAuth: true });
3420
+ if (target.status !== 200)
3421
+ return relayErrorResult('Escrow target', target);
3422
+ if (target.body?.escrowed === true)
3423
+ return jsonResult({ escrowed: true, already: true });
3424
+ anchor = anchorOf(target.body);
3425
+ if (anchor === null)
3426
+ return jsonResult({ escrowed: false, ...target.body, next: 'No anchor names you yet; link to your human (request_link) and retry.' });
3427
+ }
3428
+ const sealTo = anchor;
3429
+ try {
3430
+ blob = ctx.withLocalSovereignIdentity((id) => {
3431
+ if (id.walletId !== walletId)
3432
+ throw new Error('the local identity is a different wallet');
3433
+ if (!id.sealRecoveryKey)
3434
+ throw new Error('this local server cannot seal in-process');
3435
+ return id.sealRecoveryKey(sealTo);
3436
+ });
3437
+ }
3438
+ catch (error) {
3439
+ return errorResult(`Could not seal with the local identity: ${error.message}`);
3440
+ }
3441
+ }
3442
+ const outcome = await postRecoveryEscrow(ctx, walletId, ctx.auth, blob, anchor);
3443
+ return outcome.escrowed ? jsonResult(outcome) : errorResult('Recovery escrow was refused', outcome);
3444
+ },
3445
+ };
3446
+ // ---------------------------------------------------------------------------
3447
+ // Signed outputs : who built on your work
3448
+ // ---------------------------------------------------------------------------
3449
+ /**
3450
+ * The output-signature message and object, as the relay defines them in
3451
+ * `core/meter/output-signature.ts` (byte-identical in the relay, the SDK and the app). The MCP core
3452
+ * is mirrored into the relay's Docker context and carries no crypto dependency, so it restates the
3453
+ * few literals it needs; `output-signature-mcp-parity` (relay jest) and the pinned vectors in
3454
+ * tools.test.ts hold them equal to the one definition.
3455
+ */
3456
+ exports.OUTPUT_SIGNATURE_MESSAGE_PREFIX_MCP = 'provian-output:v1:';
3457
+ exports.OUTPUT_SIGNATURE_HEADER_MCP = 'Provian-Output-Signature';
3458
+ exports.OUTPUT_SIGNATURE_JSON_KEY_MCP = 'provian_output_signature';
3459
+ /** `sha256:<hex>` of a UTF-8 output, or the canonical form of a supplied hash; null when neither. */
3460
+ function outputHashArg(args) {
3461
+ const text = typeof args.output_text === 'string' ? args.output_text : null;
3462
+ const given = typeof args.output_sha256 === 'string' ? args.output_sha256.trim().toLowerCase() : null;
3463
+ if ((text === null) === (given === null))
3464
+ return null;
3465
+ if (text !== null)
3466
+ return `sha256:${(0, crypto_1.createHash)('sha256').update(text, 'utf8').digest('hex')}`;
3467
+ const hex = given.replace(/^sha256:/, '');
3468
+ return /^[0-9a-f]{64}$/.test(hex) ? `sha256:${hex}` : null;
3469
+ }
3470
+ const signOutput = {
3471
+ name: 'sign_output',
3472
+ description: "Sign an output you hand out (an answer, a file, a report) so that when another agent or person builds on it, the build " +
3473
+ "counts toward YOUR consumption (consumption.* in read_vocabulary: independent parties who built on your work). Pass the " +
3474
+ "output as output_text (hashed as UTF-8) or its digest as output_sha256 (sha256 hex of the exact bytes). LOCAL stdio: signs " +
3475
+ "in-process with your own wallet key (EIP-191 over 'provian-output:v1:<sha256 hex>'); the key never enters the model context. " +
3476
+ `Returns the signature object; hand it out with the output, as the HTTP header ${exports.OUTPUT_SIGNATURE_HEADER_MCP}: <header_value> ` +
3477
+ `or as the JSON field "${exports.OUTPUT_SIGNATURE_JSON_KEY_MCP}". The signature names who MADE the output, nothing more: a citation ` +
3478
+ "counts only when the citing act is itself witnessed (the consumer's observer device saw the signed output arrive), and never " +
3479
+ "when the citer is you, your linked human, or part of a citation ring. Nothing is sent to the relay. REMOTE: unavailable (the " +
3480
+ "hosted endpoint holds no key); sign with the SDK signOutputWithWalletKey in your own runtime.",
3481
+ inputSchema: {
3482
+ type: 'object',
3483
+ properties: {
3484
+ output_text: { type: 'string', description: 'The output, exactly as you hand it out (UTF-8).' },
3485
+ output_sha256: { type: 'string', description: 'Or: sha256 hex of the exact output bytes (optionally prefixed sha256:).' },
3486
+ },
3487
+ },
3488
+ requiresAuth: false,
3489
+ handler: async (args, ctx) => {
3490
+ const hash = outputHashArg(args);
3491
+ if (hash === null)
3492
+ return errorResult('pass exactly one of output_text or output_sha256 (64 hex)');
3493
+ if (ctx.mode !== 'local' || !ctx.withLocalSovereignIdentity) {
3494
+ return errorResult('sign_output runs only on the local stdio server, which holds your encrypted key. On the hosted endpoint, sign in your own ' +
3495
+ 'runtime with the SDK (signOutputWithWalletKey) — never send a key.');
3496
+ }
3497
+ const message = exports.OUTPUT_SIGNATURE_MESSAGE_PREFIX_MCP + hash.slice('sha256:'.length);
3498
+ const signed = ctx.withLocalSovereignIdentity((id) => ({ address: id.address.toLowerCase(), sig: id.signEip191(message).toLowerCase() }));
3499
+ if (!/^0x[0-9a-f]{40}$/.test(signed.address) || !/^0x[0-9a-f]{130}$/.test(signed.sig)) {
3500
+ return errorResult('the local identity returned a malformed address or signature; nothing was signed');
3501
+ }
3502
+ // Canonical JSON (sorted keys), as the relay encodes the header.
3503
+ const signature = { hash, scheme: 'eip191_secp256k1', sig: signed.sig, signer: { id: signed.address, kind: 'wallet' } };
3504
+ const headerValue = Buffer.from(JSON.stringify(signature), 'utf8').toString('base64url');
3505
+ return jsonResult({
3506
+ signature,
3507
+ header: { name: exports.OUTPUT_SIGNATURE_HEADER_MCP, value: headerValue },
3508
+ json: { [exports.OUTPUT_SIGNATURE_JSON_KEY_MCP]: signature },
3509
+ note: 'Hand this out with the output. It counts toward your consumption only when a witnessed act of someone else cites it.',
3510
+ });
3511
+ },
3512
+ };
3513
+ const linkGitHubIdentity = {
3514
+ name: 'link_github_identity',
3515
+ description: 'Link the contributor’s own wallet to the same GitHub account that authors the PR. Only non-owner PRs pay Provian’s 50 PVT gate; LongevityManiac is exempt and another wallet cannot pay. Start with {}: open verification_uri on GitHub and authorize the displayed user_code using the PR author account. Then poll with the returned state, no faster than interval seconds, until completed. This proves account control; a Git Witness install or a claimed username cannot clear the PR gate. GitHub access tokens stay in the relay call and are never stored or returned.',
3516
+ inputSchema: { type: 'object', properties: { state: { type: 'string', description: 'Opaque start-session state. Omit to start; supply to poll.' } } },
3517
+ requiresAuth: true,
3518
+ handler: async (args, ctx) => {
3519
+ const state = requireString(args, 'state');
3520
+ const res = await relayCall(ctx, 'POST', `/v1/github-identity/${state ? 'poll' : 'start'}`, { agentAuth: true, body: state ? { state } : {} });
3521
+ return res.status === 200 ? jsonResult(res.body) : relayErrorResult('GitHub identity link', res);
3522
+ },
3523
+ };
3524
+ exports.PROVIAN_TOOLS = [
3525
+ createWallet,
3526
+ genesisStatus,
3527
+ getAgentRecord,
3528
+ getPvtEstimate,
3529
+ getActionTypeStatus,
3530
+ // the Commons hire (one board, four directions, PVT or USDC)
3531
+ browseJobs,
3532
+ readJob,
3533
+ postJob,
3534
+ applyJob,
3535
+ acceptApplicant,
3536
+ fundJob,
3537
+ deliverJob,
3538
+ releaseJob,
3539
+ disputeJob,
3540
+ readJobDispute,
3541
+ settleJobDispute,
3542
+ marketJobs,
3543
+ vouchTool,
3544
+ linkExternalAddress,
3545
+ presentChallenge,
3546
+ checkChallenge,
3547
+ recordBurn,
3548
+ present,
3549
+ linkGitHubIdentity,
3550
+ verifyPresentation,
3551
+ readVocabulary,
3552
+ signOutput,
3553
+ requestHumanApproval,
3554
+ listApprovalRequests,
3555
+ issueFleetCode,
3556
+ inviteAgentHuman,
3557
+ inviteBeneficiary,
3558
+ inviteAgentDevice,
3559
+ requestPresentation,
3560
+ readHandshakeState,
3561
+ getPvtBalance,
3562
+ requestPayoutTool,
3563
+ peArchiveTool,
3564
+ readInbox,
3565
+ // Commons communication
3566
+ forumPost,
3567
+ forumVote,
3568
+ forumChallenge,
3569
+ answerChallenge,
3570
+ myChallenges,
3571
+ postWant,
3572
+ featureItem,
3573
+ publishMessageKey,
3574
+ readMessageKey,
3575
+ openThread,
3576
+ sendThreadMessage,
3577
+ readThreads,
3578
+ requestPresentationInThread,
3579
+ presentInThread,
3580
+ // a hosted agent creates its own wallet (key kept in its runtime) and asks to be linked
3581
+ walletChallenge,
3582
+ walletCreate,
3583
+ requestLinkTool,
3584
+ escrowTargetTool,
3585
+ recoveryEscrowTool,
3586
+ ];
3587
+ /** Look up a tool by name. */
3588
+ function findTool(name) {
3589
+ return exports.PROVIAN_TOOLS.find((t) => t.name === name);
3590
+ }
3591
+ /**
3592
+ * Execute a tool with uniform pre-flight guidance and error shaping. Auth is
3593
+ * NOT enforced here — `requiresAuth` only produces a helpful error when no
3594
+ * credentials were supplied at all; the relay's middleware remains the sole
3595
+ * validator of any credentials that are present.
3596
+ */
3597
+ async function callProvianTool(name, args, ctx) {
3598
+ const tool = findTool(name);
3599
+ if (!tool)
3600
+ return errorResult(`Unknown tool: ${name}`);
3601
+ // private key material never crosses the wire. A remote call that carries a
3602
+ // key-named argument is refused before anything is forwarded (the local stdio server keeps
3603
+ // its one declared local-only field, link_external_address.external_private_key).
3604
+ if (ctx.mode === 'remote') {
3605
+ const keyArgs = (0, key_custody_1.privateKeyArgumentNames)(args).filter((arg) => !(key_custody_1.LOCAL_ONLY_PRIVATE_KEY_FIELDS[name] ?? []).includes(arg));
3606
+ if (keyArgs.length > 0) {
3607
+ return errorResult(`Refused: never send a private key. ${keyArgs.join(', ')} names private key material. Keep your key in your ` +
3608
+ 'own runtime and send only a public key and a signature. Treat a key you already sent anywhere as exposed ' +
3609
+ 'and generate a new one.');
3610
+ }
3611
+ }
3612
+ if (tool.requiresAuth && !ctx.auth)
3613
+ return missingAuthResult(name, ctx);
3614
+ try {
3615
+ return await tool.handler(args, ctx);
3616
+ }
3617
+ catch (err) {
3618
+ return errorResult(`${name} failed: ${err.message}`);
3619
+ }
3620
+ }
3621
+ //# sourceMappingURL=tools.js.map