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/dist/store.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir
|
|
1
|
+
import { mkdir, readFile, readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { withFileLock } from "./lock.js";
|
|
4
|
+
import { isTaskSessionExpired } from "./identity.js";
|
|
5
|
+
import { canonicalJson } from "./canonical.js";
|
|
6
|
+
import { writePrivateJson } from "./private-json.js";
|
|
4
7
|
const emptyState = () => ({ sessions: {}, receipts: [], completedEvidence: {}, batches: [] });
|
|
5
8
|
let mutationQueue = Promise.resolve();
|
|
6
9
|
function dataDir() {
|
|
@@ -26,9 +29,7 @@ async function readState() {
|
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
async function writeState(state) {
|
|
29
|
-
|
|
30
|
-
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
31
|
-
await rename(temporary, statePath());
|
|
32
|
+
await writePrivateJson(statePath(), state);
|
|
32
33
|
}
|
|
33
34
|
// The in-process queue orders local callers; the file lock orders the multiple
|
|
34
35
|
// MCP server processes (Claude Code, OpenClaw, Codex) that share one data dir.
|
|
@@ -63,7 +64,7 @@ export async function saveCompletedReceipt(evidenceKey, receipt) {
|
|
|
63
64
|
if (!session)
|
|
64
65
|
throw new Error("Unknown task nonce");
|
|
65
66
|
if (session.completedAt)
|
|
66
|
-
throw new Error("Task nonce has already been completed");
|
|
67
|
+
throw new Error("Task nonce has already been completed; recover its stored receipt with export_ai_receipts and match the nonce.");
|
|
67
68
|
if (state.completedEvidence[evidenceKey]) {
|
|
68
69
|
throw new Error(`Duplicate task/output evidence already recorded as ${state.completedEvidence[evidenceKey]}`);
|
|
69
70
|
}
|
|
@@ -95,7 +96,13 @@ async function readArchivedReceipts() {
|
|
|
95
96
|
return receipts;
|
|
96
97
|
}
|
|
97
98
|
export async function exportStoredReceipts(wallet) {
|
|
98
|
-
const
|
|
99
|
+
const unique = new Map();
|
|
100
|
+
for (const receipt of [...(await readState()).receipts, ...(await readArchivedReceipts())]) {
|
|
101
|
+
const prior = unique.get(receipt.receiptId);
|
|
102
|
+
if (!prior?.claimTransaction)
|
|
103
|
+
unique.set(receipt.receiptId, receipt);
|
|
104
|
+
}
|
|
105
|
+
const receipts = [...unique.values()];
|
|
99
106
|
if (!wallet)
|
|
100
107
|
return receipts;
|
|
101
108
|
return receipts.filter((receipt) => receipt.wallet.toLowerCase() === wallet.toLowerCase());
|
|
@@ -110,15 +117,23 @@ export async function protocolStateCounts() {
|
|
|
110
117
|
sessions: sessions.length,
|
|
111
118
|
completedSessions: sessions.filter((session) => session.completedAt).length,
|
|
112
119
|
incompleteSessions: sessions.filter((session) => !session.completedAt).length,
|
|
120
|
+
activeSessions: sessions.filter((session) => !session.completedAt && !isTaskSessionExpired(session)).length,
|
|
121
|
+
expiredSessions: sessions.filter((session) => !session.completedAt && isTaskSessionExpired(session)).length,
|
|
113
122
|
batches: state.batches.length
|
|
114
123
|
};
|
|
115
124
|
}
|
|
116
125
|
export async function settledReceiptsSince(sinceMs) {
|
|
117
|
-
const receipts =
|
|
126
|
+
const receipts = await exportStoredReceipts();
|
|
118
127
|
return receipts.filter((receipt) => receipt.claimTransaction && Date.parse(receipt.recordedAt) >= sinceMs);
|
|
119
128
|
}
|
|
120
129
|
export async function markBatchSettled(batch) {
|
|
121
130
|
await mutate(async (state) => {
|
|
131
|
+
const existing = state.batches.find(item => item.root === batch.root);
|
|
132
|
+
if (existing) {
|
|
133
|
+
if (existing.claimTransaction !== batch.claimTransaction)
|
|
134
|
+
throw new Error("Settlement archive transaction mismatch");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
122
137
|
const included = new Set(batch.receiptIds);
|
|
123
138
|
const settled = [];
|
|
124
139
|
for (const receipt of state.receipts) {
|
|
@@ -133,8 +148,17 @@ export async function markBatchSettled(batch) {
|
|
|
133
148
|
// Settled receipts move to per-batch archive files so state.json stays
|
|
134
149
|
// small; their ids stay in state to keep the replay check O(state).
|
|
135
150
|
await mkdir(archiveDir(), { recursive: true });
|
|
136
|
-
const archivePath = path.join(archiveDir(), `batch-${batch.root.slice(2
|
|
137
|
-
|
|
151
|
+
const archivePath = path.join(archiveDir(), `batch-${batch.root.slice(2)}.json`);
|
|
152
|
+
try {
|
|
153
|
+
const prior = JSON.parse(await readFile(archivePath, "utf8"));
|
|
154
|
+
if (canonicalJson(prior) !== canonicalJson(settled))
|
|
155
|
+
throw new Error("Settlement archive does not match saved batch");
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.code !== "ENOENT")
|
|
159
|
+
throw error;
|
|
160
|
+
await writePrivateJson(archivePath, settled);
|
|
161
|
+
}
|
|
138
162
|
state.receipts = state.receipts.filter((receipt) => !included.has(receipt.receiptId));
|
|
139
163
|
state.settledReceiptIds = [...(state.settledReceiptIds ?? []), ...batch.receiptIds];
|
|
140
164
|
state.batches.push(batch);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const TECHNOCORE_TRANSPORT_SCHEME = "technocore-room-receipt-v1";
|
|
2
|
+
export declare const EXTERNAL_EVIDENCE_LINK_SCHEME = "external-evidence-link-v1";
|
|
3
|
+
export type TechnocoreWorkLinkInput = {
|
|
4
|
+
workReceiptId: string;
|
|
5
|
+
transportArtifactId: string;
|
|
6
|
+
transportArtifactDigest: string;
|
|
7
|
+
issuedAt: string;
|
|
8
|
+
};
|
|
9
|
+
export type TechnocoreWorkLink = {
|
|
10
|
+
scheme: typeof EXTERNAL_EVIDENCE_LINK_SCHEME;
|
|
11
|
+
/** Stable logical identity that excludes issuedAt for retry-safe deduplication. */
|
|
12
|
+
linkId: string;
|
|
13
|
+
relation: "supports";
|
|
14
|
+
source: {
|
|
15
|
+
kind: "receipt";
|
|
16
|
+
scheme: "aipou-receipt-v1";
|
|
17
|
+
id: string;
|
|
18
|
+
digest: string;
|
|
19
|
+
};
|
|
20
|
+
target: {
|
|
21
|
+
kind: "transport-record";
|
|
22
|
+
scheme: typeof TECHNOCORE_TRANSPORT_SCHEME;
|
|
23
|
+
id: string;
|
|
24
|
+
digest: string;
|
|
25
|
+
};
|
|
26
|
+
privacy: "digest_only";
|
|
27
|
+
issuedAt: string;
|
|
28
|
+
linkDigest: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Creates a digest-only correlation artifact. It never calls Technocore or
|
|
32
|
+
* upgrades a supplied snapshot into verified signed transport evidence.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createTechnocoreWorkLink(input: TechnocoreWorkLinkInput): TechnocoreWorkLink;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { canonicalJson, sha256Hex } from "./canonical.js";
|
|
2
|
+
export const TECHNOCORE_TRANSPORT_SCHEME = "technocore-room-receipt-v1";
|
|
3
|
+
export const EXTERNAL_EVIDENCE_LINK_SCHEME = "external-evidence-link-v1";
|
|
4
|
+
const receiptIdPattern = /^0x[0-9a-f]{64}$/;
|
|
5
|
+
const sha256Pattern = /^sha256:[0-9a-f]{64}$/;
|
|
6
|
+
function assertValidInput(input) {
|
|
7
|
+
if (!receiptIdPattern.test(input.workReceiptId)) {
|
|
8
|
+
throw new Error("workReceiptId must be a lowercase 0x-prefixed SHA-256 identifier");
|
|
9
|
+
}
|
|
10
|
+
if (!sha256Pattern.test(input.transportArtifactDigest)) {
|
|
11
|
+
throw new Error("transportArtifactDigest must be a lowercase sha256 digest");
|
|
12
|
+
}
|
|
13
|
+
if (input.transportArtifactId.length < 1 || input.transportArtifactId.length > 256) {
|
|
14
|
+
throw new Error("transportArtifactId must be an opaque reference between 1 and 256 characters");
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isFinite(Date.parse(input.issuedAt))) {
|
|
17
|
+
throw new Error("issuedAt must be an ISO-8601 timestamp");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a digest-only correlation artifact. It never calls Technocore or
|
|
22
|
+
* upgrades a supplied snapshot into verified signed transport evidence.
|
|
23
|
+
*/
|
|
24
|
+
export function createTechnocoreWorkLink(input) {
|
|
25
|
+
assertValidInput(input);
|
|
26
|
+
const identity = {
|
|
27
|
+
scheme: EXTERNAL_EVIDENCE_LINK_SCHEME,
|
|
28
|
+
relation: "supports",
|
|
29
|
+
source: {
|
|
30
|
+
kind: "receipt",
|
|
31
|
+
scheme: "aipou-receipt-v1",
|
|
32
|
+
id: input.workReceiptId,
|
|
33
|
+
digest: `sha256:${input.workReceiptId.slice(2)}`
|
|
34
|
+
},
|
|
35
|
+
target: {
|
|
36
|
+
kind: "transport-record",
|
|
37
|
+
scheme: TECHNOCORE_TRANSPORT_SCHEME,
|
|
38
|
+
id: input.transportArtifactId,
|
|
39
|
+
digest: input.transportArtifactDigest
|
|
40
|
+
},
|
|
41
|
+
privacy: "digest_only"
|
|
42
|
+
};
|
|
43
|
+
const payload = { ...identity, issuedAt: input.issuedAt };
|
|
44
|
+
return {
|
|
45
|
+
...payload,
|
|
46
|
+
linkId: `sha256:${sha256Hex(canonicalJson(identity)).slice(2)}`,
|
|
47
|
+
linkDigest: `sha256:${sha256Hex(canonicalJson(payload)).slice(2)}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createTechnocoreWorkLink } from "./technocore-link.js";
|
|
4
|
+
const input = {
|
|
5
|
+
workReceiptId: `0x${"ab".repeat(32)}`,
|
|
6
|
+
transportArtifactId: "technocore:lobby:receipt-synthetic-001",
|
|
7
|
+
transportArtifactDigest: `sha256:${"cd".repeat(32)}`,
|
|
8
|
+
issuedAt: "2026-08-26T14:30:00.000Z"
|
|
9
|
+
};
|
|
10
|
+
test("creates a digest-only Technocore to AIPOU work link without transport authority", () => {
|
|
11
|
+
const link = createTechnocoreWorkLink(input);
|
|
12
|
+
assert.equal(link.scheme, "external-evidence-link-v1");
|
|
13
|
+
assert.equal(link.relation, "supports");
|
|
14
|
+
assert.equal(link.source.id, input.workReceiptId);
|
|
15
|
+
assert.equal(link.target.scheme, "technocore-room-receipt-v1");
|
|
16
|
+
assert.equal(link.target.id, input.transportArtifactId);
|
|
17
|
+
assert.equal(link.target.digest, input.transportArtifactDigest);
|
|
18
|
+
assert.match(link.linkId, /^sha256:[0-9a-f]{64}$/);
|
|
19
|
+
assert.match(link.linkDigest, /^sha256:[0-9a-f]{64}$/);
|
|
20
|
+
assert.equal("did" in link, false);
|
|
21
|
+
assert.equal("signature" in link, false);
|
|
22
|
+
assert.equal("deliveryStatus" in link, false);
|
|
23
|
+
assert.equal("claimStatus" in link, false);
|
|
24
|
+
});
|
|
25
|
+
test("is deterministic and changes when the transport artifact changes", () => {
|
|
26
|
+
assert.equal(createTechnocoreWorkLink(input).linkDigest, createTechnocoreWorkLink(input).linkDigest);
|
|
27
|
+
assert.equal(createTechnocoreWorkLink(input).linkId, createTechnocoreWorkLink({ ...input, issuedAt: "2026-08-26T15:30:00.000Z" }).linkId);
|
|
28
|
+
assert.notEqual(createTechnocoreWorkLink(input).linkDigest, createTechnocoreWorkLink({ ...input, issuedAt: "2026-08-26T15:30:00.000Z" }).linkDigest);
|
|
29
|
+
assert.notEqual(createTechnocoreWorkLink(input).linkId, createTechnocoreWorkLink({ ...input, transportArtifactDigest: `sha256:${"ef".repeat(32)}` }).linkId);
|
|
30
|
+
});
|
|
31
|
+
test("rejects malformed receipt, digest, opaque reference, and timestamp inputs", () => {
|
|
32
|
+
assert.throws(() => createTechnocoreWorkLink({ ...input, workReceiptId: "0xabc" }));
|
|
33
|
+
assert.throws(() => createTechnocoreWorkLink({ ...input, transportArtifactDigest: "sha256:ABC" }));
|
|
34
|
+
assert.throws(() => createTechnocoreWorkLink({ ...input, transportArtifactId: "" }));
|
|
35
|
+
assert.throws(() => createTechnocoreWorkLink({ ...input, issuedAt: "not-a-date" }));
|
|
36
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
test("tool metadata explains parameters, side effects, and safe alternatives", async () => {
|
|
7
|
+
const transport = new StdioClientTransport({
|
|
8
|
+
command: process.execPath,
|
|
9
|
+
args: [fileURLToPath(new URL("./index.js", import.meta.url))],
|
|
10
|
+
stderr: "pipe"
|
|
11
|
+
});
|
|
12
|
+
const client = new Client({ name: "aipou-tool-metadata-test", version: "1.0.0" });
|
|
13
|
+
await client.connect(transport);
|
|
14
|
+
try {
|
|
15
|
+
const { tools } = await client.listTools();
|
|
16
|
+
const complete = tools.find((tool) => tool.name === "complete_ai_task");
|
|
17
|
+
const estimate = tools.find((tool) => tool.name === "estimate_ai_reward");
|
|
18
|
+
const status = tools.find((tool) => tool.name === "get_aipou_status");
|
|
19
|
+
const begin = tools.find((tool) => tool.name === "begin_ai_task");
|
|
20
|
+
const paybox = tools.find((tool) => tool.name === "create_paybox_work_link");
|
|
21
|
+
const technocore = tools.find((tool) => tool.name === "create_technocore_work_link");
|
|
22
|
+
const verifyReceipt = tools.find((tool) => tool.name === "verify_aipou_receipt");
|
|
23
|
+
const settleAll = tools.find((tool) => tool.name === "settle_all_ai_rewards");
|
|
24
|
+
assert.ok(complete);
|
|
25
|
+
assert.match(complete.description ?? "", /once after begin_ai_task/i);
|
|
26
|
+
assert.match(complete.description ?? "", /writes the local receipt store/i);
|
|
27
|
+
assert.match(complete.description ?? "", /does not .*submit a blockchain transaction/i);
|
|
28
|
+
assert.match(complete.description ?? "", /workReceiptId/i);
|
|
29
|
+
assert.match(complete.description ?? "", /get_aipou_status/i);
|
|
30
|
+
assert.deepEqual(complete.annotations, {
|
|
31
|
+
readOnlyHint: false,
|
|
32
|
+
destructiveHint: false,
|
|
33
|
+
idempotentHint: false,
|
|
34
|
+
openWorldHint: false
|
|
35
|
+
});
|
|
36
|
+
const completeProperties = complete.inputSchema.properties;
|
|
37
|
+
for (const field of ["nonce", "inputTokens", "outputTokens", "durationSeconds", "outputHash"]) {
|
|
38
|
+
assert.ok(completeProperties[field]?.description, `${field} must have a description`);
|
|
39
|
+
}
|
|
40
|
+
assert.ok(completeProperties.providerEvidence?.description);
|
|
41
|
+
assert.ok(completeProperties.providerEvidence?.properties?.keyId?.description);
|
|
42
|
+
assert.ok(completeProperties.providerEvidence?.properties?.signature?.description);
|
|
43
|
+
assert.ok(estimate);
|
|
44
|
+
assert.match(estimate.description ?? "", /informational/i);
|
|
45
|
+
assert.match(estimate.description ?? "", /inputTokens, outputTokens, and durationSeconds/i);
|
|
46
|
+
assert.equal(estimate.annotations?.readOnlyHint, true);
|
|
47
|
+
assert.equal(estimate.annotations?.idempotentHint, true);
|
|
48
|
+
assert.ok(status);
|
|
49
|
+
assert.match(status.description ?? "", /read-only JSON summary/i);
|
|
50
|
+
assert.match(status.description ?? "", /never signs or submits a transaction/i);
|
|
51
|
+
assert.match(status.description ?? "", /explicit claim request/i);
|
|
52
|
+
assert.equal(status.annotations?.readOnlyHint, true);
|
|
53
|
+
assert.equal(status.annotations?.openWorldHint, true);
|
|
54
|
+
assert.ok(begin);
|
|
55
|
+
assert.match(begin.description ?? "", /writes a pending local task session/i);
|
|
56
|
+
assert.match(begin.description ?? "", /does not publish, mint, or submit an on-chain transaction/i);
|
|
57
|
+
assert.equal(begin.annotations?.readOnlyHint, false);
|
|
58
|
+
assert.equal(begin.annotations?.idempotentHint, false);
|
|
59
|
+
const beginProperties = begin.inputSchema.properties;
|
|
60
|
+
for (const field of ["provider", "model", "taskHash", "client"]) {
|
|
61
|
+
assert.ok(beginProperties[field]?.description, `${field} must have a description`);
|
|
62
|
+
}
|
|
63
|
+
for (const tool of [paybox, technocore]) {
|
|
64
|
+
assert.ok(tool);
|
|
65
|
+
assert.match(tool.description ?? "", /pure local calculation/i);
|
|
66
|
+
assert.match(tool.description ?? "", /does not persist/i);
|
|
67
|
+
assert.equal(tool.annotations?.readOnlyHint, true);
|
|
68
|
+
assert.equal(tool.annotations?.idempotentHint, true);
|
|
69
|
+
}
|
|
70
|
+
assert.ok(verifyReceipt);
|
|
71
|
+
assert.match(verifyReceipt.description ?? "", /self-contained integrity/i);
|
|
72
|
+
assert.match(verifyReceipt.description ?? "", /does not .*prove delivery/i);
|
|
73
|
+
assert.equal(verifyReceipt.annotations?.readOnlyHint, true);
|
|
74
|
+
assert.equal(verifyReceipt.annotations?.idempotentHint, true);
|
|
75
|
+
assert.ok(settleAll);
|
|
76
|
+
assert.match(settleAll.description ?? "", /explicit broad request/i);
|
|
77
|
+
assert.equal(settleAll.annotations?.readOnlyHint, false);
|
|
78
|
+
assert.equal(settleAll.annotations?.openWorldHint, true);
|
|
79
|
+
const settleAllProperties = settleAll.inputSchema.properties;
|
|
80
|
+
assert.ok(settleAllProperties.batchSize?.description);
|
|
81
|
+
assert.ok(settleAllProperties.maxBatches?.description);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await transport.close();
|
|
85
|
+
}
|
|
86
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type TrustTier = "client_signed" | "provider_signed";
|
|
2
|
+
export type TokenUsageMeasurement = "measured" | "unmeasured" | "unspecified";
|
|
2
3
|
export interface TaskSession {
|
|
3
4
|
nonce: string;
|
|
4
5
|
wallet: string;
|
|
@@ -7,6 +8,8 @@ export interface TaskSession {
|
|
|
7
8
|
taskHash: string;
|
|
8
9
|
client: string;
|
|
9
10
|
issuedAt: number;
|
|
11
|
+
/** Unix seconds. A pending session is not authority to complete after this point. */
|
|
12
|
+
expiresAt?: number;
|
|
10
13
|
chainId: number;
|
|
11
14
|
verifyingContract: string;
|
|
12
15
|
walletAuthorization: string;
|
|
@@ -17,6 +20,7 @@ export interface ProviderEvidence {
|
|
|
17
20
|
signature: string;
|
|
18
21
|
}
|
|
19
22
|
export interface CompleteTaskInput {
|
|
23
|
+
tokenUsageMeasurement?: TokenUsageMeasurement;
|
|
20
24
|
nonce: string;
|
|
21
25
|
inputTokens: number;
|
|
22
26
|
outputTokens: number;
|
|
@@ -25,6 +29,8 @@ export interface CompleteTaskInput {
|
|
|
25
29
|
providerEvidence?: ProviderEvidence;
|
|
26
30
|
}
|
|
27
31
|
export interface UsageReceipt {
|
|
32
|
+
/** Absent on legacy signed receipts; absence never implies measured usage. */
|
|
33
|
+
tokenUsageMeasurement?: TokenUsageMeasurement;
|
|
28
34
|
receiptId: string;
|
|
29
35
|
nonce: string;
|
|
30
36
|
wallet: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Wallet } from "ethers";
|
|
7
|
+
import { canonicalJson, sha256Hex } from "./canonical.js";
|
|
8
|
+
import { signCollectorPayload } from "./collector.js";
|
|
9
|
+
import { localFeedbackSummary } from "./feedback.js";
|
|
10
|
+
import { beginTask, completeTask, exportReceipts } from "./receipts.js";
|
|
11
|
+
import { verifyAipouReceipt } from "./receipt-verifier.js";
|
|
12
|
+
import { markBatchSettled, unsettledReceipts } from "./store.js";
|
|
13
|
+
test("unmeasured zero differs from measured zero and recovered receipts are not duplicated", async () => {
|
|
14
|
+
const dir = await mkdtemp(path.join(tmpdir(), "aipou-usage-"));
|
|
15
|
+
process.env.AIPOU_DATA_DIR = dir;
|
|
16
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = Wallet.createRandom().privateKey;
|
|
17
|
+
try {
|
|
18
|
+
const create = (s) => beginTask({
|
|
19
|
+
provider: "local",
|
|
20
|
+
model: "test",
|
|
21
|
+
client: "test",
|
|
22
|
+
taskHash: sha256Hex(s),
|
|
23
|
+
chainId: 8453,
|
|
24
|
+
verifyingContract: Wallet.createRandom().address,
|
|
25
|
+
});
|
|
26
|
+
const one = await create("one");
|
|
27
|
+
await assert.rejects(completeTask({
|
|
28
|
+
nonce: one.nonce,
|
|
29
|
+
inputTokens: 1,
|
|
30
|
+
outputTokens: 0,
|
|
31
|
+
durationSeconds: 1,
|
|
32
|
+
outputHash: sha256Hex("out"),
|
|
33
|
+
tokenUsageMeasurement: "unmeasured",
|
|
34
|
+
}), /zero counts/);
|
|
35
|
+
const receipt = await completeTask({
|
|
36
|
+
nonce: one.nonce,
|
|
37
|
+
inputTokens: 0,
|
|
38
|
+
outputTokens: 0,
|
|
39
|
+
durationSeconds: 1,
|
|
40
|
+
outputHash: sha256Hex("out"),
|
|
41
|
+
tokenUsageMeasurement: "unmeasured",
|
|
42
|
+
});
|
|
43
|
+
assert.equal(receipt.tokenUsageMeasurement, "unmeasured");
|
|
44
|
+
assert.equal(verifyAipouReceipt(receipt).valid, true);
|
|
45
|
+
const { tokenUsageMeasurement: _measurement, receiptId: _id, collectorSignature: _signature, ...legacyUnsigned } = receipt;
|
|
46
|
+
const legacyId = sha256Hex(canonicalJson(legacyUnsigned));
|
|
47
|
+
const legacy = {
|
|
48
|
+
receiptId: legacyId,
|
|
49
|
+
...legacyUnsigned,
|
|
50
|
+
collectorSignature: await signCollectorPayload({
|
|
51
|
+
receiptId: legacyId,
|
|
52
|
+
...legacyUnsigned,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
assert.equal(verifyAipouReceipt(legacy).valid, true, "legacy receipts remain valid without the new optional field");
|
|
56
|
+
await assert.rejects(completeTask({
|
|
57
|
+
nonce: one.nonce,
|
|
58
|
+
inputTokens: 0,
|
|
59
|
+
outputTokens: 0,
|
|
60
|
+
durationSeconds: 1,
|
|
61
|
+
outputHash: receipt.outputHash,
|
|
62
|
+
}), /export_ai_receipts/);
|
|
63
|
+
assert.equal((await exportReceipts()).find((r) => r.nonce === one.nonce)?.receiptId, receipt.receiptId);
|
|
64
|
+
const two = await create("two");
|
|
65
|
+
const measured = await completeTask({
|
|
66
|
+
nonce: two.nonce,
|
|
67
|
+
inputTokens: 0,
|
|
68
|
+
outputTokens: 0,
|
|
69
|
+
durationSeconds: 1,
|
|
70
|
+
outputHash: sha256Hex("out2"),
|
|
71
|
+
tokenUsageMeasurement: "measured",
|
|
72
|
+
});
|
|
73
|
+
assert.equal(measured.tokenUsageMeasurement, "measured");
|
|
74
|
+
const three = await create("three");
|
|
75
|
+
const unspecified = await completeTask({
|
|
76
|
+
nonce: three.nonce,
|
|
77
|
+
inputTokens: 0,
|
|
78
|
+
outputTokens: 0,
|
|
79
|
+
durationSeconds: 1,
|
|
80
|
+
outputHash: sha256Hex("out3"),
|
|
81
|
+
});
|
|
82
|
+
assert.equal(unspecified.tokenUsageMeasurement, "unspecified");
|
|
83
|
+
const feedback = await localFeedbackSummary();
|
|
84
|
+
assert.equal(feedback.localReceiptCount, 3);
|
|
85
|
+
assert.equal(feedback.measuredUsageReceipts, 1);
|
|
86
|
+
assert.equal(feedback.unmeasuredUsageReceipts, 1);
|
|
87
|
+
assert.equal(feedback.unspecifiedUsageReceipts, 1);
|
|
88
|
+
const text = JSON.stringify(feedback);
|
|
89
|
+
assert.ok(!text.includes(receipt.receiptId));
|
|
90
|
+
assert.ok(!text.includes(receipt.wallet));
|
|
91
|
+
assert.ok(!text.includes(dir));
|
|
92
|
+
// Simulate archive written but state update interrupted; recovery is idempotent.
|
|
93
|
+
const batch = {
|
|
94
|
+
root: sha256Hex("batch"),
|
|
95
|
+
receiptIds: [receipt.receiptId],
|
|
96
|
+
publishTransaction: "0xpublish",
|
|
97
|
+
claimTransaction: "0xclaim",
|
|
98
|
+
settledAt: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
await mkdir(path.join(dir, "settled"), { recursive: true });
|
|
101
|
+
await writeFile(path.join(dir, "settled", `batch-${batch.root.slice(2)}.json`), JSON.stringify([
|
|
102
|
+
{
|
|
103
|
+
...receipt,
|
|
104
|
+
batchRoot: batch.root,
|
|
105
|
+
claimTransaction: batch.claimTransaction,
|
|
106
|
+
},
|
|
107
|
+
]));
|
|
108
|
+
await markBatchSettled(batch);
|
|
109
|
+
await markBatchSettled(batch);
|
|
110
|
+
assert.equal((await unsettledReceipts(100)).length, 2);
|
|
111
|
+
assert.equal((await exportReceipts()).filter((r) => r.receiptId === receipt.receiptId)
|
|
112
|
+
.length, 1);
|
|
113
|
+
assert.equal(JSON.parse(await readFile(path.join(dir, "state.json"), "utf8")).batches
|
|
114
|
+
.length, 1);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await rm(dir, { recursive: true, force: true });
|
|
118
|
+
delete process.env.AIPOU_DATA_DIR;
|
|
119
|
+
delete process.env.AIPOU_AGENT_PRIVATE_KEY;
|
|
120
|
+
}
|
|
121
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aipou-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"mcpName": "io.github.0xddneto/ai-proof-of-us",
|
|
5
5
|
"description": "MCP server for private, signed AI task receipts and optional validated AIPOU claims.",
|
|
6
6
|
"author": "0xddneto",
|
|
@@ -29,14 +29,15 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
31
|
"README.md",
|
|
32
|
+
"NOTICE",
|
|
32
33
|
"server.json"
|
|
33
34
|
],
|
|
34
35
|
"bin": {
|
|
35
36
|
"aipou-mcp": "dist/index.js"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"build": "tsc -p tsconfig.json",
|
|
39
|
-
"test": "npm run build && node --test dist
|
|
39
|
+
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:dotenv --external:dotenv/* --external:ethers --external:merkletreejs --external:zod --external:zod/*",
|
|
40
|
+
"test": "npm run build && node --test dist/*.test.js",
|
|
40
41
|
"pack:check": "npm test && npm pack --dry-run",
|
|
41
42
|
"demo": "npm run build && node dist/index.js --demo",
|
|
42
43
|
"dev": "tsx src/index.ts",
|
|
@@ -50,14 +51,15 @@
|
|
|
50
51
|
"node": ">=20"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
54
54
|
"dotenv": "^16.6.1",
|
|
55
55
|
"ethers": "^6.17.0",
|
|
56
56
|
"merkletreejs": "^0.6.0",
|
|
57
|
-
"zod": "^
|
|
57
|
+
"zod": "^4.6.7"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
60
61
|
"@types/node": "^22.5.4",
|
|
62
|
+
"esbuild": "0.28.1",
|
|
61
63
|
"tsx": "^4.19.0",
|
|
62
64
|
"typescript": "^5.5.4"
|
|
63
65
|
}
|
package/server.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"source": "github",
|
|
10
10
|
"subfolder": "mcp-server"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.6.0",
|
|
13
13
|
"packages": [
|
|
14
14
|
{
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"identifier": "aipou-mcp-server",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.6.0",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
20
20
|
},
|