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/init.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare const AIPOU_TOKEN_ADDRESS = "0x55f0Cc5e51A1284D20337d6cbb18938C8A1ABCbB";
|
|
2
|
+
export declare const AIPOU_CLAIMS_ADDRESS = "0x4ca4C98fB784D20EdC8E2A7F531dAab4c6e53058";
|
|
1
3
|
export interface InitializationResult {
|
|
2
4
|
mode: "persistent_identity_initialized" | "persistent_identity_already_initialized";
|
|
3
5
|
wallet: string;
|
|
@@ -7,8 +9,8 @@ export interface InitializationResult {
|
|
|
7
9
|
mcpConfig: {
|
|
8
10
|
mcpServers: {
|
|
9
11
|
aipou: {
|
|
10
|
-
command:
|
|
11
|
-
args: [
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
12
14
|
env: {
|
|
13
15
|
AIPOU_AGENT_KEY_FILE: string;
|
|
14
16
|
AIPOU_DATA_DIR: string;
|
|
@@ -26,5 +28,9 @@ export interface InitializationResult {
|
|
|
26
28
|
claimSubmitted: false;
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
export declare function installationCommand(moduleUrl?: string): Promise<{
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
}>;
|
|
29
35
|
export declare function initializePersistentIdentity(dataDirInput?: string): Promise<InitializationResult>;
|
|
30
36
|
export declare function describeExistingIdentity(dataDirInput?: string): Promise<InitializationResult>;
|
package/dist/init.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { Wallet } from "ethers";
|
|
5
6
|
import { restrictToCurrentUser } from "./secure-file.js";
|
|
6
|
-
const
|
|
7
|
-
const
|
|
7
|
+
export const AIPOU_TOKEN_ADDRESS = "0x55f0Cc5e51A1284D20337d6cbb18938C8A1ABCbB";
|
|
8
|
+
export const AIPOU_CLAIMS_ADDRESS = "0x4ca4C98fB784D20EdC8E2A7F531dAab4c6e53058";
|
|
8
9
|
function identityPaths(dataDirInput) {
|
|
9
10
|
const dataDir = path.resolve(dataDirInput || path.join(os.homedir(), ".aipou"));
|
|
10
11
|
return { dataDir, keyFile: path.join(dataDir, "agent-wallet.key") };
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
+
export async function installationCommand(moduleUrl = import.meta.url) {
|
|
14
|
+
const directory = path.dirname(fileURLToPath(moduleUrl));
|
|
15
|
+
try {
|
|
16
|
+
const workspace = JSON.parse(await readFile(path.resolve(directory, "../../package.json"), "utf8"));
|
|
17
|
+
if (workspace.name === "ai-proof-of-us" && workspace.workspaces?.includes("mcp-server")) {
|
|
18
|
+
return { command: process.execPath, args: [path.join(directory, "index.js")] };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch { /* An npm installation has no source workspace. */ }
|
|
22
|
+
const pkg = JSON.parse(await readFile(path.resolve(directory, "../package.json"), "utf8"));
|
|
23
|
+
return { command: "npx", args: ["-y", `aipou-mcp-server@${pkg.version}`] };
|
|
24
|
+
}
|
|
25
|
+
async function buildResult(mode, walletAddress, dataDir, keyFile) {
|
|
26
|
+
const launch = await installationCommand();
|
|
13
27
|
return {
|
|
14
28
|
mode,
|
|
15
29
|
wallet: walletAddress,
|
|
@@ -19,13 +33,12 @@ function buildResult(mode, walletAddress, dataDir, keyFile) {
|
|
|
19
33
|
mcpConfig: {
|
|
20
34
|
mcpServers: {
|
|
21
35
|
aipou: {
|
|
22
|
-
|
|
23
|
-
args: ["-y", "aipou-mcp-server"],
|
|
36
|
+
...launch,
|
|
24
37
|
env: {
|
|
25
38
|
AIPOU_AGENT_KEY_FILE: keyFile,
|
|
26
39
|
AIPOU_DATA_DIR: dataDir,
|
|
27
|
-
AIPOU_CONTRACT_ADDRESS:
|
|
28
|
-
AIPOU_CLAIMS_ADDRESS
|
|
40
|
+
AIPOU_CONTRACT_ADDRESS: AIPOU_TOKEN_ADDRESS,
|
|
41
|
+
AIPOU_CLAIMS_ADDRESS
|
|
29
42
|
}
|
|
30
43
|
}
|
|
31
44
|
}
|
package/dist/init.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -9,8 +9,20 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
10
10
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
11
11
|
import { agentWallet } from "./identity.js";
|
|
12
|
-
import { describeExistingIdentity, initializePersistentIdentity } from "./init.js";
|
|
12
|
+
import { describeExistingIdentity, initializePersistentIdentity, installationCommand } from "./init.js";
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
|
+
test("installed package config pins its actual version instead of silently switching latest", async () => {
|
|
15
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "aipou-installed-config-"));
|
|
16
|
+
try {
|
|
17
|
+
await mkdir(path.join(dir, "dist"));
|
|
18
|
+
await writeFile(path.join(dir, "package.json"), JSON.stringify({ version: "0.6.0" }));
|
|
19
|
+
const { pathToFileURL } = await import("node:url");
|
|
20
|
+
assert.deepEqual(await installationCommand(pathToFileURL(path.join(dir, "dist", "index.js")).href), { command: "npx", args: ["-y", "aipou-mcp-server@0.6.0"] });
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await rm(dir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
14
26
|
function parseTextResult(result) {
|
|
15
27
|
const content = result.content;
|
|
16
28
|
const text = content?.find((part) => part.type === "text")?.text;
|
|
@@ -91,9 +103,11 @@ test("CLI init output starts an MCP server with the generated key file", async (
|
|
|
91
103
|
const privateKey = (await readFile(result.keyFile, "utf8")).trim();
|
|
92
104
|
assert.equal(stdout.includes(privateKey), false);
|
|
93
105
|
const config = result.mcpConfig.mcpServers.aipou;
|
|
106
|
+
assert.equal(config.command, process.execPath);
|
|
107
|
+
assert.deepEqual(config.args, [indexPath]);
|
|
94
108
|
transport = new StdioClientTransport({
|
|
95
|
-
command:
|
|
96
|
-
args:
|
|
109
|
+
command: config.command,
|
|
110
|
+
args: config.args,
|
|
97
111
|
stderr: "pipe",
|
|
98
112
|
env: { ...process.env, ...config.env }
|
|
99
113
|
});
|
package/dist/lock.js
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
|
-
import { rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
//
|
|
3
|
-
// belongs to a crashed process and can be reclaimed safely.
|
|
1
|
+
import { readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
// Age alone never makes a live process's lock reclaimable.
|
|
4
3
|
const STALE_LOCK_MS = 15_000;
|
|
5
4
|
const ACQUIRE_TIMEOUT_MS = 10_000;
|
|
5
|
+
async function reclaimDeadLock(lockPath) {
|
|
6
|
+
// Serialize reclaimers so a second observer cannot delete a new owner's lock
|
|
7
|
+
// using a stale PID read. An interrupted reclaimer fails closed for review.
|
|
8
|
+
const gate = `${lockPath}.reclaim`;
|
|
9
|
+
try {
|
|
10
|
+
await writeFile(gate, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === "EEXIST")
|
|
14
|
+
return;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const info = await stat(lockPath);
|
|
19
|
+
if (Date.now() - info.mtimeMs <= STALE_LOCK_MS)
|
|
20
|
+
return;
|
|
21
|
+
const pid = Number((await readFile(lockPath, "utf8")).trim());
|
|
22
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === "ESRCH")
|
|
29
|
+
await rm(lockPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== "ENOENT")
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await rm(gate, { force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
6
40
|
async function acquire(lockPath) {
|
|
7
41
|
const deadline = Date.now() + ACQUIRE_TIMEOUT_MS;
|
|
8
42
|
for (;;) {
|
|
9
43
|
try {
|
|
10
|
-
await writeFile(lockPath, `${process.pid}\n`, {
|
|
44
|
+
await writeFile(lockPath, `${process.pid}\n`, {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
flag: "wx",
|
|
47
|
+
});
|
|
11
48
|
return;
|
|
12
49
|
}
|
|
13
50
|
catch (error) {
|
|
@@ -17,16 +54,17 @@ async function acquire(lockPath) {
|
|
|
17
54
|
try {
|
|
18
55
|
const info = await stat(lockPath);
|
|
19
56
|
if (Date.now() - info.mtimeMs > STALE_LOCK_MS) {
|
|
20
|
-
await
|
|
21
|
-
continue;
|
|
57
|
+
await reclaimDeadLock(lockPath);
|
|
22
58
|
}
|
|
23
59
|
}
|
|
24
|
-
catch {
|
|
60
|
+
catch (error) {
|
|
25
61
|
// The holder released the lock between attempts; retry immediately.
|
|
26
|
-
|
|
62
|
+
if (error.code === "ENOENT")
|
|
63
|
+
continue;
|
|
64
|
+
throw error;
|
|
27
65
|
}
|
|
28
66
|
if (Date.now() > deadline) {
|
|
29
|
-
throw new Error(
|
|
67
|
+
throw new Error("AIPOU operation is busy; retry when the other process finishes. Never remove an active lock.");
|
|
30
68
|
}
|
|
31
69
|
await new Promise((resolve) => setTimeout(resolve, 25 + Math.floor(Math.random() * 75)));
|
|
32
70
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtemp, writeFile, utimes, rm, readFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { once } from "node:events";
|
|
8
|
+
import { withFileLock } from "./lock.js";
|
|
9
|
+
test("a live PID keeps its old lock; a stopped process can be reclaimed", async () => {
|
|
10
|
+
const dir = await mkdtemp(path.join(tmpdir(), "aipou-lock-test-"));
|
|
11
|
+
const file = path.join(dir, "operation.lock");
|
|
12
|
+
try {
|
|
13
|
+
await writeFile(file, String(process.pid));
|
|
14
|
+
await utimes(file, 0, 0);
|
|
15
|
+
await assert.rejects(withFileLock(file, async () => assert.fail("live lock was stolen")), /busy/);
|
|
16
|
+
assert.equal(await readFile(file, "utf8"), String(process.pid));
|
|
17
|
+
const child = spawn(process.execPath, ["-e", "process.exit(0)"]);
|
|
18
|
+
await once(child, "exit");
|
|
19
|
+
await writeFile(file, String(child.pid));
|
|
20
|
+
await utimes(file, 0, 0);
|
|
21
|
+
assert.equal(await withFileLock(file, async () => "recovered"), "recovered");
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await rm(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EvidenceReport } from "./evidence.js";
|
|
2
|
+
export interface ObservedOutcomePilotResult {
|
|
3
|
+
mode: "observed_outcome_pilot";
|
|
4
|
+
storage: "removed_after_pilot";
|
|
5
|
+
workReceiptId: string;
|
|
6
|
+
evidence: EvidenceReport;
|
|
7
|
+
safety: {
|
|
8
|
+
networkRequired: false;
|
|
9
|
+
fundsMoved: false;
|
|
10
|
+
claimSubmitted: false;
|
|
11
|
+
deliveryClaimed: false;
|
|
12
|
+
acceptanceClaimed: false;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Rehearses local receipt creation and independent verification only. */
|
|
16
|
+
export declare function runObservedOutcomePilot(): Promise<ObservedOutcomePilotResult>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Wallet } from "ethers";
|
|
5
|
+
import { canonicalJson, sha256Hex } from "./canonical.js";
|
|
6
|
+
import { beginTask, completeTask } from "./receipts.js";
|
|
7
|
+
import { verifyAipouReceipt } from "./receipt-verifier.js";
|
|
8
|
+
const CLAIMS_ADDRESS = "0x4ca4C98fB784D20EdC8E2A7F531dAab4c6e53058";
|
|
9
|
+
const ENVIRONMENT_KEYS = ["AIPOU_AGENT_PRIVATE_KEY", "AIPOU_DATA_DIR"];
|
|
10
|
+
/** Rehearses local receipt creation and independent verification only. */
|
|
11
|
+
export async function runObservedOutcomePilot() {
|
|
12
|
+
const previous = new Map(ENVIRONMENT_KEYS.map((key) => [key, process.env[key]]));
|
|
13
|
+
const dataDir = await mkdtemp(path.join(os.tmpdir(), "aipou-observed-outcome-pilot-"));
|
|
14
|
+
process.env.AIPOU_DATA_DIR = dataDir;
|
|
15
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = Wallet.createRandom().privateKey;
|
|
16
|
+
try {
|
|
17
|
+
const session = await beginTask({
|
|
18
|
+
provider: "local-pilot",
|
|
19
|
+
model: "observed-outcome-pilot",
|
|
20
|
+
client: "aipou-cli",
|
|
21
|
+
taskHash: sha256Hex(canonicalJson({ kind: "pilot", action: "bounded_local_execution" })),
|
|
22
|
+
chainId: 8453,
|
|
23
|
+
verifyingContract: CLAIMS_ADDRESS
|
|
24
|
+
});
|
|
25
|
+
const receipt = await completeTask({
|
|
26
|
+
nonce: session.nonce,
|
|
27
|
+
inputTokens: 24,
|
|
28
|
+
outputTokens: 16,
|
|
29
|
+
durationSeconds: 1,
|
|
30
|
+
outputHash: sha256Hex(canonicalJson({ artifact: "pilot-output-reference", status: "host-observed" }))
|
|
31
|
+
});
|
|
32
|
+
const verification = verifyAipouReceipt(receipt);
|
|
33
|
+
if (!verification.valid)
|
|
34
|
+
throw new Error(`Pilot receipt verification failed: ${verification.errors.join("; ")}`);
|
|
35
|
+
return {
|
|
36
|
+
mode: "observed_outcome_pilot",
|
|
37
|
+
storage: "removed_after_pilot",
|
|
38
|
+
workReceiptId: receipt.receiptId,
|
|
39
|
+
evidence: verification.evidence,
|
|
40
|
+
safety: {
|
|
41
|
+
networkRequired: false,
|
|
42
|
+
fundsMoved: false,
|
|
43
|
+
claimSubmitted: false,
|
|
44
|
+
deliveryClaimed: false,
|
|
45
|
+
acceptanceClaimed: false
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
51
|
+
for (const key of ENVIRONMENT_KEYS) {
|
|
52
|
+
const value = previous.get(key);
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
delete process.env[key];
|
|
55
|
+
else
|
|
56
|
+
process.env[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { runObservedOutcomePilot } from "./observed-outcome-pilot.js";
|
|
4
|
+
test("observed-outcome pilot does not upgrade local execution into delivery or acceptance", async () => {
|
|
5
|
+
const result = await runObservedOutcomePilot();
|
|
6
|
+
assert.equal(result.mode, "observed_outcome_pilot");
|
|
7
|
+
assert.match(result.workReceiptId, /^0x[0-9a-f]{64}$/);
|
|
8
|
+
assert.deepEqual(result.evidence, {
|
|
9
|
+
receiptIntegrity: "VERIFIED",
|
|
10
|
+
walletAuthorization: "VERIFIED",
|
|
11
|
+
collectorSignature: "VERIFIED",
|
|
12
|
+
hostExecution: "OBSERVED",
|
|
13
|
+
delivery: "UNKNOWN",
|
|
14
|
+
businessAcceptance: "ABSENT",
|
|
15
|
+
settlement: "ABSENT",
|
|
16
|
+
externalEffect: "UNKNOWN"
|
|
17
|
+
});
|
|
18
|
+
assert.deepEqual(result.safety, {
|
|
19
|
+
networkRequired: false,
|
|
20
|
+
fundsMoved: false,
|
|
21
|
+
claimSubmitted: false,
|
|
22
|
+
deliveryClaimed: false,
|
|
23
|
+
acceptanceClaimed: false
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const PAYBOX_OPERATION_SCHEME = "paybox-operation-v1";
|
|
2
|
+
export declare const EXTERNAL_EVIDENCE_LINK_SCHEME = "external-evidence-link-v1";
|
|
3
|
+
export type PayboxWorkLinkInput = {
|
|
4
|
+
workReceiptId: string;
|
|
5
|
+
payboxOperationId: string;
|
|
6
|
+
payboxOperationDigest: string;
|
|
7
|
+
issuedAt: string;
|
|
8
|
+
};
|
|
9
|
+
export type PayboxWorkLink = {
|
|
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: "payment-operation";
|
|
22
|
+
scheme: typeof PAYBOX_OPERATION_SCHEME;
|
|
23
|
+
id: string;
|
|
24
|
+
digest: string;
|
|
25
|
+
};
|
|
26
|
+
privacy: "digest_only";
|
|
27
|
+
issuedAt: string;
|
|
28
|
+
linkDigest: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Creates a correlation artifact only. It neither calls Paybox nor establishes
|
|
32
|
+
* payment, settlement, claim eligibility, or any authority beyond the link.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createPayboxWorkLink(input: PayboxWorkLinkInput): PayboxWorkLink;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { canonicalJson, sha256Hex } from "./canonical.js";
|
|
2
|
+
export const PAYBOX_OPERATION_SCHEME = "paybox-operation-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.payboxOperationDigest)) {
|
|
11
|
+
throw new Error("payboxOperationDigest must be a lowercase sha256 digest");
|
|
12
|
+
}
|
|
13
|
+
if (input.payboxOperationId.length < 1 || input.payboxOperationId.length > 256) {
|
|
14
|
+
throw new Error("payboxOperationId 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 correlation artifact only. It neither calls Paybox nor establishes
|
|
22
|
+
* payment, settlement, claim eligibility, or any authority beyond the link.
|
|
23
|
+
*/
|
|
24
|
+
export function createPayboxWorkLink(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: "payment-operation",
|
|
37
|
+
scheme: PAYBOX_OPERATION_SCHEME,
|
|
38
|
+
id: input.payboxOperationId,
|
|
39
|
+
digest: input.payboxOperationDigest
|
|
40
|
+
},
|
|
41
|
+
privacy: "digest_only"
|
|
42
|
+
};
|
|
43
|
+
const payload = {
|
|
44
|
+
...identity,
|
|
45
|
+
issuedAt: input.issuedAt
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
...payload,
|
|
49
|
+
linkId: `sha256:${sha256Hex(canonicalJson(identity)).slice(2)}`,
|
|
50
|
+
linkDigest: `sha256:${sha256Hex(canonicalJson(payload)).slice(2)}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createPayboxWorkLink } from "./paybox-link.js";
|
|
4
|
+
const input = {
|
|
5
|
+
workReceiptId: `0x${"ab".repeat(32)}`,
|
|
6
|
+
payboxOperationId: "pbx_op_synthetic_delivery_001",
|
|
7
|
+
payboxOperationDigest: `sha256:${"cd".repeat(32)}`,
|
|
8
|
+
issuedAt: "2026-07-30T13:30:00.000Z"
|
|
9
|
+
};
|
|
10
|
+
test("creates a digest-only Paybox to AIPOU work link without payment authority", () => {
|
|
11
|
+
const link = createPayboxWorkLink(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, "paybox-operation-v1");
|
|
16
|
+
assert.equal(link.target.id, input.payboxOperationId);
|
|
17
|
+
assert.equal(link.target.digest, input.payboxOperationDigest);
|
|
18
|
+
assert.match(link.linkId, /^sha256:[0-9a-f]{64}$/);
|
|
19
|
+
assert.match(link.linkDigest, /^sha256:[0-9a-f]{64}$/);
|
|
20
|
+
assert.equal("wallet" in link, false);
|
|
21
|
+
assert.equal("paymentStatus" in link, false);
|
|
22
|
+
assert.equal("claimStatus" in link, false);
|
|
23
|
+
assert.equal("reward" in link, false);
|
|
24
|
+
});
|
|
25
|
+
test("is deterministic and changes when the external artifact changes", () => {
|
|
26
|
+
assert.equal(createPayboxWorkLink(input).linkDigest, createPayboxWorkLink(input).linkDigest);
|
|
27
|
+
assert.equal(createPayboxWorkLink(input).linkId, createPayboxWorkLink({ ...input, issuedAt: "2026-07-30T14:30:00.000Z" }).linkId);
|
|
28
|
+
assert.notEqual(createPayboxWorkLink(input).linkDigest, createPayboxWorkLink({ ...input, issuedAt: "2026-07-30T14:30:00.000Z" }).linkDigest);
|
|
29
|
+
assert.notEqual(createPayboxWorkLink(input).linkId, createPayboxWorkLink({ ...input, payboxOperationDigest: `sha256:${"ef".repeat(32)}` }).linkId);
|
|
30
|
+
});
|
|
31
|
+
test("rejects malformed receipt, digest, opaque reference, and timestamp inputs", () => {
|
|
32
|
+
assert.throws(() => createPayboxWorkLink({ ...input, workReceiptId: "0xabc" }));
|
|
33
|
+
assert.throws(() => createPayboxWorkLink({ ...input, payboxOperationDigest: "sha256:ABC" }));
|
|
34
|
+
assert.throws(() => createPayboxWorkLink({ ...input, payboxOperationId: "" }));
|
|
35
|
+
assert.throws(() => createPayboxWorkLink({ ...input, issuedAt: "not-a-date" }));
|
|
36
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdir, open, rename, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { restrictToCurrentUser } from "./secure-file.js";
|
|
4
|
+
export async function writePrivateJson(file, value) {
|
|
5
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
6
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
7
|
+
const handle = await open(temp, "w", 0o600);
|
|
8
|
+
try {
|
|
9
|
+
if (process.platform !== "win32")
|
|
10
|
+
await handle.chmod(0o600);
|
|
11
|
+
await handle.writeFile(JSON.stringify(value, null, 2) + "\n");
|
|
12
|
+
await handle.sync();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await handle.close();
|
|
16
|
+
}
|
|
17
|
+
if (!(await restrictToCurrentUser(temp)))
|
|
18
|
+
throw new Error("Could not restrict private state file access; update was not committed");
|
|
19
|
+
await rename(temp, file);
|
|
20
|
+
if (process.platform !== "win32") {
|
|
21
|
+
const directory = await open(path.dirname(file), "r");
|
|
22
|
+
try {
|
|
23
|
+
await directory.sync();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await directory.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function readOptionalJson(file) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code === "ENOENT")
|
|
36
|
+
return null;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/protocol.test.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Wallet, keccak256, toUtf8Bytes } from "ethers";
|
|
|
8
8
|
import { canonicalJson } from "./canonical.js";
|
|
9
9
|
import { verifyCollectorPayload } from "./collector.js";
|
|
10
10
|
import { verifyTaskAuthorization } from "./identity.js";
|
|
11
|
+
import { isTaskSessionExpired, taskSessionExpiry } from "./identity.js";
|
|
11
12
|
import { filterEligibleReceipts } from "./policy.js";
|
|
12
13
|
import { providerAssertion } from "./providers.js";
|
|
13
14
|
import { beginTask, completeTask } from "./receipts.js";
|
|
@@ -63,6 +64,28 @@ test("creates verifiable receipts and rejects nonce/evidence replay", async () =
|
|
|
63
64
|
outputHash
|
|
64
65
|
}), /Duplicate task\/output evidence/);
|
|
65
66
|
});
|
|
67
|
+
test("expires abandoned task sessions instead of treating stale authority as completable", async () => {
|
|
68
|
+
const dataDir = await mkdtemp(path.join(tmpdir(), "aipou-expiry-"));
|
|
69
|
+
process.env.AIPOU_DATA_DIR = dataDir;
|
|
70
|
+
process.env.AIPOU_AGENT_PRIVATE_KEY = Wallet.createRandom().privateKey;
|
|
71
|
+
process.env.AIPOU_TASK_SESSION_TTL_SECONDS = "60";
|
|
72
|
+
try {
|
|
73
|
+
const session = await beginTask({
|
|
74
|
+
provider: "local",
|
|
75
|
+
model: "expiry-test",
|
|
76
|
+
taskHash: keccak256(toUtf8Bytes("expired task")),
|
|
77
|
+
client: "test-client",
|
|
78
|
+
chainId: 8453,
|
|
79
|
+
verifyingContract: Wallet.createRandom().address
|
|
80
|
+
});
|
|
81
|
+
assert.equal(taskSessionExpiry(session), session.issuedAt + 60);
|
|
82
|
+
assert.equal(isTaskSessionExpired(session, session.issuedAt + 59), false);
|
|
83
|
+
assert.equal(isTaskSessionExpired(session, session.issuedAt + 60), true);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
delete process.env.AIPOU_TASK_SESSION_TTL_SECONDS;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
66
89
|
test("derives provider_signed only from a configured valid signature", async () => {
|
|
67
90
|
const dataDir = await mkdtemp(path.join(tmpdir(), "aipou-provider-"));
|
|
68
91
|
process.env.AIPOU_DATA_DIR = dataDir;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const AIPOU_RECEIPT_META_KEY = "io.github.0xddneto/aipou-receipt";
|
|
2
|
+
export declare const AIPOU_SPAN_ATTRIBUTE_NAMES: {
|
|
3
|
+
readonly workReceiptId: "aipou.work_receipt_id";
|
|
4
|
+
readonly evidenceClass: "aipou.evidence_class";
|
|
5
|
+
readonly scheme: "aipou.scheme";
|
|
6
|
+
};
|
|
7
|
+
export declare function buildReceiptSpanAttributes(receipt: {
|
|
8
|
+
receiptId: string;
|
|
9
|
+
}): Record<string, string>;
|
|
10
|
+
export declare function buildReceiptResultMeta(receipt: {
|
|
11
|
+
receiptId: string;
|
|
12
|
+
collectorPublicKey: string;
|
|
13
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { collectorFingerprint } from "./collector.js";
|
|
2
|
+
export const AIPOU_RECEIPT_META_KEY = "io.github.0xddneto/aipou-receipt";
|
|
3
|
+
export const AIPOU_SPAN_ATTRIBUTE_NAMES = {
|
|
4
|
+
workReceiptId: "aipou.work_receipt_id",
|
|
5
|
+
evidenceClass: "aipou.evidence_class",
|
|
6
|
+
scheme: "aipou.scheme"
|
|
7
|
+
};
|
|
8
|
+
export function buildReceiptSpanAttributes(receipt) {
|
|
9
|
+
return {
|
|
10
|
+
[AIPOU_SPAN_ATTRIBUTE_NAMES.workReceiptId]: receipt.receiptId,
|
|
11
|
+
[AIPOU_SPAN_ATTRIBUTE_NAMES.evidenceClass]: "issuer_asserted",
|
|
12
|
+
[AIPOU_SPAN_ATTRIBUTE_NAMES.scheme]: "aipou-receipt-v1"
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function buildReceiptResultMeta(receipt) {
|
|
16
|
+
const digest = receipt.receiptId.replace(/^0x/, "").toLowerCase();
|
|
17
|
+
return {
|
|
18
|
+
[AIPOU_RECEIPT_META_KEY]: {
|
|
19
|
+
id: receipt.receiptId,
|
|
20
|
+
uri: `aipou://receipts/${receipt.receiptId}`,
|
|
21
|
+
issuer: collectorFingerprint(receipt.collectorPublicKey),
|
|
22
|
+
digest: `sha256:${digest}`,
|
|
23
|
+
evidenceClass: "issuer_asserted",
|
|
24
|
+
scheme: "aipou-receipt-v1",
|
|
25
|
+
status: "local"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { AIPOU_RECEIPT_META_KEY, AIPOU_SPAN_ATTRIBUTE_NAMES, buildReceiptResultMeta, buildReceiptSpanAttributes } from "./receipt-metadata.js";
|
|
5
|
+
test("receipt result metadata is compact, namespaced, and contains no claim authority", () => {
|
|
6
|
+
const pair = generateKeyPairSync("ed25519");
|
|
7
|
+
const collectorPublicKey = pair.publicKey.export({ format: "pem", type: "spki" }).toString();
|
|
8
|
+
const receiptId = `0x${"ab".repeat(32)}`;
|
|
9
|
+
const metadata = buildReceiptResultMeta({ receiptId, collectorPublicKey });
|
|
10
|
+
const projection = metadata[AIPOU_RECEIPT_META_KEY];
|
|
11
|
+
assert.deepEqual(Object.keys(metadata), [AIPOU_RECEIPT_META_KEY]);
|
|
12
|
+
assert.equal(projection.id, receiptId);
|
|
13
|
+
assert.equal(projection.uri, `aipou://receipts/${receiptId}`);
|
|
14
|
+
assert.match(String(projection.issuer), /^sha256:[0-9a-f]{64}$/);
|
|
15
|
+
assert.equal(projection.digest, `sha256:${"ab".repeat(32)}`);
|
|
16
|
+
assert.equal(projection.evidenceClass, "issuer_asserted");
|
|
17
|
+
assert.equal(projection.scheme, "aipou-receipt-v1");
|
|
18
|
+
assert.equal(projection.status, "local");
|
|
19
|
+
assert.equal("wallet" in projection, false);
|
|
20
|
+
assert.equal("reward" in projection, false);
|
|
21
|
+
assert.equal("claimStatus" in projection, false);
|
|
22
|
+
});
|
|
23
|
+
test("span projection matches the OpenLLMetry correlation boundary", () => {
|
|
24
|
+
const receiptId = `0x${"cd".repeat(32)}`;
|
|
25
|
+
const attributes = buildReceiptSpanAttributes({ receiptId });
|
|
26
|
+
assert.deepEqual(attributes, {
|
|
27
|
+
"aipou.work_receipt_id": receiptId,
|
|
28
|
+
"aipou.evidence_class": "issuer_asserted",
|
|
29
|
+
"aipou.scheme": "aipou-receipt-v1"
|
|
30
|
+
});
|
|
31
|
+
assert.deepEqual(AIPOU_SPAN_ATTRIBUTE_NAMES, {
|
|
32
|
+
workReceiptId: "aipou.work_receipt_id",
|
|
33
|
+
evidenceClass: "aipou.evidence_class",
|
|
34
|
+
scheme: "aipou.scheme"
|
|
35
|
+
});
|
|
36
|
+
for (const forbidden of ["wallet", "reward", "prompt", "output", "claimAuthority", "aipou.validation_status"]) {
|
|
37
|
+
assert.equal(forbidden in attributes, false);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type EvidenceReport } from "./evidence.js";
|
|
2
|
+
export interface ReceiptVerificationResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
workReceiptId: string | null;
|
|
6
|
+
claimedWorkReceiptId: string | null;
|
|
7
|
+
evidence: EvidenceReport;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Verifies only the self-contained receipt facts. It does not check a
|
|
11
|
+
* collector allowlist, an on-chain claim, delivery, or business acceptance.
|
|
12
|
+
*/
|
|
13
|
+
export declare function verifyAipouReceipt(candidate: unknown): ReceiptVerificationResult;
|