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.
Files changed (66) hide show
  1. package/NOTICE +8 -0
  2. package/README.md +18 -0
  3. package/dist/bounded-input.d.ts +8 -0
  4. package/dist/bounded-input.js +21 -0
  5. package/dist/bounded-input.test.d.ts +1 -0
  6. package/dist/bounded-input.test.js +24 -0
  7. package/dist/claims.js +119 -31
  8. package/dist/doctor.d.ts +32 -0
  9. package/dist/doctor.js +112 -0
  10. package/dist/doctor.test.d.ts +1 -0
  11. package/dist/doctor.test.js +164 -0
  12. package/dist/evidence.d.ts +21 -0
  13. package/dist/evidence.js +14 -0
  14. package/dist/feedback.d.ts +17 -0
  15. package/dist/feedback.js +24 -0
  16. package/dist/identity.d.ts +3 -0
  17. package/dist/identity.js +25 -1
  18. package/dist/index.js +15121 -106
  19. package/dist/init.d.ts +8 -2
  20. package/dist/init.js +20 -7
  21. package/dist/init.test.js +18 -4
  22. package/dist/lock.js +47 -9
  23. package/dist/lock.test.d.ts +1 -0
  24. package/dist/lock.test.js +26 -0
  25. package/dist/observed-outcome-pilot.d.ts +16 -0
  26. package/dist/observed-outcome-pilot.js +59 -0
  27. package/dist/observed-outcome-pilot.test.d.ts +1 -0
  28. package/dist/observed-outcome-pilot.test.js +25 -0
  29. package/dist/paybox-link.d.ts +34 -0
  30. package/dist/paybox-link.js +52 -0
  31. package/dist/paybox-link.test.d.ts +1 -0
  32. package/dist/paybox-link.test.js +36 -0
  33. package/dist/private-json.d.ts +2 -0
  34. package/dist/private-json.js +39 -0
  35. package/dist/protocol.test.js +23 -0
  36. package/dist/receipt-metadata.d.ts +13 -0
  37. package/dist/receipt-metadata.js +28 -0
  38. package/dist/receipt-metadata.test.d.ts +1 -0
  39. package/dist/receipt-metadata.test.js +39 -0
  40. package/dist/receipt-verifier.d.ts +13 -0
  41. package/dist/receipt-verifier.js +125 -0
  42. package/dist/receipt-verifier.test.d.ts +1 -0
  43. package/dist/receipt-verifier.test.js +54 -0
  44. package/dist/receipts.js +11 -2
  45. package/dist/settlement-driver.d.ts +3 -0
  46. package/dist/settlement-driver.js +120 -0
  47. package/dist/settlement-driver.test.d.ts +1 -0
  48. package/dist/settlement-driver.test.js +78 -0
  49. package/dist/settlement-runner.d.ts +34 -0
  50. package/dist/settlement-runner.js +40 -0
  51. package/dist/settlement-runner.test.d.ts +1 -0
  52. package/dist/settlement-runner.test.js +128 -0
  53. package/dist/status.js +2 -0
  54. package/dist/store.d.ts +2 -0
  55. package/dist/store.js +33 -9
  56. package/dist/technocore-link.d.ts +34 -0
  57. package/dist/technocore-link.js +49 -0
  58. package/dist/technocore-link.test.d.ts +1 -0
  59. package/dist/technocore-link.test.js +36 -0
  60. package/dist/tool-metadata.test.d.ts +1 -0
  61. package/dist/tool-metadata.test.js +86 -0
  62. package/dist/types.d.ts +6 -0
  63. package/dist/usage-recovery.test.d.ts +1 -0
  64. package/dist/usage-recovery.test.js +121 -0
  65. package/package.json +7 -5
  66. 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: "npx";
11
- args: ["-y", "aipou-mcp-server"];
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 TOKEN_ADDRESS = "0x55f0Cc5e51A1284D20337d6cbb18938C8A1ABCbB";
7
- const CLAIMS_ADDRESS = "0x4ca4C98fB784D20EdC8E2A7F531dAab4c6e53058";
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 buildResult(mode, walletAddress, dataDir, keyFile) {
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
- command: "npx",
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: TOKEN_ADDRESS,
28
- AIPOU_CLAIMS_ADDRESS: 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: process.execPath,
96
- args: [indexPath],
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
- // State mutations finish in well under a second, so a lock older than this
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`, { encoding: "utf8", flag: "wx" });
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 rm(lockPath, { force: true });
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
- continue;
62
+ if (error.code === "ENOENT")
63
+ continue;
64
+ throw error;
27
65
  }
28
66
  if (Date.now() > deadline) {
29
- throw new Error(`Timed out waiting for lock ${lockPath}; remove it manually if no other AIPOU process is running`);
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,2 @@
1
+ export declare function writePrivateJson(file: string, value: unknown): Promise<void>;
2
+ export declare function readOptionalJson<T>(file: string): Promise<T | null>;
@@ -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
+ }
@@ -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;