@c4roa0/agentpay-user 0.0.0-stage → 0.3.1

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 (175) hide show
  1. package/AGENTS.md +137 -0
  2. package/CONTRIBUTING.md +16 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE.md +20 -0
  5. package/README.md +85 -2
  6. package/SECURITY.md +16 -0
  7. package/doc/README.md +52 -0
  8. package/doc/TODO.md +107 -0
  9. package/doc/architecture.md +99 -0
  10. package/doc/design.md +60 -0
  11. package/doc/develope.md +68 -0
  12. package/doc/plans/user-wallet-distribution.md +91 -0
  13. package/doc/reference/harness-skill-runtime.md +120 -0
  14. package/doc/reference/local-cache-foundation.md +176 -0
  15. package/doc/reference/local-safe-owner-ui.md +25 -0
  16. package/doc/reference/local-update.md +18 -0
  17. package/doc/reference/multi-service-runtime.md +119 -0
  18. package/doc/reference/npm-integration-verification.md +103 -0
  19. package/doc/reference/onboarding-writing.md +48 -0
  20. package/doc/reference/original-order-recovery.md +97 -0
  21. package/doc/reference/private-anvil-onboarding-config.example.json +18 -0
  22. package/doc/reference/public-deployment-settings.md +56 -0
  23. package/doc/reference/random-skill-contract.md +18 -0
  24. package/doc/reference/result-file-cache.md +166 -0
  25. package/doc/reference/runtime-address-recovery.md +42 -0
  26. package/doc/reference/runtime-result-cache.md +157 -0
  27. package/doc/reference/runtime-startup-recovery.md +141 -0
  28. package/doc/reference/skill-input-output-cache.md +93 -0
  29. package/doc/reference/wallet-identity-policy-history.md +23 -0
  30. package/doc/runbooks/commands.md +103 -0
  31. package/doc/runbooks/deployment-authorization-update.md +42 -0
  32. package/doc/runbooks/emergency-payment-stop.md +80 -0
  33. package/doc/runbooks/local-autostart.md +212 -0
  34. package/doc/runbooks/local-cache.md +115 -0
  35. package/doc/runbooks/local-update.md +84 -0
  36. package/doc/runbooks/market-connection.md +73 -0
  37. package/doc/runbooks/npm-publication.md +56 -0
  38. package/doc/runbooks/official-test-package.md +62 -0
  39. package/doc/runbooks/order-cancellation.md +56 -0
  40. package/doc/runbooks/order-support.md +41 -0
  41. package/doc/runbooks/policy-renewal.md +80 -0
  42. package/doc/runbooks/private-anvil-terminal-setup.md +58 -0
  43. package/doc/runbooks/runtime-skill-client.md +155 -0
  44. package/doc/runbooks/safe-owner-web.md +104 -0
  45. package/doc/runbooks/safe-policy-migration.md +36 -0
  46. package/doc/runbooks/safe-policy-setup.md +52 -0
  47. package/doc/runbooks/terminal-authorization-preview.md +38 -0
  48. package/doc/runbooks/unlock-wallet.md +68 -0
  49. package/doc/runbooks/user-onboarding.md +87 -0
  50. package/doc/runbooks/wallet-identity.md +59 -0
  51. package/doc/security/public-source-review-2026-11-06.md +71 -0
  52. package/doc/usage.md +54 -0
  53. package/package.json +49 -4
  54. package/progress.md +210 -0
  55. package/run +29 -0
  56. package/scripts/agentpay-cli.mjs +11 -0
  57. package/scripts/authorize-existing-safe.mjs +211 -0
  58. package/scripts/autostart.mjs +186 -0
  59. package/scripts/cancel-order.mjs +76 -0
  60. package/scripts/check-installation.mjs +26 -0
  61. package/scripts/check-official-test-package.mjs +35 -0
  62. package/scripts/check-purchase-readiness.mjs +275 -0
  63. package/scripts/configure-runtime-client.mjs +16 -0
  64. package/scripts/guide-policy.mjs +64 -0
  65. package/scripts/initialize-caller-credential.mjs +11 -0
  66. package/scripts/install-skill-bundle.mjs +176 -0
  67. package/scripts/install-user.mjs +24 -0
  68. package/scripts/local-service.mjs +60 -0
  69. package/scripts/lock.mjs +11 -0
  70. package/scripts/login-notify.mjs +39 -0
  71. package/scripts/migrate-safe-policy.mjs +165 -0
  72. package/scripts/official-paid-test.mjs +9 -0
  73. package/scripts/poll-unconfirmed-notice.mjs +75 -0
  74. package/scripts/prepare-onboarding.mjs +33 -0
  75. package/scripts/preview-policy.mjs +65 -0
  76. package/scripts/random-model-ack-smoke.mjs +235 -0
  77. package/scripts/random-reject-smoke.mjs +147 -0
  78. package/scripts/renew.mjs +9 -0
  79. package/scripts/repair.mjs +10 -0
  80. package/scripts/revoke.mjs +9 -0
  81. package/scripts/select-skill-bundle.mjs +39 -0
  82. package/scripts/setup-local.mjs +199 -0
  83. package/scripts/status.mjs +13 -0
  84. package/scripts/test-package.mjs +35 -0
  85. package/scripts/unlock.mjs +13 -0
  86. package/scripts/update.mjs +7 -0
  87. package/scripts/user-setup.mjs +219 -0
  88. package/scripts/verify-npm-release.mjs +32 -0
  89. package/src/active-skill.mjs +160 -0
  90. package/src/anvil-onboarding-state.mjs +96 -0
  91. package/src/anvil-onboarding.mjs +1036 -0
  92. package/src/authorization-question-checkpoint.mjs +114 -0
  93. package/src/autostart-files.mjs +50 -0
  94. package/src/cache-management-cli.mjs +302 -0
  95. package/src/cache-management.mjs +47 -0
  96. package/src/caller-auth.mjs +58 -0
  97. package/src/client-request-lock.mjs +44 -0
  98. package/src/connect-market.mjs +408 -0
  99. package/src/demo-unlock-secret.mjs +64 -0
  100. package/src/deployment-status.mjs +198 -0
  101. package/src/file-cache-cli.mjs +83 -0
  102. package/src/file-cache-owner.mjs +74 -0
  103. package/src/file-cache-settings.mjs +50 -0
  104. package/src/file-cache.mjs +790 -0
  105. package/src/file-download.mjs +281 -0
  106. package/src/finalization-disclosure.mjs +16 -0
  107. package/src/guided-setup.mjs +194 -0
  108. package/src/index.mjs +1233 -0
  109. package/src/install-and-setup.mjs +38 -0
  110. package/src/install-preflight.mjs +201 -0
  111. package/src/install-user.mjs +279 -0
  112. package/src/installation-paths.mjs +31 -0
  113. package/src/invocation-result.mjs +148 -0
  114. package/src/invocation-store.mjs +385 -0
  115. package/src/local-data-directory.mjs +117 -0
  116. package/src/local-info.mjs +181 -0
  117. package/src/local-services.mjs +156 -0
  118. package/src/local-time.mjs +36 -0
  119. package/src/local-update.mjs +316 -0
  120. package/src/local-wallet-ipc.mjs +93 -0
  121. package/src/login-reminder.mjs +28 -0
  122. package/src/market-download-url.mjs +21 -0
  123. package/src/node-version.mjs +12 -0
  124. package/src/npm-update.mjs +126 -0
  125. package/src/official-paid-test.mjs +407 -0
  126. package/src/official-test-package.mjs +41 -0
  127. package/src/official-test-update.mjs +162 -0
  128. package/src/order-cancellation.mjs +241 -0
  129. package/src/order-closure.mjs +42 -0
  130. package/src/order-list-panel.mjs +110 -0
  131. package/src/order-review.mjs +424 -0
  132. package/src/order-status-panel.mjs +65 -0
  133. package/src/order-support.mjs +365 -0
  134. package/src/owner-approval-server.mjs +332 -0
  135. package/src/owner-signer.mjs +67 -0
  136. package/src/policy-info.mjs +38 -0
  137. package/src/policy-migration.mjs +84 -0
  138. package/src/policy-operations.mjs +67 -0
  139. package/src/policy-preview-server.mjs +236 -0
  140. package/src/policy-terminal-guide.mjs +377 -0
  141. package/src/prepare-local-services.mjs +143 -0
  142. package/src/prepare-onboarding.mjs +210 -0
  143. package/src/private-journal-files.mjs +29 -0
  144. package/src/protocol-package.mjs +25 -0
  145. package/src/readiness-profile.mjs +30 -0
  146. package/src/renew-policy.mjs +605 -0
  147. package/src/repair-market-wallet.mjs +137 -0
  148. package/src/restore-wallet.mjs +255 -0
  149. package/src/result-cache-view.mjs +276 -0
  150. package/src/runtime-client.mjs +1145 -0
  151. package/src/runtime-connection-settings.mjs +101 -0
  152. package/src/runtime-recovery.mjs +667 -0
  153. package/src/runtime-status.mjs +32 -0
  154. package/src/safe-owner-anvil.mjs +115 -0
  155. package/src/safe-policy-plan.mjs +195 -0
  156. package/src/service-reference.mjs +233 -0
  157. package/src/service-url.mjs +52 -0
  158. package/src/settlement-response.mjs +69 -0
  159. package/src/setup-autostart.mjs +197 -0
  160. package/src/setup-demo.mjs +152 -0
  161. package/src/skill-bundle.mjs +142 -0
  162. package/src/skill-client.mjs +198 -0
  163. package/src/skill-install-prompt.mjs +31 -0
  164. package/src/skill-package.mjs +50 -0
  165. package/src/skill-zip.mjs +220 -0
  166. package/src/software-versions.mjs +48 -0
  167. package/src/status-panel.mjs +603 -0
  168. package/src/transport.mjs +48 -0
  169. package/src/uninstall-local.mjs +299 -0
  170. package/src/user-cli.mjs +354 -0
  171. package/src/user-setup-guide.mjs +193 -0
  172. package/src/wallet-control.mjs +182 -0
  173. package/src/wallet-ipc-error.mjs +16 -0
  174. package/src/wallet-package.mjs +28 -0
  175. package/update-pair.json +6 -0
@@ -0,0 +1,126 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ // Update only a real global npm installation. Source checkouts/npm links are never
3
+ // overwritten. npm selects User; User's exact dependencies select Wallet/Protocol.
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { access, appendFile, lstat, mkdir, open, readFile, realpath, unlink } from 'node:fs/promises';
7
+ import { constants } from 'node:fs';
8
+ import { dirname, isAbsolute, join } from 'node:path';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { createInterface } from 'node:readline/promises';
11
+ import { installedUserDir, resolveUserDataDir, rememberUserDataDir } from './local-data-directory.mjs';
12
+ import { installationDirectory } from './installation-paths.mjs';
13
+ import { prepareOfficialTestUpdate } from './official-test-update.mjs';
14
+ const execute = promisify(execFile);
15
+ const name = '@c4roa0/agentpay-user';
16
+ const stable = value => typeof value === 'string' && /^\d+\.\d+\.\d+$/.test(value);
17
+ function newer(next, current) {
18
+ const a = next.split('.').map(BigInt), b = current.split('.').map(BigInt);
19
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
20
+ return false;
21
+ }
22
+ async function command(file, args, { logPath, timeout = 120000, env = process.env, cwd = process.cwd() } = {}) {
23
+ try {
24
+ const result = await execute(file, args, { timeout, cwd, maxBuffer: 2 * 1024 * 1024,
25
+ env: { ...env, PATH: dirname(process.execPath) + ':' + (env.PATH || '') } });
26
+ if (logPath) await appendFile(logPath, result.stdout + result.stderr, {mode:0o600});
27
+ return result.stdout.trim();
28
+ } catch (error) {
29
+ if (logPath) await appendFile(logPath, String(error.stdout || '') + String(error.stderr || ''), {mode:0o600});
30
+ throw Error(logPath ? 'The command did not complete. Check the update log.' : 'Could not query npm. Check your network and npm configuration, then retry.');
31
+ }
32
+ }
33
+ async function askConfirmation(message) {
34
+ if (!process.stdin.isTTY) throw Error('Run agentpay update in a terminal, or use -f.');
35
+ const terminal = createInterface({ input:process.stdin, output:process.stdout });
36
+ try { return await terminal.question(message + '\n> '); } finally { terminal.close(); }
37
+ }
38
+ export async function updateFromNpm({
39
+ userDir = installedUserDir, dataDir, force = false, write = console.log,
40
+ ask = askConfirmation,
41
+ npm = (args, options) => command('npm', args, options),
42
+ service = (action, options) => command(process.execPath, [join(userDir,'scripts/autostart.mjs'), action], options),
43
+ repair = options => command(process.execPath, [join(userDir,'scripts/repair.mjs'), '-f'], options),
44
+ officialTest = prepareOfficialTestUpdate,
45
+ } = {}) {
46
+ let lock, lockPath, logPath, installing = false;
47
+ try {
48
+ const current = JSON.parse(await readFile(join(userDir, 'package.json'), 'utf8'));
49
+ if (current.name !== name || !stable(current.version)) throw Error('Unrecognized User installation.');
50
+ write('Checking for a User update…');
51
+ const root = await npm(['root','--global']), prefix = await npm(['prefix','--global']);
52
+ if (!isAbsolute(root) || !isAbsolute(prefix)) throw Error('npm installation paths could not be verified.');
53
+ const globalPath = join(root, name);
54
+ const globalInfo = await lstat(globalPath).catch(e => { if(e.code !== 'ENOENT') throw e; return null; });
55
+ if (!globalInfo || globalInfo.isSymbolicLink() || await realpath(globalPath) !== await realpath(userDir))
56
+ throw Error('This is a source checkout or linked installation. Update its files, run npm ci, then agentpay repair.');
57
+ await access(dirname(globalPath), constants.W_OK);
58
+ const metadata = JSON.parse(await npm(['view',name + '@latest','--global','--json']));
59
+ if (metadata.name !== name || !stable(metadata.version) ||
60
+ !stable(metadata.dependencies?.['@c4roa0/agentpay-wallet']) ||
61
+ !stable(metadata.dependencies?.['@c4roa0/agentpay-protocol']))
62
+ throw Error('The published User release does not declare reviewed package versions.');
63
+ dataDir ??= await resolveUserDataDir({userDir});
64
+ if (!newer(metadata.version,current.version)) {
65
+ write(`User ${current.version} is current. Checking the official test Skill.`);
66
+ await officialTest({userDir,dataDir,write});
67
+ return 0;
68
+ }
69
+ write(`User ${current.version} → ${metadata.version} · Wallet ${metadata.dependencies['@c4roa0/agentpay-wallet']}`);
70
+ write('Background services will pause. Keys, permissions, Orders and cache are kept.');
71
+ if (!force && (await ask("Type 'confirm' to update, or 'q' to cancel")).trim().toLowerCase() !== 'confirm') {
72
+ write('Update cancelled. Nothing was changed.'); return 0;
73
+ }
74
+ // Persist a custom directory before replacing this package; no key/Policy changes.
75
+ await rememberUserDataDir({userDir,dataDir});
76
+ const stateDir = installationDirectory();
77
+ await mkdir(stateDir,{recursive:true,mode:0o700});
78
+ logPath = join(stateDir, `update-${Date.now()}-${randomUUID()}.log`);
79
+ lockPath = join(stateDir,'install.lock');
80
+ lock = await open(lockPath,'wx',0o600);
81
+ const rechecked = JSON.parse(await readFile(join(userDir,'package.json'),'utf8'));
82
+ if (rechecked.version !== current.version || rechecked.name !== name)
83
+ throw Error('Program files changed while reviewing the update. Run agentpay update again.');
84
+ const env = {...process.env,AGENTPAY_USER_DATA_DIR:dataDir};
85
+ const options = {logPath,env,cwd:stateDir,timeout:600000};
86
+ const spec = name + '@' + metadata.version;
87
+ const args = ['install','--global','--prefix',prefix,'--engine-strict','--ignore-scripts','--no-audit','--no-fund',spec];
88
+ write('1/3 Check the update requirements.');
89
+ await npm([...args,'--dry-run'],options);
90
+ let configured = false;
91
+ try { configured = (await lstat(join(dataDir,'runtime/local-services.json'))).isFile(); }
92
+ catch(e) { if(e.code !== 'ENOENT') throw e; }
93
+ // All imports used by this updater are loaded before npm replaces its files.
94
+ if (configured) await service('stop',options);
95
+ installing = true;
96
+ write('2/3 Install User and its matching Wallet. This may take a few minutes.');
97
+ await npm(args,options);
98
+ const installed = JSON.parse(await readFile(join(userDir,'package.json'),'utf8'));
99
+ if(installed.name !== name || installed.version !== metadata.version ||
100
+ installed.dependencies?.['@c4roa0/agentpay-wallet'] !== metadata.dependencies['@c4roa0/agentpay-wallet'] ||
101
+ installed.dependencies?.['@c4roa0/agentpay-protocol'] !== metadata.dependencies['@c4roa0/agentpay-protocol'])
102
+ throw Error('The installed version does not match the confirmed update.');
103
+ // Release before the new version's repair acquires the same lock. Another
104
+ // operation may win that lock; repair then refuses safely and can be retried.
105
+ await lock.close(); lock = null; await unlink(lockPath); lockPath = null;
106
+ if (configured) {
107
+ write('3/3 Restore services and verify the official test Skill.');
108
+ // A fresh Node process loads the NEW repair/client/official digest, not old imports.
109
+ const output = await repair(options);
110
+ if(output) write(output);
111
+ } else {
112
+ write('3/3 Program files are ready. Local setup has not been configured.');
113
+ write('Next: agentpay install --market YOUR_MARKET_URL');
114
+ }
115
+ write(`User ${metadata.version} installed. No paid test was started.`);
116
+ return 0;
117
+ } catch(error) {
118
+ write(installing
119
+ ? 'Update did not finish. Your private data was kept. Reinstall the intended User version with npm if needed, then run agentpay repair.'
120
+ : 'Update stopped before replacing program files. ' + (error.code === 'EEXIST' ? 'Another installation is running.' : error.message));
121
+ if(logPath) write('Log: ' + logPath);
122
+ return 1;
123
+ } finally {
124
+ if(lock) { await lock.close(); await unlink(lockPath); }
125
+ }
126
+ }
@@ -0,0 +1,407 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
3
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
4
+ import { randomUUID } from "node:crypto";
5
+ import { constants } from "node:fs";
6
+ import { open, link, rename, unlink, mkdir, lstat } from "node:fs/promises";
7
+ import { join, dirname } from "node:path";
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import { createInterface } from "node:readline/promises";
11
+ import { Contract, JsonRpcProvider, formatUnits } from "ethers";
12
+ import { resolveUserDataDir } from "./local-data-directory.mjs";
13
+ import { OFFICIAL_TEST_PACKAGE } from "./official-test-package.mjs";
14
+ import { inspectInstalledBundle } from "./active-skill.mjs";
15
+ import { loadRuntimeConnection, readServiceOffer } from "./runtime-client.mjs";
16
+ import { normalizeAcceptedTerms } from "./service-reference.mjs";
17
+ import { readPrivateFile } from "./runtime-client.mjs";
18
+ import { readServiceSettings } from "./local-services.mjs";
19
+ import { callWallet } from "@c4roa0/agentpay-wallet";
20
+ import { withClientRequestLock } from "./client-request-lock.mjs";
21
+
22
+ const run = promisify(execFile),
23
+ version = "agentpay.official-paid-test.v1";
24
+ const input = { min: 10, max: 99 };
25
+ function serviceTime(seconds) {
26
+ const [value, unit] =
27
+ seconds % 3600 === 0
28
+ ? [seconds / 3600, "hour"]
29
+ : seconds % 60 === 0
30
+ ? [seconds / 60, "minute"]
31
+ : [seconds, "second"];
32
+ return `${value} ${unit}${value === 1 ? "" : "s"}`;
33
+ }
34
+ async function readPrivate(path) {
35
+ let h;
36
+ try {
37
+ h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
38
+ const s = await h.stat();
39
+ if (
40
+ !s.isFile() ||
41
+ s.uid !== process.getuid() ||
42
+ s.mode & 0o077 ||
43
+ s.size > 16384
44
+ )
45
+ throw Error("Official test record is unsafe");
46
+ return JSON.parse(await h.readFile("utf8"));
47
+ } catch (e) {
48
+ if (e.code === "ENOENT") return null;
49
+ throw e;
50
+ } finally {
51
+ await h?.close();
52
+ }
53
+ }
54
+ async function save(path, value, replace = false) {
55
+ const tmp = path + "." + randomUUID() + ".tmp";
56
+ let h;
57
+ try {
58
+ h = await open(tmp, "wx", 0o600);
59
+ await h.writeFile(JSON.stringify(value) + "\n");
60
+ await h.sync();
61
+ await h.close();
62
+ h = null;
63
+ if (replace) await rename(tmp, path);
64
+ else await link(tmp, path);
65
+ const dir = await open(
66
+ dirname(path),
67
+ constants.O_RDONLY | constants.O_DIRECTORY,
68
+ );
69
+ try {
70
+ await dir.sync();
71
+ } finally {
72
+ await dir.close();
73
+ }
74
+ } finally {
75
+ await h?.close();
76
+ await unlink(tmp).catch((e) => {
77
+ if (e.code !== "ENOENT") throw e;
78
+ });
79
+ }
80
+ }
81
+ async function prompt(question) {
82
+ if (!process.stdin.isTTY)
83
+ throw Error("Run agentpay payment test in your terminal.");
84
+ const t = createInterface({ input: process.stdin, output: process.stdout });
85
+ try {
86
+ return (await t.question(question + "\n> ")).trim().toLowerCase();
87
+ } finally {
88
+ t.close();
89
+ }
90
+ }
91
+ async function archiveCompleted(dataDir, record) {
92
+ const dir = join(dataDir, "runtime", "official-tests");
93
+ await mkdir(dir, { mode: 0o700 }).catch((e) => {
94
+ if (e.code !== "EEXIST") throw e;
95
+ });
96
+ const st = await lstat(dir);
97
+ if (
98
+ !st.isDirectory() ||
99
+ st.isSymbolicLink() ||
100
+ st.uid !== process.getuid() ||
101
+ st.mode & 0o077
102
+ )
103
+ throw Error("Official test history directory is unsafe");
104
+ const path = join(dir, `${record.requestId}.json`);
105
+ try {
106
+ await save(path, record);
107
+ } catch (e) {
108
+ if (e.code !== "EEXIST") throw e;
109
+ const prior = await readPrivate(path);
110
+ if (JSON.stringify(prior) !== JSON.stringify(record))
111
+ throw Error("Official test history conflicts with the saved result");
112
+ }
113
+ }
114
+ function resultIdentity(body) {
115
+ const r = body?.result;
116
+ if (
117
+ body?.ok !== true ||
118
+ !/^0x[0-9a-f]{64}$/.test(r?.orderId || "") ||
119
+ !/^0x[0-9a-f]{64}$/.test(r?.outputHash || "") ||
120
+ r.output?.kind !== "random" ||
121
+ !Number.isSafeInteger(r.output.value) ||
122
+ r.output.value < input.min ||
123
+ r.output.value > input.max ||
124
+ r.deliveryEvidenceVerified !== true
125
+ )
126
+ throw Error(
127
+ "Official Skill did not return verified delivery. Keep the test record and retry the same request.",
128
+ );
129
+ return { orderId: r.orderId, outputHash: r.outputHash };
130
+ }
131
+ async function confirmDelivery(dataDir, orderId) {
132
+ const token = (
133
+ await readPrivateFile(join(dataDir, "wallet", "agent.token"), 256)
134
+ ).trim();
135
+ if (!/^[0-9a-f]{64}$/.test(token))
136
+ throw Error("Local Wallet credential is invalid.");
137
+ const { settlementUrl } = await readServiceSettings(dataDir);
138
+ const deadline = Date.now() + 30000;
139
+ for (;;) {
140
+ try {
141
+ return await callWallet(
142
+ join(dataDir, "wallet", "wallet.sock"),
143
+ token,
144
+ "confirmSettlementDelivery",
145
+ { settlementUrl, orderId },
146
+ );
147
+ } catch (e) {
148
+ if (
149
+ Date.now() >= deadline ||
150
+ e.code !== "SETTLEMENT_REQUEST_REJECTED" ||
151
+ !String(e.message || "").includes("finalization cannot be confirmed")
152
+ )
153
+ throw e;
154
+ await new Promise((resolve) => setTimeout(resolve, 250));
155
+ }
156
+ }
157
+ }
158
+ export async function runOfficialPaidTest({
159
+ dataDir,
160
+ write = console.log,
161
+ ask = prompt,
162
+ read = readPrivate,
163
+ saveRecord = save,
164
+ archive = archiveCompleted,
165
+ lock = withClientRequestLock,
166
+ offer = readServiceOffer,
167
+ execute = run,
168
+ ack = confirmDelivery,
169
+ inspect = inspectInstalledBundle,
170
+ connect = loadRuntimeConnection,
171
+ tokenInfo = async (policy) => {
172
+ const rpc = new JsonRpcProvider(policy.registryRpcUrl, policy.chainId, {
173
+ staticNetwork: true,
174
+ batchMaxCount: 1,
175
+ });
176
+ try {
177
+ const token = new Contract(
178
+ policy.token,
179
+ [
180
+ "function decimals() view returns(uint8)",
181
+ "function symbol() view returns(string)",
182
+ ],
183
+ rpc,
184
+ );
185
+ const [decimals, symbol] = await Promise.all([
186
+ token.decimals(),
187
+ token.symbol(),
188
+ ]);
189
+ return { decimals: Number(decimals), symbol };
190
+ } finally {
191
+ rpc.destroy();
192
+ }
193
+ },
194
+ } = {}) {
195
+ try {
196
+ dataDir ??= await resolveUserDataDir();
197
+ return await lock(
198
+ join(dataDir, "runtime", "official-test.lock"),
199
+ async () => {
200
+ const path = join(
201
+ dataDir,
202
+ "runtime",
203
+ `official-paid-test-${OFFICIAL_TEST_PACKAGE.version}.json`,
204
+ );
205
+ let record = await read(path);
206
+ const directory = join(
207
+ dataDir,
208
+ "runtime",
209
+ "skill-bundles",
210
+ `${OFFICIAL_TEST_PACKAGE.name}-${OFFICIAL_TEST_PACKAGE.version}-${OFFICIAL_TEST_PACKAGE.sha256}`,
211
+ );
212
+ const installed = await inspect(directory);
213
+ if (
214
+ installed.sha256 !== OFFICIAL_TEST_PACKAGE.sha256 ||
215
+ installed.locallyModified
216
+ )
217
+ throw Error(
218
+ "The installed official Skill differs from this User release. Run agentpay repair first.",
219
+ );
220
+ const connection = await connect({ dataDir });
221
+ if (
222
+ record &&
223
+ (record.version !== version ||
224
+ record.packageSha256 !== OFFICIAL_TEST_PACKAGE.sha256 ||
225
+ !/^[0-9a-f-]{36}$/.test(record.requestId || "") ||
226
+ JSON.stringify(record.input) !== JSON.stringify(input) ||
227
+ !record.terms ||
228
+ (record.needsRequote && record.result) ||
229
+ (record.result &&
230
+ (!/^0x[0-9a-f]{64}$/.test(record.result.orderId || "") ||
231
+ !/^0x[0-9a-f]{64}$/.test(record.result.outputHash || ""))))
232
+ )
233
+ throw Error(
234
+ "Saved official test record needs review. It was preserved; no new request was started.",
235
+ );
236
+ const completed = record?.acknowledged === true;
237
+ const previous = completed ? record : null;
238
+ if (completed) record = null;
239
+ if (!record || record.needsRequote) {
240
+ const current = await offer(connection, installed.service.reference);
241
+ if (
242
+ current?.commercialReady !== true ||
243
+ current.blockers?.length ||
244
+ !current.quote ||
245
+ BigInt(current.quote.maxUsageUnits || 0) < 1n
246
+ )
247
+ throw Error(
248
+ "Official service is not ready for a paid test. Check agentpay status and retry later.",
249
+ );
250
+ const terms = normalizeAcceptedTerms({
251
+ baseAmountAtomic: String(current.quote.baseAmountAtomic),
252
+ unitPriceAtomic: String(current.quote.unitPriceAtomic),
253
+ serviceWindowSeconds:
254
+ current.orderDeadlinePolicy?.serviceWindowSeconds,
255
+ });
256
+ const policy = await read(join(dataDir, "wallet", "policy.json"));
257
+ const ref = installed.service.reference;
258
+ if (
259
+ !policy ||
260
+ policy.chainId !== 31337 ||
261
+ policy.token?.toLowerCase() !==
262
+ current.quote.asset?.toLowerCase() ||
263
+ current.quote.network !== ref.profile.network ||
264
+ current.quote.providerId !== ref.profile.providerId ||
265
+ current.quote.skillId !== ref.skillId ||
266
+ current.quote.profileHash !== ref.profileHash
267
+ )
268
+ throw Error(
269
+ "Saved test Policy or service identity does not match the current offer on private Anvil.",
270
+ );
271
+ const { decimals, symbol } = await tokenInfo(policy);
272
+ const maximum =
273
+ BigInt(terms.baseAmountAtomic) + BigInt(terms.unitPriceAtomic);
274
+ if (maximum > BigInt(policy.maxAmount))
275
+ throw Error(
276
+ "Current quote exceeds this Wallet’s per-payment limit.",
277
+ );
278
+ write(
279
+ `\n> ${record ? "Review the updated test price" : "Test your payment connection"}\n\nUse the official test service to check payment and delivery.\n\nMaximum cost: ${formatUnits(maximum, Number(decimals))} ${symbol}\nService time limit: ${serviceTime(terms.serviceWindowSeconds)}\n\nAfter successful delivery, Wallet confirms receipt automatically.`,
280
+ );
281
+ if (previous)
282
+ write(
283
+ "Your previous test is complete. This will be a new paid test.",
284
+ );
285
+ if (
286
+ (await ask("Type 'confirm' to pay and test, or 'q' to cancel")) !==
287
+ "confirm"
288
+ ) {
289
+ write("Test cancelled. No new Order was started.");
290
+ return 0;
291
+ }
292
+ write("Preparing your test…");
293
+ if (record) {
294
+ record = { ...record, terms, needsRequote: false };
295
+ await saveRecord(path, record, true);
296
+ } else {
297
+ record = {
298
+ version,
299
+ packageSha256: OFFICIAL_TEST_PACKAGE.sha256,
300
+ requestId: randomUUID(),
301
+ input,
302
+ terms,
303
+ };
304
+ if (previous) {
305
+ await archive(dataDir, previous);
306
+ await saveRecord(path, record, true);
307
+ } else
308
+ try {
309
+ await saveRecord(path, record);
310
+ } catch (e) {
311
+ if (e.code !== "EEXIST") throw e;
312
+ record = await read(path);
313
+ }
314
+ }
315
+ } else
316
+ write(
317
+ "\n> Continue the unfinished payment test\nContinuing the original Order.",
318
+ );
319
+ if (
320
+ record.version !== version ||
321
+ record.packageSha256 !== OFFICIAL_TEST_PACKAGE.sha256 ||
322
+ JSON.stringify(record.input) !== JSON.stringify(input)
323
+ )
324
+ throw Error(
325
+ "Saved test record conflicts with this release. No request was started.",
326
+ );
327
+ normalizeAcceptedTerms(record.terms);
328
+ const script = join(directory, "random-skill", "scripts", "random.mjs");
329
+ const args = [
330
+ script,
331
+ "--invoke",
332
+ String(input.min),
333
+ String(input.max),
334
+ "--units",
335
+ "1",
336
+ "--request-id",
337
+ record.requestId,
338
+ "--accept-terms",
339
+ JSON.stringify(record.terms),
340
+ ];
341
+ const call = async () => {
342
+ let stdout;
343
+ try {
344
+ ({ stdout } = await execute(process.execPath, args, {
345
+ env: { ...process.env, AGENTPAY_USER_DATA_DIR: dataDir },
346
+ timeout: 330000,
347
+ maxBuffer: 2097152,
348
+ }));
349
+ } catch (error) {
350
+ let response;
351
+ try {
352
+ response = JSON.parse(error.stdout);
353
+ } catch {}
354
+ if (response?.error?.code === "QUOTE_TERMS_CHANGED") {
355
+ record = { ...record, needsRequote: true };
356
+ await saveRecord(path, record, true);
357
+ throw Error(
358
+ "Price or service time changed before Order creation. Run agentpay payment test again to review the new quote with the same request ID.",
359
+ );
360
+ }
361
+ throw error;
362
+ }
363
+ return resultIdentity(JSON.parse(stdout));
364
+ };
365
+ write("Waiting for the test result…");
366
+ const first = await call();
367
+ if (
368
+ record.result &&
369
+ (first.orderId !== record.result.orderId ||
370
+ first.outputHash !== record.result.outputHash)
371
+ )
372
+ throw Error(
373
+ "Retry returned different delivery. Preserve the Order for review.",
374
+ );
375
+ if (!record.result) {
376
+ record = { ...record, result: first };
377
+ await saveRecord(path, record, true);
378
+ }
379
+ write("Result received. Checking the saved result…");
380
+ const repeat = await call();
381
+ if (
382
+ repeat.orderId !== first.orderId ||
383
+ repeat.outputHash !== first.outputHash
384
+ )
385
+ throw Error(
386
+ "Repeated call returned different delivery. Preserve the Order for review.",
387
+ );
388
+ if (!record.acknowledged) {
389
+ write("Confirming receipt…");
390
+ await ack(dataDir, first.orderId);
391
+ record = { ...record, acknowledged: true };
392
+ await saveRecord(path, record, true);
393
+ }
394
+ write(
395
+ `Test complete. Payment and delivery are working.\nOrder ID: ${first.orderId}`,
396
+ );
397
+ return 0;
398
+ },
399
+ );
400
+ } catch (e) {
401
+ write(`Official test paused: ${e.message || "Unknown error"}`);
402
+ write(
403
+ "Retry agentpay payment test. Keep the original Order and local test record; do not start a replacement purchase.",
404
+ );
405
+ return 1;
406
+ }
407
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
3
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
4
+ import { createHash } from "node:crypto";
5
+
6
+ // Release-reviewed ZIP bytes, not a value supplied by Market or deployment config.
7
+ // Update only after reviewing the actual official package. See the release runbook.
8
+ // This belongs to the official onboarding test, never generic Runtime routing.
9
+ export const OFFICIAL_TEST_PACKAGE = Object.freeze({
10
+ name: "agentpay-random",
11
+ version: "1.2.0",
12
+ sha256: "aa366fc2e747b5b444786d542b0a412a505e58ea15252ac1a792b99079084146",
13
+ });
14
+ const mismatch = () =>
15
+ Object.assign(
16
+ new Error(
17
+ "The official test package does not match this User release. Keep your existing files and requests. Check for a reviewed User update; do not bypass the file check.",
18
+ ),
19
+ { code: "OFFICIAL_TEST_PACKAGE_MISMATCH" },
20
+ );
21
+
22
+ export function verifyOfficialTestBytes(bytes) {
23
+ if (
24
+ createHash("sha256").update(bytes).digest("hex") !==
25
+ OFFICIAL_TEST_PACKAGE.sha256
26
+ )
27
+ throw mismatch();
28
+ }
29
+ export function verifyOfficialTestDigest(digest) {
30
+ if (digest !== OFFICIAL_TEST_PACKAGE.sha256) throw mismatch();
31
+ }
32
+ // A saved test may recover using its original locally verified material. This
33
+ // never selects a replacement package or grants permission for another purchase.
34
+ // The caller must still validate the checkpoint against local invocation history.
35
+ export function verifyOfficialTestSelection(skill, checkpoint, savedDigest) {
36
+ if (!skill) return;
37
+ if (skill.bundle?.locallyModified) throw mismatch();
38
+ if (skill.bundle?.sha256 === OFFICIAL_TEST_PACKAGE.sha256) return;
39
+ if (checkpoint && skill.bundle?.sha256 === savedDigest) return;
40
+ throw mismatch();
41
+ }