@c4roa0/agentpay-user 0.0.0-stage → 0.3.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 (173) 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 +84 -2
  6. package/SECURITY.md +16 -0
  7. package/doc/README.md +52 -0
  8. package/doc/TODO.md +97 -0
  9. package/doc/architecture.md +96 -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 +67 -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 +89 -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 +178 -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 +1231 -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 +306 -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/readiness-profile.mjs +30 -0
  145. package/src/renew-policy.mjs +605 -0
  146. package/src/repair-market-wallet.mjs +137 -0
  147. package/src/restore-wallet.mjs +255 -0
  148. package/src/result-cache-view.mjs +276 -0
  149. package/src/runtime-client.mjs +1145 -0
  150. package/src/runtime-connection-settings.mjs +101 -0
  151. package/src/runtime-recovery.mjs +667 -0
  152. package/src/runtime-status.mjs +32 -0
  153. package/src/safe-owner-anvil.mjs +115 -0
  154. package/src/safe-policy-plan.mjs +195 -0
  155. package/src/service-reference.mjs +233 -0
  156. package/src/service-url.mjs +52 -0
  157. package/src/settlement-response.mjs +69 -0
  158. package/src/setup-autostart.mjs +197 -0
  159. package/src/setup-demo.mjs +152 -0
  160. package/src/skill-bundle.mjs +142 -0
  161. package/src/skill-client.mjs +198 -0
  162. package/src/skill-install-prompt.mjs +31 -0
  163. package/src/skill-package.mjs +50 -0
  164. package/src/skill-zip.mjs +220 -0
  165. package/src/status-panel.mjs +589 -0
  166. package/src/transport.mjs +48 -0
  167. package/src/uninstall-local.mjs +299 -0
  168. package/src/user-cli.mjs +354 -0
  169. package/src/user-setup-guide.mjs +193 -0
  170. package/src/wallet-control.mjs +182 -0
  171. package/src/wallet-ipc-error.mjs +16 -0
  172. package/src/wallet-package.mjs +22 -0
  173. package/update-pair.json +6 -0
@@ -0,0 +1,38 @@
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 { rememberUserDataDir } from "./local-data-directory.mjs";
5
+ import { spawn } from "node:child_process";
6
+ import { join } from "node:path";
7
+ import { runInstall } from "./install-user.mjs";
8
+
9
+ export async function installAndSetup({
10
+ userDir,
11
+ market,
12
+ dataDir,
13
+ install = runInstall,
14
+ start = startGuide,
15
+ remember = rememberUserDataDir,
16
+ write = console.log,
17
+ }) {
18
+ const result = await install({ userDir, market, dataDir, write });
19
+ if (!result?.completed)
20
+ throw Error(
21
+ "Installation did not complete. Run the same Install command to resume.",
22
+ );
23
+ await remember({ userDir, dataDir });
24
+ return start({ userDir, dataDir });
25
+ }
26
+ function startGuide({ userDir, dataDir }) {
27
+ return new Promise((resolve, reject) => {
28
+ // Start only after the installed packages are verified. Keeping imports in the
29
+ // child avoids requiring ethers before the installer has installed it.
30
+ const child = spawn(
31
+ process.execPath,
32
+ [join(userDir, "scripts/user-setup.mjs"), "--guided", dataDir],
33
+ { stdio: "inherit" },
34
+ );
35
+ child.once("error", reject);
36
+ child.once("exit", (code) => resolve(code ?? 1));
37
+ });
38
+ }
@@ -0,0 +1,201 @@
1
+ import { walletPackage } from "./wallet-package.mjs";
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
5
+ import { NODE_REQUIREMENT, supportedNodeVersion } from "./node-version.mjs";
6
+ import { lstat, readFile, realpath } from "node:fs/promises";
7
+ import { dirname, join, resolve, relative, isAbsolute } from "node:path";
8
+ import { createRequire } from "node:module";
9
+
10
+ const inside = (parent, child) => {
11
+ const path = relative(parent, child);
12
+ return (
13
+ !path ||
14
+ (!path.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) &&
15
+ path !== ".." &&
16
+ !isAbsolute(path))
17
+ );
18
+ };
19
+ export async function checkLocalInstallation({
20
+ userDir,
21
+ dataDir,
22
+ nodeVersion = process.versions.node,
23
+ uid = process.getuid(),
24
+ }) {
25
+ userDir = resolve(userDir);
26
+ let walletDir = null;
27
+ const checks = [];
28
+ const add = (code, status, message, fix = null) =>
29
+ checks.push({ code, status, message, fix });
30
+ add(
31
+ "NODE_VERSION",
32
+ supportedNodeVersion(nodeVersion) ? "passed" : "failed",
33
+ `Node ${nodeVersion}; requires ${NODE_REQUIREMENT}`,
34
+ `Install ${NODE_REQUIREMENT}`,
35
+ );
36
+ async function repository(path, name, label) {
37
+ try {
38
+ const manifest = JSON.parse(
39
+ await readFile(join(path, "package.json"), "utf8"),
40
+ );
41
+ if (manifest.name !== name) throw new Error("wrong package");
42
+ add(`${label}_REPOSITORY`, "passed", `${label} repository found`);
43
+ return manifest;
44
+ } catch {
45
+ add(
46
+ `${label}_REPOSITORY`,
47
+ "failed",
48
+ `${label} repository missing or invalid`,
49
+ "Reinstall this User version and its pinned npm dependencies",
50
+ );
51
+ return null;
52
+ }
53
+ }
54
+ const user = await repository(userDir, "@c4roa0/agentpay-user", "USER");
55
+ try {
56
+ const wallet = walletPackage(userDir);
57
+ walletDir = wallet.directory;
58
+ for (const entry of [wallet.cli, join(walletDir, 'lib/client.js'), join(walletDir, 'lib/local.js')]) {
59
+ if (!(await lstat(entry)).isFile()) throw Error('Missing compiled Wallet entry');
60
+ }
61
+ add('WALLET_PACKAGE', 'passed', `Wallet ${wallet.manifest.version} matches the installed User`);
62
+ } catch (error) {
63
+ add('WALLET_PACKAGE', 'failed', 'Compatible Wallet package is missing or incomplete', 'Reinstall the reviewed User version, then run agentpay repair. Source checkout: npm ci.');
64
+ }
65
+ if (user) {
66
+ const require = createRequire(join(userDir, 'package.json'));
67
+ for (const dependency of Object.keys(user.dependencies || {})) {
68
+ try {
69
+ try { require.resolve(dependency); }
70
+ catch (error) {
71
+ if (error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error;
72
+ let found = false;
73
+ for (const base of require.resolve.paths(dependency) || []) {
74
+ try {
75
+ const dir = join(base, dependency);
76
+ const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'));
77
+ const entry = pkg.exports?.['.']?.import;
78
+ if (pkg.name !== dependency || typeof entry !== 'string' || !entry.startsWith('./') || entry.includes('..')) continue;
79
+ if (!(await lstat(join(dir, entry))).isFile()) continue;
80
+ found = true; break;
81
+ } catch {}
82
+ }
83
+ if (!found) throw error;
84
+ }
85
+ add(`USER_DEPENDENCY_${dependency}`, 'passed', `${dependency} resolves without loading code`);
86
+ } catch (error) {
87
+ add(`USER_DEPENDENCY_${dependency}`, 'failed', `${dependency} is not installed or resolvable (${error.code || 'INVALID_PACKAGE_ENTRY'})`, 'Reinstall this User version. Source checkout: npm ci.');
88
+ }
89
+ }
90
+ }
91
+ dataDir = resolve(dataDir);
92
+ let privatePath = dataDir;
93
+ try {
94
+ // Resolve existing ancestors, including symlinks, even for a new directory.
95
+ let existing = dataDir;
96
+ const missing = [];
97
+ while (true) {
98
+ try {
99
+ privatePath = resolve(await realpath(existing), ...missing.reverse());
100
+ break;
101
+ } catch (error) {
102
+ if (error.code !== "ENOENT") throw error;
103
+ if (existing === dirname(existing)) throw error;
104
+ missing.push(existing.slice(dirname(existing).length + 1));
105
+ existing = dirname(existing);
106
+ }
107
+ }
108
+ const roots = await Promise.all(
109
+ [userDir, walletDir].filter(Boolean).map((path) =>
110
+ realpath(path).catch(() => resolve(path)),
111
+ ),
112
+ );
113
+ if (roots.some((root) => inside(root, privatePath))) {
114
+ add(
115
+ "PRIVATE_PATH",
116
+ "failed",
117
+ "Private data must be outside user and wallet repositories",
118
+ "Choose an owner-held private directory outside both repositories",
119
+ );
120
+ } else {
121
+ const targets = [
122
+ dataDir,
123
+ join(dataDir, "wallet"),
124
+ join(dataDir, "runtime"),
125
+ ];
126
+ for (const target of targets) {
127
+ try {
128
+ const info = await lstat(target);
129
+ const valid =
130
+ info.isDirectory() &&
131
+ !info.isSymbolicLink() &&
132
+ info.uid === uid &&
133
+ !(info.mode & 0o077);
134
+ add(
135
+ `PRIVATE_DIRECTORY_${relative(dataDir, target) || "root"}`,
136
+ valid ? "passed" : "failed",
137
+ valid
138
+ ? "Private directory permissions checked"
139
+ : "Private directory must be owner-held, non-symlink and 0700-or-stricter",
140
+ "Inspect ownership/type; choose or secure your own private directory before installation",
141
+ );
142
+ } catch (error) {
143
+ add(
144
+ `PRIVATE_DIRECTORY_${relative(dataDir, target) || "root"}`,
145
+ error.code === "ENOENT" ? "pending" : "failed",
146
+ error.code === "ENOENT"
147
+ ? "Not created yet; setup install will create this directory"
148
+ : "Private directory could not be inspected",
149
+ );
150
+ }
151
+ }
152
+ }
153
+ } catch {
154
+ add(
155
+ "PRIVATE_PATH",
156
+ "failed",
157
+ "Private path could not be resolved",
158
+ "Check path access and symbolic links",
159
+ );
160
+ }
161
+ // Advisory inventory only; do not migrate or discard existing requests during setup.
162
+ if (!checks.some((check) => check.status === "failed")) {
163
+ try {
164
+ const { createInvocationStore } = await import("./invocation-store.mjs");
165
+ const entries = Object.values(
166
+ await createInvocationStore(
167
+ join(dataDir, "runtime/invocations.json"),
168
+ ).inventory(),
169
+ );
170
+ const legacy = entries.filter(
171
+ (entry) => entry.routingVersion !== "agentpay.runtime-call.v1",
172
+ ).length;
173
+ add(
174
+ "INVOCATION_HISTORY",
175
+ legacy ? "warning" : "passed",
176
+ legacy
177
+ ? `${legacy} older request(s) are preserved. Runtime can start; check agentpay status after unlocking Wallet.`
178
+ : "Local invocation history can be inspected without migration",
179
+ );
180
+ } catch {
181
+ add(
182
+ "INVOCATION_HISTORY",
183
+ "failed",
184
+ "Local invocation history needs review; existing data was preserved",
185
+ "Start Runtime for its status view; do not delete history or repeat purchases",
186
+ );
187
+ }
188
+ }
189
+ return {
190
+ ok: !checks.some((check) => check.status === "failed"),
191
+ checks,
192
+ userDir,
193
+ walletDir,
194
+ dataDir,
195
+ readyToPurchase: false,
196
+ scope: "read_only_installation_preflight",
197
+ networkChecked: false,
198
+ message:
199
+ "This checks local prerequisites only; Wallet package compatibility is checked; keys, Policy and running-process readiness are not verified",
200
+ };
201
+ }
@@ -0,0 +1,279 @@
1
+ import { installationDirectory, importSetupFiles } from "./installation-paths.mjs";
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
5
+ import { NODE_REQUIREMENT, supportedNodeVersion } from "./node-version.mjs";
6
+ import { spawn } from "node:child_process";
7
+ import { createHash, randomUUID } from "node:crypto";
8
+ import {
9
+ appendFile,
10
+ lstat,
11
+ mkdir,
12
+ open,
13
+ readFile,
14
+ readdir,
15
+ rename,
16
+ unlink,
17
+ } from "node:fs/promises";
18
+ import { dirname, join, resolve } from "node:path";
19
+ import { checkLocalInstallation } from "./install-preflight.mjs";
20
+
21
+ export function marketSettingsUrl(value) {
22
+ const url = new URL(value);
23
+ if (
24
+ url.username ||
25
+ url.password ||
26
+ url.search ||
27
+ url.hash ||
28
+ url.pathname !== "/" ||
29
+ !(
30
+ url.protocol === "https:" ||
31
+ (url.protocol === "http:" &&
32
+ ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))
33
+ )
34
+ )
35
+ throw Error(
36
+ "Use the Market HTTPS origin, or a loopback HTTP origin for private rehearsal.",
37
+ );
38
+ return new URL("/market/web/onboarding/user-config", url).href;
39
+ }
40
+ export async function fingerprint(root, paths) {
41
+ const hash = createHash("sha256");
42
+ async function visit(name) {
43
+ const full = join(root, name),
44
+ info = await lstat(full);
45
+ if (info.isSymbolicLink())
46
+ throw Error(`Inspect the symbolic link before installing: ${full}`);
47
+ if (info.isDirectory()) {
48
+ for (const child of (await readdir(full)).sort())
49
+ await visit(join(name, child));
50
+ } else if (info.isFile()) {
51
+ hash.update(name);
52
+ hash.update("\0");
53
+ hash.update(await readFile(full));
54
+ hash.update("\0");
55
+ } else throw Error(`Unexpected installation input: ${full}`);
56
+ }
57
+ for (const path of paths) await visit(path);
58
+ return hash.digest("hex");
59
+ }
60
+ async function saveJson(path, value) {
61
+ const temp = `${path}.${randomUUID()}.tmp`;
62
+ const file = await open(temp, "wx", 0o600);
63
+ try {
64
+ await file.writeFile(JSON.stringify(value, null, 2) + "\n");
65
+ await file.sync();
66
+ await file.close();
67
+ await rename(temp, path);
68
+ } finally {
69
+ await file.close().catch(() => {});
70
+ await unlink(temp).catch(() => {});
71
+ }
72
+ }
73
+ export async function runInstall({
74
+ userDir,
75
+ market,
76
+ dataDir,
77
+ run = runCommand,
78
+ fetchImpl = fetch,
79
+ write = console.log,
80
+ nodeVersion = process.versions.node,
81
+ prepareOnly = false,
82
+ beforePrepare,
83
+ afterPrepare,
84
+ }) {
85
+ userDir = resolve(userDir);
86
+
87
+ const endpoint = prepareOnly ? null : marketSettingsUrl(market);
88
+ if (typeof process.getuid !== "function")
89
+ throw Error(
90
+ "This installer needs POSIX file ownership support. Native Windows installation is not available; use a supported Linux environment.",
91
+ );
92
+ if (!supportedNodeVersion(nodeVersion))
93
+ throw Error(
94
+ `Install ${NODE_REQUIREMENT} first. Detected Node ${nodeVersion}.`,
95
+ );
96
+ const pkg = JSON.parse(await readFile(join(userDir, 'package.json'), 'utf8'));
97
+ if (pkg.name !== '@c4roa0/agentpay-user') throw Error('Unexpected User package.');
98
+ const privateDir = resolve(dataDir), stateDir = installationDirectory();
99
+ await mkdir(stateDir, {recursive:true, mode:0o700});
100
+ const stateInfo = await lstat(stateDir);
101
+ if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink() || stateInfo.uid !== process.getuid() || stateInfo.mode & 0o077)
102
+ throw Error('Installation state directory must be owner-held and private.');
103
+ const lockPath = join(stateDir, "install.lock");
104
+ let lock;
105
+ try {
106
+ lock = await open(lockPath, "wx", 0o600);
107
+ } catch (e) {
108
+ if (e.code === "EEXIST")
109
+ throw Error(
110
+ `Another installation may be running. Check ${lockPath} before removing a stale lock.`,
111
+ );
112
+ throw e;
113
+ }
114
+ const logPath = join(stateDir, `install-${Date.now()}-${randomUUID()}.log`),
115
+ statePath = join(stateDir, "state.json");
116
+ let state = { version: 1 };
117
+ try {
118
+ try {
119
+ const info = await lstat(statePath);
120
+ if (
121
+ !info.isFile() ||
122
+ info.isSymbolicLink() ||
123
+ info.uid !== process.getuid() ||
124
+ info.mode & 0o077
125
+ )
126
+ throw Error("Unsafe install checkpoint");
127
+ state = JSON.parse(await readFile(statePath, "utf8"));
128
+ if (state.version !== 1) throw Error("Unknown install checkpoint");
129
+ } catch (e) {
130
+ if (e.code !== "ENOENT") throw e;
131
+ }
132
+ const initial = await checkLocalInstallation({
133
+ userDir,
134
+ dataDir: privateDir,
135
+ nodeVersion,
136
+ });
137
+ const pathErrors = initial.checks.filter(
138
+ (c) =>
139
+ c.status === "failed" &&
140
+ (c.code === "PRIVATE_PATH" || c.code.startsWith("PRIVATE_DIRECTORY_")),
141
+ );
142
+ if (pathErrors.length)
143
+ throw Error(`${pathErrors[0].message}. ${pathErrors[0].fix || ""}`);
144
+ // Update service handoff runs inside the same installer lock as dependency writes.
145
+ const preparationContext = {
146
+ logPath,
147
+ state,
148
+ initial,
149
+ saveState: () => saveJson(statePath, state),
150
+ };
151
+ if (
152
+ prepareOnly &&
153
+ beforePrepare &&
154
+ (await beforePrepare(preparationContext)) === false
155
+ )
156
+ return { completed: true, skipped: true, logPath };
157
+ // npm installations already contain their dependencies. A source checkout may
158
+ // install its frozen dependency tree, but never compiles or edits Wallet source.
159
+ const lockfile = join(userDir, 'package-lock.json');
160
+ let sourceInstall = false;
161
+ try { sourceInstall = (await lstat(lockfile)).isFile(); } catch (e) { if(e.code !== 'ENOENT') throw e; }
162
+ const inputs = sourceInstall ? await fingerprint(userDir, ['package.json', 'package-lock.json']) : null;
163
+ if (sourceInstall && (state.userDependencies !== inputs || !initial.ok)) {
164
+ write('1/2 Install the pinned User and Wallet dependencies. This may take a few minutes.');
165
+ await run('npm', ['ci'], {cwd:userDir, logPath});
166
+ state.userDependencies = inputs;
167
+ } else write('1/2 Check installed User and Wallet packages.');
168
+ const checked = await checkLocalInstallation({
169
+ userDir,
170
+ dataDir: privateDir,
171
+ nodeVersion,
172
+ });
173
+ if (!checked.ok) {
174
+ const failed = checked.checks.find((c) => c.status === "failed");
175
+ throw Error(`${failed.message}. ${failed.fix || ""}`);
176
+ }
177
+ await mkdir(privateDir, {recursive:true, mode:0o700});
178
+ await importSetupFiles(privateDir, userDir);
179
+ state.dataDir = privateDir;
180
+ await saveJson(statePath, state);
181
+ if (prepareOnly) {
182
+ if (afterPrepare) await afterPrepare(preparationContext);
183
+ return { completed: true, logPath };
184
+ }
185
+ write("2/2 Download public Market settings.");
186
+ let response;
187
+ try {
188
+ response = await fetchImpl(endpoint, {
189
+ redirect: "error",
190
+ signal: AbortSignal.timeout(15000),
191
+ });
192
+ } catch (error) {
193
+ const code =
194
+ error.cause?.code || error.code || error.name || "NETWORK_ERROR";
195
+ const safeCode = String(code)
196
+ .replace(/[^A-Za-z0-9_]/g, "")
197
+ .slice(0, 64);
198
+ await appendFile(
199
+ logPath,
200
+ `Market settings request failed: ${endpoint} (${safeCode})\n`,
201
+ );
202
+ throw Error(
203
+ `Cannot download Market settings from ${endpoint} (${safeCode}). Check the Market connection; for private rehearsal, keep the website SSH tunnel open and try 127.0.0.1 instead of localhost.`,
204
+ );
205
+ }
206
+ if (!response.ok)
207
+ throw Error(
208
+ `Market settings unavailable (HTTP ${response.status}). Retry the same Install command later.`,
209
+ );
210
+ const chunks = [];
211
+ let bytes = 0;
212
+ if (!response.body) throw Error("Empty Market settings response.");
213
+ for await (const chunk of response.body) {
214
+ bytes += chunk.length;
215
+ if (bytes > 16384) throw Error("Market settings response too large.");
216
+ chunks.push(Buffer.from(chunk));
217
+ }
218
+ const raw = Buffer.concat(chunks).toString("utf8");
219
+ const { validatePublicDeployment } = await import(
220
+ "./prepare-onboarding.mjs"
221
+ );
222
+ const config = validatePublicDeployment(JSON.parse(raw));
223
+ await saveJson(join(privateDir, "user-deployment.json"), config);
224
+ write(
225
+ "User, Wallet and Market settings are ready. Continuing to setup.",
226
+ );
227
+ return { completed: true, logPath };
228
+ } catch (error) {
229
+ await appendFile(logPath, `Installation stopped: ${error.message}\n`, {
230
+ mode: 0o600,
231
+ }).catch(() => {});
232
+ write(
233
+ `Installation stopped. ${error.message}\nAfter resolving the cause, rerun the same Install command. Log: ${logPath}`,
234
+ );
235
+ error.messageDisplayed = true;
236
+ error.logPath = logPath;
237
+ throw error;
238
+ } finally {
239
+ await lock.close();
240
+ await unlink(lockPath);
241
+ }
242
+ }
243
+ async function runCommand(command, args, { cwd, logPath }) {
244
+ const log = await open(logPath, "a", 0o600);
245
+ try {
246
+ await new Promise((resolve, reject) => {
247
+ const child = spawn(command, args, {
248
+ cwd,
249
+ env: {
250
+ ...process.env,
251
+ PATH:
252
+ dirname(process.execPath) +
253
+ (process.platform === "win32" ? ";" : ":") +
254
+ (process.env.PATH || ""),
255
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
256
+ },
257
+ stdio: ["ignore", log.fd, log.fd],
258
+ });
259
+ child.once("error", (e) =>
260
+ reject(
261
+ Error(
262
+ `${command} could not start (${e.code}). Check that it is installed and on PATH.`,
263
+ ),
264
+ ),
265
+ );
266
+ child.once("exit", (code) =>
267
+ code === 0
268
+ ? resolve()
269
+ : reject(
270
+ Error(
271
+ `${command} ${args.join(" ")} failed (exit ${code}). See the install log.`,
272
+ ),
273
+ ),
274
+ );
275
+ });
276
+ } finally {
277
+ await log.close();
278
+ }
279
+ }
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ // Program upgrades never own or replace private state. XDG_STATE_HOME is standard
3
+ // per-user state storage; explicit data-directory settings still select key/history data.
4
+ import { homedir } from 'node:os';
5
+ import { isAbsolute, join, dirname } from 'node:path';
6
+ import { lstat, mkdir, copyFile, constants } from 'node:fs/promises';
7
+ export function installationDirectory() {
8
+ const base = process.env.XDG_STATE_HOME || join(homedir(), '.local/state');
9
+ if (!isAbsolute(base)) throw Error('XDG_STATE_HOME must be an absolute path.');
10
+ return join(base, 'agentpay/user');
11
+ }
12
+ export function legacyInstallationDirectory(userDir) { return join(dirname(userDir), '.agentpay-install'); }
13
+ export async function deploymentFilePath(dataDir, userDir, name) {
14
+ if (!['anvil-onboarding.json', 'user-deployment.json'].includes(name)) throw Error('Invalid setup filename.');
15
+ const current = join(dataDir, name);
16
+ try { await lstat(current); return current; } catch (e) { if (e.code !== 'ENOENT') throw e; }
17
+ // Read-only fallback for an existing source installation. Never search unrelated machines.
18
+ return join(dirname(userDir), name);
19
+ }
20
+ export async function importSetupFiles(dataDir, userDir) {
21
+ for (const name of ['anvil-onboarding.json', 'user-deployment.json']) {
22
+ const source = await deploymentFilePath(dataDir, userDir, name);
23
+ if (source === join(dataDir, name)) continue;
24
+ let info;
25
+ try { info = await lstat(source); } catch (e) { if (e.code === 'ENOENT') continue; throw e; }
26
+ if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid() || (info.mode & 0o077) || info.size > 16384)
27
+ throw Error('Old setup configuration needs ownership/permission review before migration.');
28
+ await mkdir(dataDir, {recursive:true, mode:0o700});
29
+ await copyFile(source, join(dataDir,name), constants.COPYFILE_EXCL).catch(e => { if(e.code !== 'EEXIST') throw e; });
30
+ }
31
+ }
@@ -0,0 +1,148 @@
1
+ // Private result persistence. Received output and verified replayable output are distinct
2
+ // states; storing bytes alone does not establish delivery or final settlement.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
6
+ import { constants } from "node:fs";
7
+ import { open, mkdir, lstat, link, unlink } from "node:fs/promises";
8
+ import { join, dirname } from "node:path";
9
+ import { randomUUID } from "node:crypto";
10
+ function resultFiles(statePath, pending = false) {
11
+ const directory = statePath + (pending ? ".pending-results" : ".results");
12
+ const version = pending
13
+ ? "agentpay.pending-result.v1"
14
+ : "agentpay.invocation-result.v1";
15
+ const name = (id) => {
16
+ if (!/^0x[0-9a-f]{64}$/.test(id || ""))
17
+ throw Error("INVOCATION_RESULT_ID_INVALID");
18
+ return join(directory, id + ".json");
19
+ };
20
+ return {
21
+ async read(invocation) {
22
+ try {
23
+ const dir = await lstat(directory);
24
+ if (
25
+ !dir.isDirectory() ||
26
+ dir.isSymbolicLink() ||
27
+ dir.uid !== process.getuid() ||
28
+ dir.mode & 0o077
29
+ )
30
+ throw Error("INVOCATION_RESULT_UNSAFE");
31
+ } catch (error) {
32
+ if (error.code === "ENOENT") return null;
33
+ throw error;
34
+ }
35
+
36
+ let file;
37
+ try {
38
+ file = await open(
39
+ name(invocation.invocationId),
40
+ constants.O_RDONLY | constants.O_NOFOLLOW,
41
+ );
42
+ } catch (error) {
43
+ if (error.code === "ENOENT") return null;
44
+ throw error;
45
+ }
46
+ let value;
47
+ try {
48
+ const st = await file.stat();
49
+ if (
50
+ !st.isFile() ||
51
+ st.uid !== process.getuid() ||
52
+ st.mode & 0o077 ||
53
+ st.size > 2 * 1048576
54
+ )
55
+ throw Error("INVOCATION_RESULT_UNSAFE");
56
+ value = JSON.parse(await file.readFile("utf8"));
57
+ } finally {
58
+ await file.close();
59
+ }
60
+ if (
61
+ value?.version !== version ||
62
+ value.invocationId !== invocation.invocationId ||
63
+ value.requestHash !== invocation.requestHash ||
64
+ value.serviceBinding !== invocation.serviceBinding ||
65
+ !value.result ||
66
+ typeof value.result !== "object" ||
67
+ Array.isArray(value.result)
68
+ )
69
+ throw Error("INVOCATION_RESULT_CONFLICT");
70
+ return value.result;
71
+ },
72
+ async save(invocation, result) {
73
+ const content =
74
+ JSON.stringify({
75
+ version,
76
+ invocationId: invocation.invocationId,
77
+ requestHash: invocation.requestHash,
78
+ serviceBinding: invocation.serviceBinding,
79
+ result,
80
+ }) + "\n";
81
+ if (Buffer.byteLength(content) > 2 * 1048576)
82
+ throw Error("INVOCATION_RESULT_TOO_LARGE");
83
+ await mkdir(directory, { mode: 0o700 }).catch((error) => {
84
+ if (error.code !== "EEXIST") throw error;
85
+ });
86
+ const st = await lstat(directory);
87
+ if (
88
+ !st.isDirectory() ||
89
+ st.isSymbolicLink() ||
90
+ st.uid !== process.getuid() ||
91
+ st.mode & 0o077
92
+ )
93
+ throw Error("INVOCATION_RESULT_UNSAFE");
94
+ const parent = await open(dirname(directory), "r");
95
+ try {
96
+ await parent.sync();
97
+ } finally {
98
+ await parent.close();
99
+ }
100
+ const target = name(invocation.invocationId),
101
+ temporary = join(directory, ".result-" + randomUUID());
102
+ const file = await open(temporary, "wx", 0o600);
103
+ try {
104
+ try {
105
+ await file.writeFile(content);
106
+ await file.sync();
107
+ } finally {
108
+ await file.close();
109
+ }
110
+ await link(temporary, target).catch(async (error) => {
111
+ if (error.code !== "EEXIST") throw error;
112
+ const existing = await this.read(invocation);
113
+ const immutable = (v) => {
114
+ if (pending) return v;
115
+ const { finalization, deadlines, ...content } = v;
116
+ return content;
117
+ };
118
+ if (
119
+ JSON.stringify(immutable(existing)) !==
120
+ JSON.stringify(immutable(result))
121
+ )
122
+ throw Error("INVOCATION_RESULT_CONFLICT");
123
+ });
124
+ const dir = await open(directory, "r");
125
+ try {
126
+ await dir.sync();
127
+ } finally {
128
+ await dir.close();
129
+ }
130
+ } finally {
131
+ await unlink(temporary).catch((error) => {
132
+ if (error.code !== "ENOENT") throw error;
133
+ });
134
+ }
135
+ },
136
+ };
137
+ }
138
+
139
+ export function invocationResults(statePath) {
140
+ const verified = resultFiles(statePath),
141
+ pending = resultFiles(statePath, true);
142
+ return {
143
+ read: verified.read.bind(verified),
144
+ save: verified.save.bind(verified),
145
+ readPending: pending.read.bind(pending),
146
+ savePending: pending.save.bind(pending),
147
+ };
148
+ }