@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,156 @@
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 { constants } from "node:fs";
5
+ import { lstat, open, rename, unlink } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+ import { createConnection } from "node:net";
9
+ export function serviceSettings(input) {
10
+ if (
11
+ !input ||
12
+ Object.keys(input).some(
13
+ (k) =>
14
+ !["version", "runtimeUrl", "settlementUrl", "startupChoice"].includes(
15
+ k,
16
+ ),
17
+ ) ||
18
+ input.version !== 1
19
+ )
20
+ throw Error("Invalid local service settings.");
21
+ if (
22
+ input.startupChoice !== undefined &&
23
+ !["enabled", "disabled"].includes(input.startupChoice)
24
+ )
25
+ throw Error("Invalid startup preference.");
26
+ const runtime = new URL(input.runtimeUrl),
27
+ settlement = new URL(input.settlementUrl);
28
+ if (
29
+ runtime.protocol !== "http:" ||
30
+ runtime.hostname !== "127.0.0.1" ||
31
+ !runtime.port ||
32
+ runtime.pathname !== "/" ||
33
+ runtime.search ||
34
+ runtime.hash ||
35
+ runtime.username ||
36
+ runtime.password
37
+ )
38
+ throw Error("Runtime needs a loopback HTTP origin with an explicit port.");
39
+ if (
40
+ settlement.username ||
41
+ settlement.password ||
42
+ settlement.search ||
43
+ settlement.hash ||
44
+ !(
45
+ settlement.protocol === "https:" ||
46
+ (settlement.protocol === "http:" &&
47
+ ["127.0.0.1", "localhost", "[::1]"].includes(settlement.hostname))
48
+ )
49
+ )
50
+ throw Error("Settlement needs HTTPS or a local rehearsal URL.");
51
+ return {
52
+ version: 1,
53
+ runtimeUrl: runtime.origin,
54
+ settlementUrl: settlement.href.replace(/\/+$/, ""),
55
+ ...(input.startupChoice ? { startupChoice: input.startupChoice } : {}),
56
+ };
57
+ }
58
+ function owned(info) {
59
+ return (
60
+ info.uid === process.getuid() &&
61
+ (info.mode & 0o077) === 0 &&
62
+ !info.isSymbolicLink()
63
+ );
64
+ }
65
+ export async function checkServiceDirectory(dataDir) {
66
+ for (const dir of [
67
+ dataDir,
68
+ join(dataDir, "wallet"),
69
+ join(dataDir, "runtime"),
70
+ ]) {
71
+ const info = await lstat(dir);
72
+ if (!info.isDirectory() || !owned(info))
73
+ throw Error(
74
+ "Local service directories must be private and owned by you.",
75
+ );
76
+ }
77
+ }
78
+ export async function saveServiceSettings({
79
+ dataDir,
80
+ runtimeUrl,
81
+ settlementUrl,
82
+ startupChoice,
83
+ }) {
84
+ await checkServiceDirectory(dataDir);
85
+ let previous;
86
+ try {
87
+ previous = await readServiceSettings(dataDir);
88
+ } catch (e) {
89
+ if (e.code !== "ENOENT") throw e;
90
+ }
91
+ const choice = startupChoice ?? previous?.startupChoice;
92
+ const settings = serviceSettings({
93
+ version: 1,
94
+ runtimeUrl,
95
+ settlementUrl,
96
+ ...(choice ? { startupChoice: choice } : {}),
97
+ });
98
+ const temp = join(dataDir, "runtime", `services-${randomUUID()}.tmp`);
99
+ let handle;
100
+ try {
101
+ handle = await open(temp, "wx", 0o600);
102
+ await handle.writeFile(JSON.stringify(settings) + "\n");
103
+ await handle.sync();
104
+ await handle.close();
105
+ handle = null;
106
+ await rename(temp, join(dataDir, "runtime", "local-services.json"));
107
+ } finally {
108
+ await handle?.close();
109
+ await unlink(temp).catch(() => {});
110
+ }
111
+ }
112
+ export async function readServiceSettings(dataDir) {
113
+ await checkServiceDirectory(dataDir);
114
+ const file = await open(
115
+ join(dataDir, "runtime", "local-services.json"),
116
+ constants.O_RDONLY | constants.O_NOFOLLOW,
117
+ );
118
+ try {
119
+ const info = await file.stat();
120
+ if (!info.isFile() || !owned(info) || info.size > 4096)
121
+ throw Error("Invalid local service settings file.");
122
+ return serviceSettings(JSON.parse(await file.readFile("utf8")));
123
+ } finally {
124
+ await file.close();
125
+ }
126
+ }
127
+ export function runtimeEnvironment(dataDir, settings) {
128
+ return {
129
+ AGENTPAY_USER_HOST: "127.0.0.1",
130
+ AGENTPAY_USER_PORT: new URL(settings.runtimeUrl).port,
131
+ AGENTPAY_WALLET_SOCKET: join(dataDir, "wallet", "wallet.sock"),
132
+ AGENTPAY_WALLET_AGENT_TOKEN_FILE: join(dataDir, "wallet", "agent.token"),
133
+ AGENTPAY_USER_CALLER_TOKEN_FILE: join(dataDir, "runtime", "caller.token"),
134
+ AGENTPAY_USER_STATE_FILE: join(dataDir, "runtime", "invocations.json"),
135
+ AGENTPAY_SETTLEMENT_URL: settings.settlementUrl,
136
+ };
137
+ }
138
+ export function listenerPresent(address) {
139
+ return new Promise((resolve, reject) => {
140
+ const socket = createConnection(address);
141
+ socket.setTimeout(2000);
142
+ socket.once("connect", () => {
143
+ socket.destroy();
144
+ resolve(true);
145
+ });
146
+ socket.once("error", (e) => {
147
+ socket.destroy();
148
+ if (["ENOENT", "ECONNREFUSED"].includes(e.code)) resolve(false);
149
+ else reject(Error("Could not safely check the existing listener."));
150
+ });
151
+ socket.once("timeout", () => {
152
+ socket.destroy();
153
+ reject(Error("Existing listener check timed out."));
154
+ });
155
+ });
156
+ }
@@ -0,0 +1,36 @@
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 {
5
+ assertUnixMilliseconds,
6
+ parseLegacyIsoMilliseconds,
7
+ unixSecondsToMilliseconds,
8
+ } from "@c4roa0/agentpay-protocol";
9
+ export const currentTimeMs = () => assertUnixMilliseconds(Date.now());
10
+ // Explicit migration adapter for known local fields only; never inspect signed payloads.
11
+ export function localTimeMs(entry, key) {
12
+ if (Object.hasOwn(entry, key + "Ms")) {
13
+ const ms = assertUnixMilliseconds(entry[key + "Ms"]);
14
+ if (
15
+ Object.hasOwn(entry, key) &&
16
+ parseLegacyIsoMilliseconds(entry[key]) !== ms
17
+ )
18
+ throw Error("LOCAL_TIME_CONFLICT");
19
+ return ms;
20
+ }
21
+ return parseLegacyIsoMilliseconds(entry[key]);
22
+ }
23
+ export function displayTime(ms) {
24
+ assertUnixMilliseconds(ms);
25
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
26
+ return (
27
+ new Intl.DateTimeFormat(undefined, {
28
+ dateStyle: "medium",
29
+ timeStyle: "long",
30
+ timeZone: zone,
31
+ }).format(ms) + ` (${zone})`
32
+ );
33
+ }
34
+ export function displaySeconds(seconds) {
35
+ return displayTime(unixSecondsToMilliseconds(seconds));
36
+ }
@@ -0,0 +1,316 @@
1
+ import { requireRunningSoftware } from "./software-versions.mjs";
2
+ import { walletPackage } from "./wallet-package.mjs";
3
+ // Apply installed packages. Preserve identity/history and startup preferences;
4
+ // preparation failure must not enable a mismatched Wallet/Runtime pair.
5
+ // SPDX-License-Identifier: MPL-2.0
6
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
7
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
8
+ import { ensureRuntimeConnection } from "./runtime-connection-settings.mjs";
9
+ import { prepareOfficialTestUpdate } from "./official-test-update.mjs";
10
+ import { readFile, open, lstat } from "node:fs/promises";
11
+ import { dirname, join } from "node:path";
12
+ import { spawn } from "node:child_process";
13
+ import {
14
+ installedUserDir,
15
+ resolveUserDataDir,
16
+ } from "./local-data-directory.mjs";
17
+ import {
18
+ checkServiceDirectory,
19
+ readServiceSettings,
20
+ } from "./local-services.mjs";
21
+ import { createInterface } from "node:readline/promises";
22
+ import { createHash } from "node:crypto";
23
+ import { runInstall, fingerprint } from "./install-user.mjs";
24
+ import { localWalletAdmin } from "./local-wallet-ipc.mjs";
25
+ import { readRuntimeStatus } from "./runtime-status.mjs";
26
+
27
+ async function json(path) {
28
+ const info = await lstat(path);
29
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 65536)
30
+ throw Error("Update metadata must be a regular file.");
31
+ return JSON.parse(await readFile(path, "utf8"));
32
+ }
33
+ // The v1 metadata field typesDependency now pins the public npm Protocol package.
34
+ // Keep exact pair checks until the separate compatibility-range design is implemented.
35
+ export async function checkUpdatePair(userDir) {
36
+ const { directory: walletDir } = walletPackage(userDir);
37
+ const [pair, user, wallet] = await Promise.all([
38
+ json(join(userDir, "update-pair.json")),
39
+ json(join(userDir, "package.json")),
40
+ json(join(walletDir, "package.json")),
41
+ ]);
42
+ if (
43
+ pair.format !== "agentpay.local-update-pair.v1" ||
44
+ !/^\d+\.\d+\.\d+$/.test(pair.userVersion) ||
45
+ !/^\d+\.\d+\.\d+$/.test(pair.walletVersion) ||
46
+ typeof pair.typesDependency !== "string" ||
47
+ !pair.typesDependency ||
48
+ user.name !== "@c4roa0/agentpay-user" ||
49
+ wallet.name !== "@c4roa0/agentpay-wallet" ||
50
+ user.version !== pair.userVersion ||
51
+ wallet.version !== pair.walletVersion ||
52
+ user.dependencies?.["@c4roa0/agentpay-protocol"] !== pair.typesDependency ||
53
+ wallet.dependencies?.["@c4roa0/agentpay-protocol"] !== pair.typesDependency
54
+ ) {
55
+ throw Error(
56
+ "User and Wallet do not match the downloaded update. Reinstall the reviewed User package version, then retry agentpay repair.",
57
+ );
58
+ }
59
+ return pair;
60
+ }
61
+ export async function updateSnapshot(userDir) {
62
+ const { directory: walletDir } = walletPackage(userDir);
63
+ const inputs = createHash("sha256")
64
+ .update(
65
+ JSON.stringify([
66
+ userDir,
67
+ process.execPath,
68
+ process.versions.node,
69
+ await fingerprint(userDir, [
70
+ "package.json",
71
+ "update-pair.json",
72
+ "src",
73
+ "scripts",
74
+ ]),
75
+ await fingerprint(walletDir, [
76
+ "package.json",
77
+ "lib",
78
+ "bin",
79
+ ]),
80
+ ]),
81
+ )
82
+ .digest("hex");
83
+ let outputs = null;
84
+ try {
85
+ outputs = await fingerprint(walletDir, ["lib", "bin"]);
86
+ } catch (e) {
87
+ if (e.code !== "ENOENT") throw e;
88
+ }
89
+ return { inputs, outputs };
90
+ }
91
+ async function askUpdate(prompt) {
92
+ if (!process.stdin.isTTY)
93
+ throw Error(
94
+ "Run agentpay repair in a terminal to confirm, or use agentpay repair -f to skip confirmation.",
95
+ );
96
+ const terminal = createInterface({
97
+ input: process.stdin,
98
+ output: process.stdout,
99
+ });
100
+ try {
101
+ return await terminal.question(prompt + "\n> ");
102
+ } finally {
103
+ terminal.close();
104
+ }
105
+ }
106
+ async function manage(action, { userDir, dataDir, logPath }) {
107
+ const file = await open(logPath, "a", 0o600);
108
+ try {
109
+ await new Promise((done, reject) => {
110
+ const child = spawn(
111
+ process.execPath,
112
+ [join(userDir, "scripts/autostart.mjs"), action],
113
+ {
114
+ shell: false,
115
+ env: { ...process.env, AGENTPAY_USER_DATA_DIR: dataDir },
116
+ stdio: ["ignore", file.fd, file.fd],
117
+ },
118
+ );
119
+ child.once("error", reject);
120
+ child.once("exit", (code) =>
121
+ code === 0
122
+ ? done()
123
+ : reject(Error("Background service operation failed.")),
124
+ );
125
+ });
126
+ } finally {
127
+ await file.close();
128
+ }
129
+ }
130
+ export async function applyLocalUpdate({
131
+ userDir = installedUserDir,
132
+ dataDir,
133
+ write = console.log,
134
+ platform = process.platform,
135
+ force = false,
136
+ ask = askUpdate,
137
+ snapshot = updateSnapshot,
138
+ officialTest = prepareOfficialTestUpdate,
139
+ resolveDirectory = resolveUserDataDir,
140
+ pairCheck = checkUpdatePair,
141
+ checkDirectory = checkServiceDirectory,
142
+ settings = readServiceSettings,
143
+ connection = ensureRuntimeConnection,
144
+ prepare = runInstall,
145
+ service = manage,
146
+ wallet = localWalletAdmin,
147
+ runtime = readRuntimeStatus,
148
+ pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
149
+ } = {}) {
150
+ let phase = "preflight",
151
+ logPath,
152
+ selected;
153
+ try {
154
+ write(
155
+ "\n------------------------------------------------------------\n> Repair User and Wallet\n------------------------------------------------------------",
156
+ );
157
+ write(
158
+ "Checks the installed User and Wallet packages, refreshes service paths and checks the official test Skill.",
159
+ );
160
+ if (platform !== "linux")
161
+ throw Error(
162
+ "This repair command currently supports Linux user services. macOS startup support is not ready.",
163
+ );
164
+ dataDir ??= await resolveDirectory({ userDir });
165
+ const pair = await pairCheck(userDir);
166
+ await checkDirectory(dataDir);
167
+ await settings(dataDir);
168
+ write(
169
+ `1/3 Check local files · User ${pair.userVersion} / Wallet ${pair.walletVersion}`,
170
+ );
171
+ const result = await prepare({
172
+ userDir,
173
+ dataDir,
174
+ prepareOnly: true,
175
+
176
+ write: () => {},
177
+ beforePrepare: async (context) => {
178
+ logPath = context.logPath;
179
+ // Recheck after taking the shared installer lock, before stopping anything.
180
+ await pairCheck(userDir);
181
+ selected = await snapshot(userDir);
182
+ const applied = context.state.localUpdate;
183
+ if (
184
+ context.initial.ok &&
185
+ applied?.status === "complete" &&
186
+ applied.inputs === selected.inputs &&
187
+ applied.outputs === selected.outputs
188
+ ) {
189
+ let runningMatches = false;
190
+ try {
191
+ requireRunningSoftware(await wallet(dataDir, "status"), await runtime(dataDir), { ...pair, protocolVersion: pair.typesDependency });
192
+ runningMatches = true;
193
+ } catch { /* Keep repair available for old, stopped or unreported processes. */ }
194
+ if (runningMatches) {
195
+ await connection({ dataDir, write });
196
+ phase = "unchanged";
197
+ write("Local installation and running versions are up to date. Background services were not changed.");
198
+ return false;
199
+ }
200
+ write("Running versions need verification. Repair will restart the paired services.");
201
+ }
202
+ write(
203
+ applied?.status === "pending"
204
+ ? "A previous repair did not finish. Retry preparation."
205
+ : "Installed program changes need to be applied.",
206
+ );
207
+ write(
208
+ "Wallet and Runtime will pause, verify installed files and restart. Wallet will need unlocking afterwards.",
209
+ );
210
+ if (!force) {
211
+ while (true) {
212
+ const answer = (
213
+ await ask("Type 'confirm' to repair, or 'q' to cancel")
214
+ )
215
+ .trim()
216
+ .toLowerCase();
217
+ if (answer === "q") {
218
+ phase = "cancelled";
219
+ write("Repair cancelled. Background services were not changed.");
220
+ return false;
221
+ }
222
+ if (answer === "confirm") break;
223
+ write("Enter 'confirm' or 'q'.");
224
+ }
225
+ }
226
+ const confirmed = await snapshot(userDir);
227
+ if (JSON.stringify(confirmed) !== JSON.stringify(selected))
228
+ throw Error(
229
+ "Local files changed during confirmation. Run agentpay repair again.",
230
+ );
231
+ await connection({ dataDir, write, refreshClientModule: true, checkOnly: true });
232
+ context.state.localUpdate = {
233
+ status: "pending",
234
+ inputs: selected.inputs,
235
+ };
236
+ await context.saveState();
237
+ await service("prepare-repair", { userDir, dataDir, logPath });
238
+ phase = "stopping";
239
+ write(
240
+ "2/3 Pause background services and check installed packages. This may take a few minutes.",
241
+ );
242
+ await service("stop", { userDir, dataDir, logPath });
243
+ phase = "building";
244
+ },
245
+ afterPrepare: async (context) => {
246
+ logPath = context.logPath;
247
+ const built = await snapshot(userDir);
248
+ if (built.inputs !== selected.inputs)
249
+ throw Error("Installed files changed during repair.");
250
+ await connection({ dataDir, write, refreshClientModule: true });
251
+ phase = "starting";
252
+ write("3/3 Start Wallet and Runtime.");
253
+ await service("start", { userDir, dataDir, logPath });
254
+ let ready = false;
255
+ const deadline = Date.now() + 15000;
256
+ for (
257
+ let attempt = 0;
258
+ attempt < 20 && Date.now() < deadline;
259
+ attempt++
260
+ ) {
261
+ try {
262
+ const [w, r] = await Promise.all([
263
+ wallet(dataDir, "status"),
264
+ runtime(dataDir),
265
+ ]);
266
+ if (typeof w.locked === "boolean" && r.role === "user-runtime") {
267
+ requireRunningSoftware(w, r, { ...pair, protocolVersion: pair.typesDependency });
268
+ ready = true;
269
+ break;
270
+ }
271
+ } catch (error) {
272
+ if (error.code === "RUNNING_VERSION_UNVERIFIED") throw error;
273
+ /* Startup can take a moment; final failure is reported below. */
274
+ }
275
+ await pause(500);
276
+ }
277
+ if (!ready) throw Error("Service readiness could not be confirmed.");
278
+ context.state.localUpdate = { status: "complete", ...built };
279
+ await context.saveState();
280
+ phase = "ready";
281
+ },
282
+ });
283
+ if (result?.skipped && phase === "cancelled") return 0;
284
+ if (result?.skipped && phase === "unchanged") {
285
+ await officialTest({ userDir, dataDir, write });
286
+ return 0;
287
+ }
288
+ if (!result?.completed || phase !== "ready")
289
+ throw Error("Repair did not finish.");
290
+ write(
291
+ "\nRepair complete. Wallet and Runtime are running in the background.",
292
+ );
293
+ write(
294
+ "Keys, spending permission and Order records were kept. Login startup preference is unchanged.",
295
+ );
296
+ await officialTest({ userDir, dataDir, write });
297
+ write("Next: agentpay wallet unlock\nThen: agentpay status");
298
+ return 0;
299
+ } catch (error) {
300
+ if (phase === "preflight")
301
+ write("Repair stopped before pausing services. " + error.message);
302
+ else if (phase === "building")
303
+ write(
304
+ "Repair could not finish. Wallet and Runtime remain stopped; your local data was kept.",
305
+ );
306
+ else
307
+ write(
308
+ "Service changes could not be confirmed. Check agentpay status and agentpay service logs runtime before retrying.",
309
+ );
310
+ if (error.code === "RUNNING_VERSION_UNVERIFIED") write(error.message);
311
+ const log = error.logPath || logPath;
312
+ if (log) write("Details: " + log);
313
+ write("After resolving the problem, run agentpay repair again.");
314
+ return 1;
315
+ }
316
+ }
@@ -0,0 +1,93 @@
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 { checkServiceDirectory } from "./local-services.mjs";
5
+ import { constants } from "node:fs";
6
+ import { open } from "node:fs/promises";
7
+ import { createConnection } from "node:net";
8
+ import { join } from "node:path";
9
+ import { randomBytes } from "node:crypto";
10
+ export async function localWalletAdmin(dataDir, method, params = {}) {
11
+ await checkServiceDirectory(dataDir);
12
+ const h = await open(
13
+ join(dataDir, "wallet/admin.token"),
14
+ constants.O_RDONLY | constants.O_NOFOLLOW,
15
+ );
16
+ let token;
17
+ try {
18
+ const s = await h.stat();
19
+ if (
20
+ !s.isFile() ||
21
+ s.uid !== process.getuid() ||
22
+ s.mode & 0o077 ||
23
+ s.size > 4096
24
+ )
25
+ throw Error("Local Wallet access file is unsafe.");
26
+ token = (await h.readFile("utf8")).trim();
27
+ } finally {
28
+ await h.close();
29
+ }
30
+ return new Promise((resolve, reject) => {
31
+ const socket = createConnection(join(dataDir, "wallet/wallet.sock"));
32
+ let text = "",
33
+ done = false;
34
+ const finish = (e, value) => {
35
+ if (done) return;
36
+ done = true;
37
+ clearTimeout(timer);
38
+ socket.destroy();
39
+ e ? reject(e) : resolve(value);
40
+ };
41
+ const timer = setTimeout(
42
+ () => finish(Error("Wallet did not respond. Run agentpay status.")),
43
+ 30000,
44
+ );
45
+ socket.setEncoding("utf8");
46
+ socket.once("error", (e) =>
47
+ finish(
48
+ Object.assign(
49
+ Error(
50
+ "Start Wallet with agentpay service start, then run agentpay wallet unlock.",
51
+ ),
52
+ {
53
+ code: ["ENOENT", "ECONNREFUSED"].includes(e.code)
54
+ ? "WALLET_NOT_RUNNING"
55
+ : "WALLET_CONNECTION_FAILED",
56
+ },
57
+ ),
58
+ ),
59
+ );
60
+ socket.once("connect", () =>
61
+ socket.write(
62
+ JSON.stringify({
63
+ id: randomBytes(8).toString("hex"),
64
+ token,
65
+ method,
66
+ params,
67
+ }) + "\n",
68
+ ),
69
+ );
70
+ socket.on("data", (part) => {
71
+ text += part;
72
+ if (Buffer.byteLength(text) > 65536)
73
+ finish(Error("Wallet response was too large."));
74
+ });
75
+ socket.once("close", () => {
76
+ if (!done)
77
+ finish(Error("Wallet closed the connection. Check agentpay status."));
78
+ });
79
+ socket.once("end", () => {
80
+ try {
81
+ const r = JSON.parse(text);
82
+ if (!r.ok)
83
+ throw Object.assign(
84
+ Error(r.error?.message || "Wallet rejected the connection."),
85
+ { code: r.error?.code },
86
+ );
87
+ finish(null, r.result);
88
+ } catch (e) {
89
+ finish(e);
90
+ }
91
+ });
92
+ });
93
+ }
@@ -0,0 +1,28 @@
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
+ export async function remindLockedWallet({
5
+ env = process.env,
6
+ status,
7
+ notify,
8
+ pause = (ms) => new Promise((r) => setTimeout(r, ms)),
9
+ }) {
10
+ if (!env.DISPLAY && !env.WAYLAND_DISPLAY) return "headless";
11
+ // The graphical-session oneshot stays active until logout, independently of service restarts.
12
+ for (let i = 0; i < 15; i++) {
13
+ try {
14
+ const state = await status();
15
+ if (state?.locked === false) return "unlocked";
16
+ if (state?.locked === true) {
17
+ try {
18
+ await notify();
19
+ return "notified";
20
+ } catch {
21
+ return "unavailable";
22
+ }
23
+ }
24
+ } catch {}
25
+ if (i < 14) await pause(2000);
26
+ }
27
+ return "wallet_unavailable";
28
+ }
@@ -0,0 +1,21 @@
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
+ // Preserve the approved metadata URL's ingress prefix; metadata cannot select a host.
5
+ export function marketDownloadUrl(metadataUrl, serviceId) {
6
+ const url = new URL(metadataUrl);
7
+ const suffix = `/skills/bundles/${encodeURIComponent(serviceId)}/metadata`;
8
+ if (
9
+ url.username ||
10
+ url.password ||
11
+ url.search ||
12
+ url.hash ||
13
+ !url.pathname.endsWith(suffix)
14
+ )
15
+ throw Error("MARKET_METADATA_PATH_MISMATCH");
16
+ const prefix = url.pathname.slice(0, -suffix.length);
17
+ if (!["", "/market"].includes(prefix))
18
+ throw Error("MARKET_METADATA_PREFIX_INVALID");
19
+ url.pathname = prefix + suffix.replace(/\/metadata$/, "/download");
20
+ return url;
21
+ }
@@ -0,0 +1,12 @@
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
+ export const NODE_REQUIREMENT = "Node 22.22.3 or newer";
5
+ export function supportedNodeVersion(version) {
6
+ if (typeof version !== "string" || !/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version))
7
+ return false;
8
+ const [major, minor, patch] = version.split(".").map(Number);
9
+ return (
10
+ major > 22 || (major === 22 && (minor > 22 || (minor === 22 && patch >= 3)))
11
+ );
12
+ }