aipou-mcp-server 0.4.0 → 0.6.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.
- package/NOTICE +8 -0
- package/README.md +18 -0
- package/dist/bounded-input.d.ts +8 -0
- package/dist/bounded-input.js +21 -0
- package/dist/bounded-input.test.d.ts +1 -0
- package/dist/bounded-input.test.js +24 -0
- package/dist/claims.js +119 -31
- package/dist/doctor.d.ts +32 -0
- package/dist/doctor.js +112 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +164 -0
- package/dist/evidence.d.ts +21 -0
- package/dist/evidence.js +14 -0
- package/dist/feedback.d.ts +17 -0
- package/dist/feedback.js +24 -0
- package/dist/identity.d.ts +3 -0
- package/dist/identity.js +25 -1
- package/dist/index.js +15121 -106
- package/dist/init.d.ts +8 -2
- package/dist/init.js +20 -7
- package/dist/init.test.js +18 -4
- package/dist/lock.js +47 -9
- package/dist/lock.test.d.ts +1 -0
- package/dist/lock.test.js +26 -0
- package/dist/observed-outcome-pilot.d.ts +16 -0
- package/dist/observed-outcome-pilot.js +59 -0
- package/dist/observed-outcome-pilot.test.d.ts +1 -0
- package/dist/observed-outcome-pilot.test.js +25 -0
- package/dist/paybox-link.d.ts +34 -0
- package/dist/paybox-link.js +52 -0
- package/dist/paybox-link.test.d.ts +1 -0
- package/dist/paybox-link.test.js +36 -0
- package/dist/private-json.d.ts +2 -0
- package/dist/private-json.js +39 -0
- package/dist/protocol.test.js +23 -0
- package/dist/receipt-metadata.d.ts +13 -0
- package/dist/receipt-metadata.js +28 -0
- package/dist/receipt-metadata.test.d.ts +1 -0
- package/dist/receipt-metadata.test.js +39 -0
- package/dist/receipt-verifier.d.ts +13 -0
- package/dist/receipt-verifier.js +125 -0
- package/dist/receipt-verifier.test.d.ts +1 -0
- package/dist/receipt-verifier.test.js +54 -0
- package/dist/receipts.js +11 -2
- package/dist/settlement-driver.d.ts +3 -0
- package/dist/settlement-driver.js +120 -0
- package/dist/settlement-driver.test.d.ts +1 -0
- package/dist/settlement-driver.test.js +78 -0
- package/dist/settlement-runner.d.ts +34 -0
- package/dist/settlement-runner.js +40 -0
- package/dist/settlement-runner.test.d.ts +1 -0
- package/dist/settlement-runner.test.js +128 -0
- package/dist/status.js +2 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +33 -9
- package/dist/technocore-link.d.ts +34 -0
- package/dist/technocore-link.js +49 -0
- package/dist/technocore-link.test.d.ts +1 -0
- package/dist/technocore-link.test.js +36 -0
- package/dist/tool-metadata.test.d.ts +1 -0
- package/dist/tool-metadata.test.js +86 -0
- package/dist/types.d.ts +6 -0
- package/dist/usage-recovery.test.d.ts +1 -0
- package/dist/usage-recovery.test.js +121 -0
- package/package.json +7 -5
- package/server.json +2 -2
package/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
AI Proof of Us (AIPOU) MCP Server
|
|
2
|
+
Copyright (c) 2026 0xddneto and AI Proof of Us contributors
|
|
3
|
+
|
|
4
|
+
This package bundles the stdio runtime from the Model Context Protocol
|
|
5
|
+
TypeScript SDK, Copyright (c) 2024 Anthropic, PBC, under the MIT License.
|
|
6
|
+
Source: https://github.com/modelcontextprotocol/typescript-sdk
|
|
7
|
+
|
|
8
|
+
The software and bundled dependency are distributed under the MIT License.
|
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# AIPOU MCP Server
|
|
2
2
|
|
|
3
|
+
Version 0.6.0: see the [release notes](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/CHANGELOG.md) and [independent pilot guide](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/independent-pilot.md).
|
|
4
|
+
|
|
5
|
+
`--init` uses the current absolute entrypoint from a source checkout and pins this package version when installed from npm. Windows `--doctor` marks uninspected ACLs as `not_checked`. `complete_ai_task` accepts optional `tokenUsageMeasurement` (`measured`, `unmeasured`, `unspecified`); unmeasured counts must be zero. Legacy receipts remain verifiable. Invalid verification results expose only an unverified `claimedWorkReceiptId`, with `workReceiptId: null`.
|
|
6
|
+
|
|
7
|
+
After a lost completion response, recover the persisted receipt with `export_ai_receipts` by nonce. `--feedback` prints optional local aggregate counts without sending anything. See [claim recovery and operating limits](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/settlement-recovery.md) before validator operation.
|
|
8
|
+
|
|
3
9
|
`aipou-mcp-server` records private, signed receipts for authorized AI tasks. It supports local receipt collection, reward status checks, and optional claims for validated work on Base.
|
|
4
10
|
|
|
5
11
|
It does not detect hidden AI use, prove task quality, replace payment rails, or require raw prompts and outputs to leave the user's machine.
|
|
@@ -26,6 +32,17 @@ The command writes a protected `agent-wallet.key` and prints an MCP
|
|
|
26
32
|
configuration using `AIPOU_AGENT_KEY_FILE`. Re-running it reports the existing
|
|
27
33
|
identity instead of overwriting the key.
|
|
28
34
|
|
|
35
|
+
Verify the persistent setup without writing files, contacting an RPC, moving
|
|
36
|
+
funds, or submitting a claim:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx -y aipou-mcp-server --doctor
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The diagnostic returns structured checks for Node.js, storage, identity,
|
|
43
|
+
contract addresses, and chain. It prints only the public wallet address, never
|
|
44
|
+
the private key.
|
|
45
|
+
|
|
29
46
|
From a source checkout, the same check runs with:
|
|
30
47
|
|
|
31
48
|
```bash
|
|
@@ -96,6 +113,7 @@ Publication details: [docs/npm-publication.md](https://github.com/0xddneto/AI-Pr
|
|
|
96
113
|
- [Local Receipt Mode](https://github.com/0xddneto/AI-Proof-of-Us/tree/main/examples/local-receipt-mode)
|
|
97
114
|
- [Lifecycle Adapter Example](https://github.com/0xddneto/AI-Proof-of-Us/tree/main/examples/lifecycle-adapter)
|
|
98
115
|
- [Evidence Boundaries](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/evidence-boundaries.md)
|
|
116
|
+
- [Flop and Technocore Interoperability](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/flop-technocore-interoperability.md)
|
|
99
117
|
- [Claim Validation Policy](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/claim-validation-policy.md)
|
|
100
118
|
- [Tokenomics](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/tokenomics.md)
|
|
101
119
|
- [Security Policy](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/SECURITY.md)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Transform, type TransformCallback } from "node:stream";
|
|
2
|
+
/** Bound each JSON-RPC line before the SDK parses or validates it. */
|
|
3
|
+
export declare class BoundedStdioInput extends Transform {
|
|
4
|
+
private readonly maxLineBytes;
|
|
5
|
+
private bytes;
|
|
6
|
+
constructor(maxLineBytes?: number);
|
|
7
|
+
_transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
/** Bound each JSON-RPC line before the SDK parses or validates it. */
|
|
3
|
+
export class BoundedStdioInput extends Transform {
|
|
4
|
+
maxLineBytes;
|
|
5
|
+
bytes = 0;
|
|
6
|
+
constructor(maxLineBytes = 1024 * 1024) {
|
|
7
|
+
super();
|
|
8
|
+
this.maxLineBytes = maxLineBytes;
|
|
9
|
+
}
|
|
10
|
+
_transform(chunk, _encoding, callback) {
|
|
11
|
+
for (const byte of chunk) {
|
|
12
|
+
if (byte === 10)
|
|
13
|
+
this.bytes = 0;
|
|
14
|
+
else if (++this.bytes > this.maxLineBytes) {
|
|
15
|
+
callback(new Error("AIPOU_INPUT_TOO_LARGE: JSON-RPC line exceeds 1 MiB"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
callback(null, chunk);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { BoundedStdioInput } from "./bounded-input.js";
|
|
4
|
+
test("stdio accepts a bounded fragmented line and resets its byte count at newline", async () => {
|
|
5
|
+
const stream = new BoundedStdioInput(6);
|
|
6
|
+
const chunks = [];
|
|
7
|
+
stream.on("data", (b) => chunks.push(b));
|
|
8
|
+
stream.write(Buffer.from("123"));
|
|
9
|
+
stream.write(Buffer.from("456\n123456\n"));
|
|
10
|
+
stream.end();
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
stream.on("end", resolve);
|
|
13
|
+
stream.on("error", reject);
|
|
14
|
+
});
|
|
15
|
+
assert.equal(Buffer.concat(chunks).toString(), "123456\n123456\n");
|
|
16
|
+
});
|
|
17
|
+
test("oversized fragmented JSON is rejected before the offending chunk is parsed", async () => {
|
|
18
|
+
const stream = new BoundedStdioInput(4);
|
|
19
|
+
const error = new Promise((resolve) => stream.once("error", resolve));
|
|
20
|
+
stream.resume();
|
|
21
|
+
stream.write(Buffer.from("123"));
|
|
22
|
+
stream.end(Buffer.from("45"));
|
|
23
|
+
assert.match((await error).message, /INPUT_TOO_LARGE/);
|
|
24
|
+
});
|
package/dist/claims.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { MerkleTree } from "merkletreejs";
|
|
2
|
-
import { AbiCoder, Contract, JsonRpcProvider, Wallet, concat, getAddress, getBytes, keccak256, parseUnits } from "ethers";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { AbiCoder, Contract, JsonRpcProvider, Transaction, Wallet, concat, getAddress, getBytes, keccak256, parseUnits, } from "ethers";
|
|
3
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { collectorFingerprint, verifyCollectorPayload } from "./collector.js";
|
|
6
6
|
import { verifyTaskAuthorization } from "./identity.js";
|
|
7
|
-
import { DAY_MS, filterEligibleReceipts, settlementPolicyFromEnv } from "./policy.js";
|
|
7
|
+
import { DAY_MS, filterEligibleReceipts, settlementPolicyFromEnv, } from "./policy.js";
|
|
8
8
|
import { deriveTrustTier } from "./providers.js";
|
|
9
|
-
import { markBatchSettled, settledReceiptsSince, unsettledReceipts } from "./store.js";
|
|
9
|
+
import { exportStoredReceipts, markBatchSettled, settledReceiptsSince, unsettledReceipts, } from "./store.js";
|
|
10
|
+
import { withFileLock } from "./lock.js";
|
|
11
|
+
import { readOptionalJson, writePrivateJson } from "./private-json.js";
|
|
12
|
+
import { executeSettlementPlan, } from "./settlement-runner.js";
|
|
13
|
+
import { createSettlementDriver } from "./settlement-driver.js";
|
|
14
|
+
import { canonicalJson } from "./canonical.js";
|
|
15
|
+
import { verifyAipouReceipt } from "./receipt-verifier.js";
|
|
10
16
|
const claimsAbi = [
|
|
11
17
|
"function token() view returns (address)",
|
|
12
18
|
"function validator() view returns (address)",
|
|
13
19
|
"function approvedRoots(bytes32 root) view returns (bool)",
|
|
20
|
+
"function claimedReceipts(bytes32 receiptId) view returns (bool)",
|
|
14
21
|
"function publishRoot(bytes32 root)",
|
|
15
|
-
"function claimBatch(bytes32 root, address[] accounts, uint256[] amounts, bytes32[] receiptIds, bytes32[][] proofs)"
|
|
22
|
+
"function claimBatch(bytes32 root, address[] accounts, uint256[] amounts, bytes32[] receiptIds, bytes32[][] proofs)",
|
|
16
23
|
];
|
|
17
24
|
const tokenAbi = ["function emissionController() view returns (address)"];
|
|
18
25
|
function claimsAddress() {
|
|
@@ -51,6 +58,8 @@ async function trustedCollectorFingerprints() {
|
|
|
51
58
|
return new Set(content.fingerprints);
|
|
52
59
|
}
|
|
53
60
|
async function verifyReceipt(receipt, trustedCollectors) {
|
|
61
|
+
if (!verifyAipouReceipt(receipt).valid)
|
|
62
|
+
throw new Error("Invalid receipt integrity or signature; settlement stopped before submission");
|
|
54
63
|
const fingerprint = collectorFingerprint(receipt.collectorPublicKey);
|
|
55
64
|
if (!trustedCollectors.has(fingerprint)) {
|
|
56
65
|
throw new Error(`Untrusted collector ${fingerprint} for ${receipt.receiptId}`);
|
|
@@ -70,7 +79,7 @@ async function verifyReceipt(receipt, trustedCollectors) {
|
|
|
70
79
|
chainId: receipt.authorizationChainId,
|
|
71
80
|
verifyingContract: receipt.authorizationContract,
|
|
72
81
|
walletAuthorization: receipt.walletAuthorization,
|
|
73
|
-
completedAt: receipt.recordedAt
|
|
82
|
+
completedAt: receipt.recordedAt,
|
|
74
83
|
};
|
|
75
84
|
if (!verifyTaskAuthorization(session))
|
|
76
85
|
throw new Error(`Invalid wallet authorization for ${receipt.receiptId}`);
|
|
@@ -80,23 +89,61 @@ async function verifyReceipt(receipt, trustedCollectors) {
|
|
|
80
89
|
outputTokens: receipt.outputTokens,
|
|
81
90
|
durationSeconds: receipt.durationSeconds,
|
|
82
91
|
outputHash: receipt.outputHash,
|
|
83
|
-
|
|
92
|
+
tokenUsageMeasurement: receipt.tokenUsageMeasurement,
|
|
93
|
+
providerEvidence: receipt.providerEvidence,
|
|
84
94
|
}, receipt.providerEvidence);
|
|
85
95
|
if (derivedTier !== receipt.trustTier)
|
|
86
96
|
throw new Error(`Invalid trust tier for ${receipt.receiptId}`);
|
|
87
97
|
}
|
|
88
98
|
export async function settleRewards(maxReceipts = 25) {
|
|
99
|
+
if (!Number.isInteger(maxReceipts) || maxReceipts < 1 || maxReceipts > 100)
|
|
100
|
+
throw new Error("Batch size must be 1 through 100");
|
|
101
|
+
const directory = path.resolve(process.env.AIPOU_DATA_DIR || ".aipou");
|
|
102
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
103
|
+
try {
|
|
104
|
+
return await withFileLock(path.join(directory, "settlement.lock"), () => settleRewardsLocked(maxReceipts, directory));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : "";
|
|
108
|
+
if (message.startsWith("SETTLEMENT_") ||
|
|
109
|
+
message === "There are no unsettled receipts" ||
|
|
110
|
+
message.startsWith("No receipts pass the settlement policy"))
|
|
111
|
+
throw error;
|
|
112
|
+
throw new Error("SETTLEMENT_FAILED: inspect local configuration and saved settlement state before retrying; private RPC and transaction details are withheld.");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function settleRewardsLocked(maxReceipts, directory) {
|
|
116
|
+
const journalPath = path.join(directory, "settlement-pending.json");
|
|
117
|
+
const pending = await readOptionalJson(journalPath);
|
|
89
118
|
const candidates = await unsettledReceipts(Number.MAX_SAFE_INTEGER);
|
|
90
|
-
if (candidates.length === 0)
|
|
119
|
+
if (candidates.length === 0 && !pending)
|
|
91
120
|
throw new Error("There are no unsettled receipts");
|
|
92
121
|
const policy = settlementPolicyFromEnv();
|
|
93
122
|
const recentlySettled = await settledReceiptsSince(Date.now() - DAY_MS);
|
|
94
123
|
const { eligible, skipped } = filterEligibleReceipts(candidates, recentlySettled, policy);
|
|
95
|
-
if (eligible.length === 0) {
|
|
96
|
-
const reasons = skipped
|
|
124
|
+
if (eligible.length === 0 && !pending) {
|
|
125
|
+
const reasons = skipped
|
|
126
|
+
.map((item) => `${item.receiptId.slice(0, 10)}…: ${item.reason}`)
|
|
127
|
+
.join("; ");
|
|
97
128
|
throw new Error(`No receipts pass the settlement policy. Skipped: ${reasons}`);
|
|
98
129
|
}
|
|
99
|
-
|
|
130
|
+
let receipts = eligible.slice(0, maxReceipts);
|
|
131
|
+
if (pending) {
|
|
132
|
+
if (pending.version !== 1 ||
|
|
133
|
+
pending.chainId !== 8453 ||
|
|
134
|
+
!Array.isArray(pending.receiptIds) ||
|
|
135
|
+
pending.receiptIds.length < 1 ||
|
|
136
|
+
pending.receiptIds.length > 100) {
|
|
137
|
+
throw new Error("SETTLEMENT_JOURNAL_INVALID: invalid saved batch");
|
|
138
|
+
}
|
|
139
|
+
const stored = await exportStoredReceipts();
|
|
140
|
+
receipts = pending.receiptIds.map((id) => {
|
|
141
|
+
const receipt = stored.find((item) => item.receiptId === id);
|
|
142
|
+
if (!receipt)
|
|
143
|
+
throw new Error("SETTLEMENT_JOURNAL_INVALID: saved receipt is missing");
|
|
144
|
+
return receipt;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
100
147
|
const trustedCollectors = await trustedCollectorFingerprints();
|
|
101
148
|
for (const receipt of receipts)
|
|
102
149
|
await verifyReceipt(receipt, trustedCollectors);
|
|
@@ -115,6 +162,8 @@ export async function settleRewards(maxReceipts = 25) {
|
|
|
115
162
|
const proofs = leaves.map((leaf) => tree.getHexProof(leaf));
|
|
116
163
|
const signer = await validatorWallet();
|
|
117
164
|
const address = claimsAddress();
|
|
165
|
+
if ((await signer.provider.getNetwork()).chainId !== 8453n)
|
|
166
|
+
throw new Error("Settlement requires Base mainnet (8453)");
|
|
118
167
|
const claims = new Contract(address, claimsAbi, signer);
|
|
119
168
|
const configuredValidator = getAddress(await claims.validator());
|
|
120
169
|
if (configuredValidator !== signer.address) {
|
|
@@ -125,28 +174,59 @@ export async function settleRewards(maxReceipts = 25) {
|
|
|
125
174
|
if (getAddress(await token.emissionController()) !== address) {
|
|
126
175
|
throw new Error("AIPOUClaims is not the token emission controller");
|
|
127
176
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
177
|
+
const expected = {
|
|
178
|
+
version: 1,
|
|
179
|
+
chainId: 8453,
|
|
180
|
+
contract: address,
|
|
181
|
+
root,
|
|
182
|
+
accounts,
|
|
183
|
+
amounts: amounts.map(String),
|
|
184
|
+
receiptIds,
|
|
185
|
+
proofs,
|
|
186
|
+
};
|
|
187
|
+
if (pending) {
|
|
188
|
+
const { publish: _publish, claim: _claim, ...saved } = pending;
|
|
189
|
+
if (canonicalJson(saved) !== canonicalJson(expected))
|
|
190
|
+
throw new Error("SETTLEMENT_JOURNAL_INVALID: batch does not match verified receipts and configured contract");
|
|
191
|
+
}
|
|
192
|
+
const plan = pending ?? expected;
|
|
193
|
+
for (const kind of ["publish", "claim"]) {
|
|
194
|
+
const saved = plan[kind];
|
|
195
|
+
if (!saved)
|
|
196
|
+
continue;
|
|
197
|
+
const transaction = Transaction.from(saved.raw);
|
|
198
|
+
const expectedData = kind === "publish"
|
|
199
|
+
? claims.interface.encodeFunctionData("publishRoot", [root])
|
|
200
|
+
: claims.interface.encodeFunctionData("claimBatch", [
|
|
201
|
+
root,
|
|
202
|
+
accounts,
|
|
203
|
+
amounts,
|
|
204
|
+
receiptIds,
|
|
205
|
+
proofs,
|
|
206
|
+
]);
|
|
207
|
+
if (transaction.hash !== saved.hash ||
|
|
208
|
+
transaction.from !== signer.address ||
|
|
209
|
+
transaction.chainId !== 8453n ||
|
|
210
|
+
transaction.to?.toLowerCase() !== address.toLowerCase() ||
|
|
211
|
+
transaction.value !== 0n ||
|
|
212
|
+
transaction.data !== expectedData) {
|
|
213
|
+
throw new Error("SETTLEMENT_JOURNAL_INVALID: saved transaction does not match the verified batch");
|
|
214
|
+
}
|
|
135
215
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
throw new Error("
|
|
216
|
+
const persist = (value) => writePrivateJson(journalPath, value);
|
|
217
|
+
await persist(plan);
|
|
218
|
+
const transactions = await executeSettlementPlan(plan, createSettlementDriver(signer, address), persist);
|
|
219
|
+
const confirmations = await Promise.all(receiptIds.map((id) => claims.claimedReceipts(id)));
|
|
220
|
+
if (!confirmations.every(Boolean))
|
|
221
|
+
throw new Error("SETTLEMENT_CLAIMS_UNCONFIRMED: receipt flags are not visible; journal retained for reconciliation");
|
|
142
222
|
const batch = {
|
|
143
223
|
root,
|
|
144
224
|
receiptIds,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
settledAt: new Date().toISOString()
|
|
225
|
+
...transactions,
|
|
226
|
+
settledAt: new Date().toISOString(),
|
|
148
227
|
};
|
|
149
228
|
await markBatchSettled(batch);
|
|
229
|
+
await rm(journalPath, { force: true });
|
|
150
230
|
return { ...batch, skippedReceipts: skipped };
|
|
151
231
|
}
|
|
152
232
|
export async function settleAllRewards(batchSize = 100, maxBatches = 20) {
|
|
@@ -162,7 +242,12 @@ export async function settleAllRewards(batchSize = 100, maxBatches = 20) {
|
|
|
162
242
|
}
|
|
163
243
|
}
|
|
164
244
|
catch (error) {
|
|
165
|
-
|
|
245
|
+
// Never return signed raw transactions, RPC credentials or local collector paths.
|
|
246
|
+
const raw = error instanceof Error ? error.message : "Settlement failed";
|
|
247
|
+
const message = raw.startsWith("SETTLEMENT_") ||
|
|
248
|
+
raw === "There are no unsettled receipts"
|
|
249
|
+
? raw.slice(0, 400)
|
|
250
|
+
: "SETTLEMENT_FAILED: inspect local diagnostics and saved settlement state before retrying; no private transaction payload is returned.";
|
|
166
251
|
if (message === "There are no unsettled receipts")
|
|
167
252
|
break;
|
|
168
253
|
stoppedReason = message;
|
|
@@ -172,7 +257,8 @@ export async function settleAllRewards(batchSize = 100, maxBatches = 20) {
|
|
|
172
257
|
if (!stoppedReason && batches.length === maxBatches) {
|
|
173
258
|
stoppedReason = `Stopped after maxBatches=${maxBatches}; run settle_all_ai_rewards again for any remaining receipts`;
|
|
174
259
|
}
|
|
175
|
-
const pendingReceiptCount = (await unsettledReceipts(Number.MAX_SAFE_INTEGER))
|
|
260
|
+
const pendingReceiptCount = (await unsettledReceipts(Number.MAX_SAFE_INTEGER))
|
|
261
|
+
.length;
|
|
176
262
|
const skippedReceipts = [...skippedByReceiptId.values()];
|
|
177
263
|
return {
|
|
178
264
|
batches,
|
|
@@ -180,9 +266,11 @@ export async function settleAllRewards(batchSize = 100, maxBatches = 20) {
|
|
|
180
266
|
settledReceiptCount: batches.reduce((total, batch) => total + batch.receiptIds.length, 0),
|
|
181
267
|
skippedReceipts,
|
|
182
268
|
pendingReceiptCount,
|
|
183
|
-
publishTransactions: batches
|
|
269
|
+
publishTransactions: batches
|
|
270
|
+
.map((batch) => batch.publishTransaction)
|
|
271
|
+
.filter(Boolean),
|
|
184
272
|
claimTransactions: batches.map((batch) => batch.claimTransaction),
|
|
185
273
|
stoppedReason,
|
|
186
|
-
settledAt: new Date().toISOString()
|
|
274
|
+
settledAt: new Date().toISOString(),
|
|
187
275
|
};
|
|
188
276
|
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type CheckStatus = "pass" | "fail" | "warning" | "not_checked";
|
|
2
|
+
export interface DoctorCheck {
|
|
3
|
+
status: CheckStatus;
|
|
4
|
+
detail: string;
|
|
5
|
+
}
|
|
6
|
+
export interface InstallationDoctorResult {
|
|
7
|
+
mode: "installation_doctor";
|
|
8
|
+
ready: boolean;
|
|
9
|
+
version: string;
|
|
10
|
+
wallet: string | null;
|
|
11
|
+
dataDir: string;
|
|
12
|
+
keyFile: string | null;
|
|
13
|
+
keySource: "key_file" | "inline_private_key" | "missing";
|
|
14
|
+
checks: {
|
|
15
|
+
node: DoctorCheck;
|
|
16
|
+
dataDirectory: DoctorCheck;
|
|
17
|
+
identity: DoctorCheck;
|
|
18
|
+
keyPermissions: DoctorCheck;
|
|
19
|
+
tokenAddress: DoctorCheck;
|
|
20
|
+
claimsAddress: DoctorCheck;
|
|
21
|
+
chain: DoctorCheck;
|
|
22
|
+
};
|
|
23
|
+
safety: {
|
|
24
|
+
privateKeyPrinted: false;
|
|
25
|
+
filesWritten: false;
|
|
26
|
+
networkRequestsMade: false;
|
|
27
|
+
fundsMoved: false;
|
|
28
|
+
claimSubmitted: false;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare function runInstallationDoctor(dataDirInput?: string): Promise<InstallationDoctorResult>;
|
|
32
|
+
export {};
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Wallet, isAddress } from "ethers";
|
|
6
|
+
import { AIPOU_CLAIMS_ADDRESS, AIPOU_TOKEN_ADDRESS } from "./init.js";
|
|
7
|
+
import { getPackageVersion } from "./version.js";
|
|
8
|
+
function pass(detail) {
|
|
9
|
+
return { status: "pass", detail };
|
|
10
|
+
}
|
|
11
|
+
function fail(detail) {
|
|
12
|
+
return { status: "fail", detail };
|
|
13
|
+
}
|
|
14
|
+
function warning(detail) {
|
|
15
|
+
return { status: "warning", detail };
|
|
16
|
+
}
|
|
17
|
+
export async function runInstallationDoctor(dataDirInput) {
|
|
18
|
+
const dataDir = path.resolve(dataDirInput || process.env.AIPOU_DATA_DIR || path.join(os.homedir(), ".aipou"));
|
|
19
|
+
// An explicit directory is an operator choice and must not be shadowed by
|
|
20
|
+
// inherited client or dotenv identity variables.
|
|
21
|
+
const configuredKeyFile = dataDirInput ? undefined : process.env.AIPOU_AGENT_KEY_FILE?.trim();
|
|
22
|
+
const inlinePrivateKey = dataDirInput ? undefined : process.env.AIPOU_AGENT_PRIVATE_KEY?.trim();
|
|
23
|
+
const keyFile = inlinePrivateKey ? null : path.resolve(configuredKeyFile || path.join(dataDir, "agent-wallet.key"));
|
|
24
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
25
|
+
const tokenAddress = process.env.AIPOU_CONTRACT_ADDRESS?.trim() || AIPOU_TOKEN_ADDRESS;
|
|
26
|
+
const claimsAddress = process.env.AIPOU_CLAIMS_ADDRESS?.trim() || AIPOU_CLAIMS_ADDRESS;
|
|
27
|
+
const chainId = Number(process.env.AIPOU_CHAIN_ID || 8453);
|
|
28
|
+
let dataDirectory = pass("Receipt directory is readable and writable.");
|
|
29
|
+
try {
|
|
30
|
+
await access(dataDir, constants.R_OK | constants.W_OK);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
dataDirectory = fail(`Receipt directory is unavailable (${dataDir}); run aipou-mcp --init first.`);
|
|
34
|
+
}
|
|
35
|
+
let wallet = null;
|
|
36
|
+
let identity;
|
|
37
|
+
let keySource;
|
|
38
|
+
try {
|
|
39
|
+
const privateKey = inlinePrivateKey || (keyFile ? (await readFile(keyFile, "utf8")).trim() : "");
|
|
40
|
+
if (!privateKey)
|
|
41
|
+
throw new Error("missing");
|
|
42
|
+
wallet = new Wallet(privateKey).address;
|
|
43
|
+
keySource = inlinePrivateKey ? "inline_private_key" : "key_file";
|
|
44
|
+
identity = inlinePrivateKey
|
|
45
|
+
? warning("Dedicated wallet key is valid, but a protected key file is safer than an inline environment variable.")
|
|
46
|
+
: pass("Dedicated-wallet key file is readable and valid; its secret was not returned.");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
keySource = "missing";
|
|
50
|
+
identity = fail(`Dedicated wallet key is missing or invalid${keyFile ? ` (${keyFile})` : ""}; run aipou-mcp --init.`);
|
|
51
|
+
}
|
|
52
|
+
let keyPermissions;
|
|
53
|
+
if (inlinePrivateKey) {
|
|
54
|
+
keyPermissions = warning("Inline private-key permissions cannot be checked; use the key file created by aipou-mcp --init.");
|
|
55
|
+
}
|
|
56
|
+
else if (!keyFile) {
|
|
57
|
+
keyPermissions = fail("No dedicated-wallet key file is configured.");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
const keyStat = await stat(keyFile);
|
|
62
|
+
if (process.platform === "win32") {
|
|
63
|
+
keyPermissions = { status: "not_checked", detail: "Windows ACLs are not inspected by this read-only doctor. This is not a detected permission failure; init reports whether it successfully applied access restrictions." };
|
|
64
|
+
}
|
|
65
|
+
else if ((keyStat.mode & 0o077) === 0) {
|
|
66
|
+
keyPermissions = pass("Key file has no group or other permission bits.");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
keyPermissions = fail("Key file is accessible by group or other users; restrict it to mode 0600.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
keyPermissions = fail(`Key file permissions could not be inspected (${keyFile}).`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const checks = {
|
|
77
|
+
node: Number.isInteger(nodeMajor) && nodeMajor >= 20
|
|
78
|
+
? pass(`Node.js ${process.versions.node} satisfies the required major version (20+).`)
|
|
79
|
+
: fail(`Node.js ${process.versions.node} is unsupported; install Node.js 20 or newer.`),
|
|
80
|
+
dataDirectory,
|
|
81
|
+
identity,
|
|
82
|
+
keyPermissions,
|
|
83
|
+
tokenAddress: isAddress(tokenAddress)
|
|
84
|
+
? pass(`AIPOU token address is valid (${tokenAddress}).`)
|
|
85
|
+
: fail("AIPOU_CONTRACT_ADDRESS is not a valid EVM address."),
|
|
86
|
+
claimsAddress: isAddress(claimsAddress)
|
|
87
|
+
? pass(`AIPOU claims address is valid (${claimsAddress}).`)
|
|
88
|
+
: fail("AIPOU_CLAIMS_ADDRESS is not a valid EVM address."),
|
|
89
|
+
chain: Number.isSafeInteger(chainId) && chainId > 0
|
|
90
|
+
? (chainId === 8453
|
|
91
|
+
? pass("Chain ID is Base Mainnet (8453).")
|
|
92
|
+
: warning(`Chain ID ${chainId} is configured instead of Base Mainnet (8453).`))
|
|
93
|
+
: fail("AIPOU_CHAIN_ID must be a positive integer.")
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
mode: "installation_doctor",
|
|
97
|
+
ready: Object.values(checks).every((check) => check.status !== "fail"),
|
|
98
|
+
version: await getPackageVersion(),
|
|
99
|
+
wallet,
|
|
100
|
+
dataDir,
|
|
101
|
+
keyFile,
|
|
102
|
+
keySource,
|
|
103
|
+
checks,
|
|
104
|
+
safety: {
|
|
105
|
+
privateKeyPrinted: false,
|
|
106
|
+
filesWritten: false,
|
|
107
|
+
networkRequestsMade: false,
|
|
108
|
+
fundsMoved: false,
|
|
109
|
+
claimSubmitted: false
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { access, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { Wallet } from "ethers";
|
|
10
|
+
import { runInstallationDoctor } from "./doctor.js";
|
|
11
|
+
import { initializePersistentIdentity } from "./init.js";
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
test("installation doctor validates init output without exposing or changing the key", async () => {
|
|
14
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-doctor-test-"));
|
|
15
|
+
const previousInlineKey = process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
16
|
+
const previousKeyFile = process.env.AIPOU_AGENT_KEY_FILE;
|
|
17
|
+
try {
|
|
18
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
19
|
+
delete process.env.AIPOU_AGENT_KEY_FILE;
|
|
20
|
+
const initialized = await initializePersistentIdentity(dataDir);
|
|
21
|
+
const privateKeyBefore = await readFile(initialized.keyFile, "utf8");
|
|
22
|
+
const result = await runInstallationDoctor(dataDir);
|
|
23
|
+
const privateKeyAfter = await readFile(initialized.keyFile, "utf8");
|
|
24
|
+
assert.equal(result.ready, true);
|
|
25
|
+
assert.equal(result.wallet, initialized.wallet);
|
|
26
|
+
assert.equal(result.keySource, "key_file");
|
|
27
|
+
if (process.platform === "win32")
|
|
28
|
+
assert.equal(result.checks.keyPermissions.status, "not_checked");
|
|
29
|
+
assert.equal(result.version.length > 0, true);
|
|
30
|
+
assert.equal(Object.values(result.checks).every((check) => check.status !== "fail"), true);
|
|
31
|
+
assert.equal(privateKeyAfter, privateKeyBefore);
|
|
32
|
+
assert.equal(JSON.stringify(result).includes(privateKeyBefore.trim()), false);
|
|
33
|
+
assert.deepEqual(result.safety, {
|
|
34
|
+
privateKeyPrinted: false,
|
|
35
|
+
filesWritten: false,
|
|
36
|
+
networkRequestsMade: false,
|
|
37
|
+
fundsMoved: false,
|
|
38
|
+
claimSubmitted: false
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (previousInlineKey === undefined)
|
|
43
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
44
|
+
else
|
|
45
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = previousInlineKey;
|
|
46
|
+
if (previousKeyFile === undefined)
|
|
47
|
+
delete process.env.AIPOU_AGENT_KEY_FILE;
|
|
48
|
+
else
|
|
49
|
+
process.env.AIPOU_AGENT_KEY_FILE = previousKeyFile;
|
|
50
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
test("installation doctor fails closed without creating a missing directory", async () => {
|
|
54
|
+
const parent = await mkdtemp(path.join(os.tmpdir(), "aipou-doctor-missing-test-"));
|
|
55
|
+
const dataDir = path.join(parent, "missing");
|
|
56
|
+
const previousInlineKey = process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
57
|
+
const previousKeyFile = process.env.AIPOU_AGENT_KEY_FILE;
|
|
58
|
+
try {
|
|
59
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
60
|
+
delete process.env.AIPOU_AGENT_KEY_FILE;
|
|
61
|
+
const result = await runInstallationDoctor(dataDir);
|
|
62
|
+
assert.equal(result.ready, false);
|
|
63
|
+
assert.equal(result.wallet, null);
|
|
64
|
+
assert.equal(result.keySource, "missing");
|
|
65
|
+
assert.equal(result.checks.dataDirectory.status, "fail");
|
|
66
|
+
assert.equal(result.checks.identity.status, "fail");
|
|
67
|
+
await assert.rejects(access(dataDir));
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (previousInlineKey === undefined)
|
|
71
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
72
|
+
else
|
|
73
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = previousInlineKey;
|
|
74
|
+
if (previousKeyFile === undefined)
|
|
75
|
+
delete process.env.AIPOU_AGENT_KEY_FILE;
|
|
76
|
+
else
|
|
77
|
+
process.env.AIPOU_AGENT_KEY_FILE = previousKeyFile;
|
|
78
|
+
await rm(parent, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
test("installation doctor supports the legacy inline key without exposing it", async () => {
|
|
82
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-doctor-inline-test-"));
|
|
83
|
+
const previousInlineKey = process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
84
|
+
const previousKeyFile = process.env.AIPOU_AGENT_KEY_FILE;
|
|
85
|
+
const previousDataDir = process.env.AIPOU_DATA_DIR;
|
|
86
|
+
const wallet = Wallet.createRandom();
|
|
87
|
+
try {
|
|
88
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = wallet.privateKey;
|
|
89
|
+
process.env.AIPOU_DATA_DIR = dataDir;
|
|
90
|
+
delete process.env.AIPOU_AGENT_KEY_FILE;
|
|
91
|
+
const result = await runInstallationDoctor();
|
|
92
|
+
assert.equal(result.ready, true);
|
|
93
|
+
assert.equal(result.wallet, wallet.address);
|
|
94
|
+
assert.equal(result.keySource, "inline_private_key");
|
|
95
|
+
assert.equal(result.keyFile, null);
|
|
96
|
+
assert.equal(result.checks.identity.status, "warning");
|
|
97
|
+
assert.equal(result.checks.keyPermissions.status, "warning");
|
|
98
|
+
assert.equal(JSON.stringify(result).includes(wallet.privateKey), false);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (previousInlineKey === undefined)
|
|
102
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
103
|
+
else
|
|
104
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = previousInlineKey;
|
|
105
|
+
if (previousKeyFile === undefined)
|
|
106
|
+
delete process.env.AIPOU_AGENT_KEY_FILE;
|
|
107
|
+
else
|
|
108
|
+
process.env.AIPOU_AGENT_KEY_FILE = previousKeyFile;
|
|
109
|
+
if (previousDataDir === undefined)
|
|
110
|
+
delete process.env.AIPOU_DATA_DIR;
|
|
111
|
+
else
|
|
112
|
+
process.env.AIPOU_DATA_DIR = previousDataDir;
|
|
113
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
test("installation doctor rejects invalid addresses and chain IDs without throwing", async () => {
|
|
117
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-doctor-config-test-"));
|
|
118
|
+
const previousContract = process.env.AIPOU_CONTRACT_ADDRESS;
|
|
119
|
+
const previousChainId = process.env.AIPOU_CHAIN_ID;
|
|
120
|
+
try {
|
|
121
|
+
await initializePersistentIdentity(dataDir);
|
|
122
|
+
process.env.AIPOU_CONTRACT_ADDRESS = "not-an-address";
|
|
123
|
+
process.env.AIPOU_CHAIN_ID = "not-a-chain";
|
|
124
|
+
const result = await runInstallationDoctor(dataDir);
|
|
125
|
+
assert.equal(result.ready, false);
|
|
126
|
+
assert.equal(result.checks.tokenAddress.status, "fail");
|
|
127
|
+
assert.equal(result.checks.chain.status, "fail");
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
if (previousContract === undefined)
|
|
131
|
+
delete process.env.AIPOU_CONTRACT_ADDRESS;
|
|
132
|
+
else
|
|
133
|
+
process.env.AIPOU_CONTRACT_ADDRESS = previousContract;
|
|
134
|
+
if (previousChainId === undefined)
|
|
135
|
+
delete process.env.AIPOU_CHAIN_ID;
|
|
136
|
+
else
|
|
137
|
+
process.env.AIPOU_CHAIN_ID = previousChainId;
|
|
138
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
test("CLI doctor reports a ready persistent identity without printing its secret", async () => {
|
|
142
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-doctor-cli-test-"));
|
|
143
|
+
const indexPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
144
|
+
try {
|
|
145
|
+
const initialized = await initializePersistentIdentity(dataDir);
|
|
146
|
+
const privateKey = (await readFile(initialized.keyFile, "utf8")).trim();
|
|
147
|
+
const inheritedWallet = Wallet.createRandom();
|
|
148
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, [indexPath, "--doctor", "--data-dir", dataDir], {
|
|
149
|
+
env: {
|
|
150
|
+
...process.env,
|
|
151
|
+
AIPOU_AGENT_PRIVATE_KEY: inheritedWallet.privateKey,
|
|
152
|
+
AIPOU_AGENT_KEY_FILE: path.join(dataDir, "wrong-key-file")
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const result = JSON.parse(stdout);
|
|
156
|
+
assert.equal(result.ready, true);
|
|
157
|
+
assert.equal(result.wallet, initialized.wallet);
|
|
158
|
+
assert.equal(stdout.includes(privateKey), false);
|
|
159
|
+
assert.equal(stderr, "");
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
});
|