@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,605 @@
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
+ loadCurrentDeployment,
6
+ deploymentChanges,
7
+ syncSetupDeployment,
8
+ } from "./deployment-status.mjs";
9
+ import { approveOwnerOperations } from "./owner-approval-server.mjs";
10
+ import { selectedOwnerSigner, checkTestSigner } from "./owner-signer.mjs";
11
+ import { connectMarket } from "./connect-market.mjs";
12
+ import { revokeOnChain } from "./policy-operations.mjs";
13
+ import { constants } from "node:fs";
14
+ import { open, rename, unlink } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+ import { randomUUID } from "node:crypto";
17
+ import { createInterface } from "node:readline/promises";
18
+ import { resolveUserDataDir } from "./local-data-directory.mjs";
19
+ import { localWalletAdmin } from "./local-wallet-ipc.mjs";
20
+ import { controlWallet } from "./wallet-control.mjs";
21
+ import {
22
+ runPolicyTerminalGuide,
23
+ showAuthorizationSummary,
24
+ } from "./policy-terminal-guide.mjs";
25
+ import { buildSafePolicyPlan, inspectSafePolicy } from "./safe-policy-plan.mjs";
26
+ import { executeAnvilOwnerCall } from "./safe-owner-anvil.mjs";
27
+ import { JsonRpcProvider, Contract, Interface, formatUnits } from "ethers";
28
+ async function read(path) {
29
+ let h;
30
+ try {
31
+ h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
32
+ const st = await h.stat();
33
+ if (
34
+ !st.isFile() ||
35
+ st.uid !== process.getuid() ||
36
+ st.mode & 0o077 ||
37
+ st.size > 65536
38
+ )
39
+ throw Error("Unsafe saved authorization");
40
+ return JSON.parse(await h.readFile("utf8"));
41
+ } catch (e) {
42
+ if (e.code === "ENOENT") return null;
43
+ throw e;
44
+ } finally {
45
+ await h?.close();
46
+ }
47
+ }
48
+ async function save(path, value) {
49
+ const tmp = path + "." + randomUUID() + ".tmp";
50
+ let h;
51
+ try {
52
+ h = await open(tmp, "wx", 0o600);
53
+ await h.writeFile(JSON.stringify(value) + "\n");
54
+ await h.sync();
55
+ await h.close();
56
+ h = null;
57
+ await rename(tmp, path);
58
+ } finally {
59
+ await h?.close();
60
+ await unlink(tmp).catch(() => {});
61
+ }
62
+ }
63
+ async function askTerminal(prompt) {
64
+ if (!process.stdin.isTTY)
65
+ throw Error("Run agentpay policy renew in your terminal.");
66
+ const t = createInterface({ input: process.stdin, output: process.stdout });
67
+ try {
68
+ return await t.question(
69
+ prompt.trimEnd().endsWith(">") ? prompt : prompt + "\n> ",
70
+ );
71
+ } finally {
72
+ t.close();
73
+ }
74
+ }
75
+ export async function renewPolicy({
76
+ write = console.log,
77
+ ask = askTerminal,
78
+ dataDir,
79
+ ownerSigner,
80
+ readDeployment = loadCurrentDeployment,
81
+ syncDeployment = syncSetupDeployment,
82
+ approve = approveOwnerOperations,
83
+ connect = connectMarket,
84
+ call = localWalletAdmin,
85
+ unlock = controlWallet,
86
+ readState = read,
87
+ saveState = save,
88
+ guide = runPolicyTerminalGuide,
89
+ inspect = inspectSafePolicy,
90
+ submit = executeAnvilOwnerCall,
91
+ revoke = revokeOnChain,
92
+ rpcFactory = (url) =>
93
+ new JsonRpcProvider(url, undefined, { batchMaxCount: 1 }),
94
+ readPreviousTerms = async (rpc, policy) =>
95
+ new Contract(
96
+ policy.module,
97
+ [
98
+ "function policies(address,bytes32) view returns(address delegate,address token,address factory,address registry,uint256 maxPerPayment,uint256 totalBudget,uint256 spent,uint64 validAfter,uint64 validUntil,bool revoked)",
99
+ ],
100
+ rpc,
101
+ ).policies(policy.safe, policy.policyId),
102
+ tokenInfo = async (rpc, token) => {
103
+ const c = new Contract(
104
+ token,
105
+ [
106
+ "function decimals() view returns(uint8)",
107
+ "function symbol() view returns(string)",
108
+ ],
109
+ rpc,
110
+ );
111
+ return { decimals: Number(await c.decimals()), symbol: await c.symbol() };
112
+ },
113
+ } = {}) {
114
+ let rpc;
115
+ // Keep connection errors outside renewal failure handling: the on-chain renewal
116
+ // and local binding have already succeeded. Use connect's own hidden password prompt.
117
+ async function continueMarket() {
118
+ try {
119
+ const previous = await readState(join(dataDir, "market-connection.json"));
120
+ if (!previous?.marketOrigin) return;
121
+ write(
122
+ "Continuing your Market connection with the new spending permission.",
123
+ );
124
+ await connect(["--market", previous.marketOrigin], { dataDir, write });
125
+ write(
126
+ "If Market connection is unfinished, retry with agentpay market connect. Your renewal is complete.",
127
+ );
128
+ } catch {
129
+ write(
130
+ "Spending permission is renewed. Market connection could not finish. Retry: agentpay market connect",
131
+ );
132
+ }
133
+ }
134
+ try {
135
+ dataDir ??= await resolveUserDataDir();
136
+ const path = join(dataDir, "runtime/policy-renewal-guide.json");
137
+ let record = await readState(path);
138
+ const current = await readState(join(dataDir, "wallet/policy.json"));
139
+ if (!current) throw Error("No existing permission. Run agentpay setup first.");
140
+ if (
141
+ record?.sourcePolicyId !== current.policyId &&
142
+ record?.config?.policyId !== current.policyId
143
+ )
144
+ record = null;
145
+ const downloaded = await readDeployment({ dataDir, fresh: true });
146
+ const changes = downloaded
147
+ ? deploymentChanges(current, downloaded.config)
148
+ : [];
149
+ if (changes.some((k) => !["factory", "module"].includes(k)))
150
+ throw Error(
151
+ "Network, token or Registry changed. This requires a separate migration review; your original authorization is kept.",
152
+ );
153
+ const updating = changes.length > 0;
154
+ const destination = updating ? downloaded.config : current;
155
+ const status = await call(dataDir, "status");
156
+ if (
157
+ status.locked &&
158
+ (await unlock("unlock", { args: [dataDir], write })) !== 0
159
+ )
160
+ return 1;
161
+ if (
162
+ record?.config?.policyId === current.policyId &&
163
+ !status.policyRenewalPending
164
+ ) {
165
+ if (downloaded)
166
+ await syncDeployment({
167
+ dataDir,
168
+ config: downloaded.config,
169
+ authorization: record.config,
170
+ });
171
+ if (!record.localCompleted) {
172
+ record = { ...record, localCompleted: true };
173
+ await saveState(path, record);
174
+ write("Renewal already completed and local settings verified.");
175
+ await continueMarket();
176
+ write("Next: agentpay status");
177
+ return 0;
178
+ }
179
+ await saveState(
180
+ path + "." + current.policyId + ".completed.json",
181
+ record,
182
+ );
183
+ record = null;
184
+ }
185
+ if (record?.config?.policyId === current.policyId) {
186
+ await call(dataDir, "renewPolicy", {
187
+ expectedPolicyId: record.sourcePolicyId,
188
+ target: {
189
+ chainId: current.chainId,
190
+ rpcUrl: current.registryRpcUrl,
191
+ module: current.module,
192
+ safe: current.safe,
193
+ policyId: current.policyId,
194
+ },
195
+ });
196
+ if (downloaded)
197
+ await syncDeployment({
198
+ dataDir,
199
+ config: downloaded.config,
200
+ authorization: record.config,
201
+ });
202
+ write("Renewal already completed.");
203
+ await continueMarket();
204
+ write("Next: agentpay status");
205
+ return 0;
206
+ }
207
+ const { address } = await call(dataDir, "getAddress");
208
+ if (address.toLowerCase() !== current.delegate.toLowerCase())
209
+ throw Error("Wallet execution address differs from saved authorization.");
210
+ rpc = rpcFactory(current.registryRpcUrl);
211
+ if (Number((await rpc.getNetwork()).chainId) !== current.chainId)
212
+ throw Error("Wrong chain");
213
+ const signer = await selectedOwnerSigner({
214
+ dataDir,
215
+ choice: ownerSigner,
216
+ readState,
217
+ });
218
+ checkTestSigner(signer, current);
219
+ write(
220
+ signer === "anvil"
221
+ ? "Owner approval: private Anvil test signer."
222
+ : "Owner approval: your external Safe wallet. No test account will sign.",
223
+ );
224
+ const token = await tokenInfo(rpc, current.token);
225
+ const previous = await readPreviousTerms(rpc, current);
226
+ if (
227
+ ["delegate", "token", "factory", "registry"].some(
228
+ (k) => String(previous[k]).toLowerCase() !== current[k].toLowerCase(),
229
+ )
230
+ )
231
+ throw Error("Previous on-chain permission does not match this Wallet.");
232
+ const chainNow = updating
233
+ ? Number((await rpc.getBlock("latest")).timestamp)
234
+ : null;
235
+ const duration = updating
236
+ ? Number(previous.validUntil) - chainNow
237
+ : Number(previous.validUntil) - Number(previous.validAfter);
238
+ const remaining =
239
+ BigInt(previous.totalBudget) - BigInt(previous.spent ?? 0);
240
+ const budget =
241
+ updating && remaining > 0n ? remaining : BigInt(previous.totalBudget);
242
+ const perPayment =
243
+ updating && BigInt(previous.maxPerPayment) > budget
244
+ ? budget
245
+ : BigInt(previous.maxPerPayment);
246
+ const defaults = {
247
+ budget: formatUnits(budget, token.decimals),
248
+ perPayment: formatUnits(perPayment, token.decimals),
249
+ ...(updating && duration > 0
250
+ ? { days: String(Math.max(1, Math.floor(duration / 86400))) }
251
+ : Number.isSafeInteger(duration) && duration > 0
252
+ ? {
253
+ days: String(Math.ceil(duration / 86400)),
254
+ ...(duration % 86400 ? { daysRounded: true } : {}),
255
+ }
256
+ : {}),
257
+ };
258
+ if (updating && duration > 0)
259
+ defaults.validUntil = Number(previous.validUntil);
260
+ if (updating && remaining <= 0n) delete defaults.budget;
261
+ const deployment = {
262
+ mode: signer === "anvil" ? "private-anvil" : "production",
263
+ chainId: current.chainId,
264
+ rpcUrl: current.registryRpcUrl,
265
+ module: destination.module,
266
+ token: current.token,
267
+ factory: destination.factory,
268
+ registry: current.registry,
269
+ safe: current.safe,
270
+ tokenDecimals: token.decimals,
271
+ tokenSymbol: token.symbol,
272
+ source: "Existing local Wallet deployment",
273
+ };
274
+ write(
275
+ "\n------------------------------------------------------------\n> Renew spending permission\n------------------------------------------------------------",
276
+ );
277
+ if (updating) {
278
+ write(
279
+ "AgentPay contracts have changed. Update spending permission before making new purchases.",
280
+ );
281
+ write(
282
+ "Remaining allowance is used as the default budget; review any budget or expiry change before approval.",
283
+ );
284
+ for (;;) {
285
+ const answer = (
286
+ await ask(
287
+ "Enter 'confirm' to review updated permission, 'details' for contract addresses, or 'q' to return",
288
+ )
289
+ ).trim();
290
+ if (answer === "details") {
291
+ for (const key of changes)
292
+ write(`${key}: ${current[key]} → ${destination[key]}`);
293
+ continue;
294
+ }
295
+ if (answer !== "confirm") return 0;
296
+ break;
297
+ }
298
+ }
299
+ write(
300
+ "Choose a new budget and expiry. Your Safe and Wallet key stay the same; previous Orders are kept.",
301
+ );
302
+ for (;;) {
303
+ if (!record) {
304
+ const inputsPath = join(
305
+ dataDir,
306
+ "runtime",
307
+ "renewal-fields-" +
308
+ current.policyId +
309
+ (updating
310
+ ? "-" +
311
+ destination.module.toLowerCase() +
312
+ "-" +
313
+ destination.factory.toLowerCase()
314
+ : "") +
315
+ ".json",
316
+ );
317
+ const result = await guide({
318
+ deployment,
319
+ delegate: address,
320
+ collectOnly: true,
321
+ boundSafe: current.safe,
322
+ migration: updating,
323
+ defaults,
324
+ checkpoint: await readState(inputsPath),
325
+ saveInputs: (v) => saveState(inputsPath, v),
326
+ ask,
327
+ write,
328
+ inspect: (plan) => inspect(rpc, plan),
329
+ });
330
+ if (!result.policyConfig) return 1;
331
+ if (updating && defaults.days) {
332
+ const fields = await readState(inputsPath);
333
+ if (
334
+ fields?.fields?.days === defaults.days &&
335
+ Number(previous.validUntil) > chainNow
336
+ )
337
+ result.policyConfig = {
338
+ ...result.policyConfig,
339
+ validUntil: Number(previous.validUntil),
340
+ };
341
+ }
342
+ if (
343
+ result.policyConfig.safe.toLowerCase() !== current.safe.toLowerCase()
344
+ )
345
+ throw Error("Renewal must use the existing Safe.");
346
+ record = {
347
+ version: 1,
348
+ sourcePolicyId: current.policyId,
349
+ config: result.policyConfig,
350
+ };
351
+ await saveState(path, record);
352
+ }
353
+ const plan = buildSafePolicyPlan(record.config, address);
354
+ if (
355
+ plan.factory.toLowerCase() !== destination.factory.toLowerCase() ||
356
+ plan.module.toLowerCase() !== destination.module.toLowerCase()
357
+ )
358
+ throw Error(
359
+ "Saved renewal draft uses another deployment. Keep it for review; do not submit a replacement.",
360
+ );
361
+ write(
362
+ "Checking the saved permission on-chain before review… No transaction is being submitted.",
363
+ );
364
+ const state = await inspect(rpc, plan);
365
+ showAuthorizationSummary(plan, {
366
+ write,
367
+ currency: deployment.tokenSymbol,
368
+ decimals: deployment.tokenDecimals,
369
+ });
370
+ write(
371
+ "This revokes your previous permission and creates the new one. Existing Orders remain accessible.",
372
+ );
373
+ const answer = (
374
+ await ask(
375
+ "Type 'confirm' to authorize and bind, 'back' to change the draft, or 'q' to return",
376
+ )
377
+ ).trim();
378
+ if (answer === "back") {
379
+ const latest = await inspect(rpc, plan);
380
+ const expired =
381
+ signer === "external" &&
382
+ record.submitted &&
383
+ BigInt((await rpc.getBlock("latest")).timestamp) >=
384
+ BigInt(plan.validUntil);
385
+ if (
386
+ latest.policyExists ||
387
+ status.policyRenewalPending ||
388
+ (record.submitted && !expired)
389
+ ) {
390
+ write(
391
+ "This authorization may already be on-chain and cannot be edited yet. Continue verification.",
392
+ );
393
+ continue;
394
+ }
395
+ const inputsPath = join(
396
+ dataDir,
397
+ "runtime",
398
+ "renewal-fields-" +
399
+ current.policyId +
400
+ (updating
401
+ ? "-" +
402
+ destination.module.toLowerCase() +
403
+ "-" +
404
+ destination.factory.toLowerCase()
405
+ : "") +
406
+ ".json",
407
+ );
408
+ const fields = await readState(inputsPath);
409
+ if (fields) {
410
+ const { days, ...previous } = fields.fields;
411
+ await saveState(inputsPath, { ...fields, fields: previous });
412
+ }
413
+ await saveState(path, null);
414
+ record = null;
415
+ continue;
416
+ }
417
+ if (answer !== "confirm") {
418
+ write("Renewal paused. Your saved choices are kept.");
419
+ return 0;
420
+ }
421
+ if (!state.moduleEnabled && !updating)
422
+ throw Error(
423
+ "AgentPay Module is disabled. Review its removal before re-enabling it.",
424
+ );
425
+ if (state.policyExists && !state.policyMatches)
426
+ throw Error("Existing new Policy does not match your choices.");
427
+ if (signer === "anvil") {
428
+ record = { ...record, submitted: true };
429
+ await saveState(path, record);
430
+ }
431
+ if (signer === "external") {
432
+ const revokeCall = {
433
+ purpose: "Revoke old permission",
434
+ to: current.module,
435
+ value: "0",
436
+ operation: 0,
437
+ data: new Interface([
438
+ "function revokePolicy(bytes32)",
439
+ ]).encodeFunctionData("revokePolicy", [current.policyId]),
440
+ };
441
+ // Keep the plan stable across restarts; the page skips verified completed steps.
442
+ const calls = [revokeCall, plan.calls[0], plan.calls[1]];
443
+ const verified = await approve({
444
+ source: {
445
+ ...current,
446
+ module: plan.module,
447
+ previousModule: current.module,
448
+ },
449
+ calls,
450
+ dataDir,
451
+ ask,
452
+ write,
453
+ onSubmitted: async (event) => {
454
+ // Old/unknown journal entries and portable exports remain conservative.
455
+ // Creation intent is persisted before the wallet prompt, including approveHash.
456
+ if (!Number.isInteger(event?.callIndex) || event.callIndex === 2) {
457
+ record = { ...record, submitted: true };
458
+ await saveState(path, record);
459
+ }
460
+ },
461
+ check: async () => {
462
+ const old = await readPreviousTerms(rpc, current);
463
+ const next = await inspect(rpc, plan);
464
+ return old.revoked && next.moduleEnabled && next.policyMatches;
465
+ },
466
+ });
467
+ if (!verified) return 0;
468
+ await saveState(join(dataDir, "wallet/policy-revocation.json"), {
469
+ policyId: current.policyId,
470
+ revoked: true,
471
+ checkedAtMs: Date.now(),
472
+ });
473
+ } else {
474
+ if (
475
+ !(await revoke({
476
+ rpc,
477
+ policy: current,
478
+ write,
479
+ submit,
480
+ ownerSigner: signer,
481
+ dataDir,
482
+ ask,
483
+ approve,
484
+ }))
485
+ )
486
+ return 0;
487
+ await saveState(join(dataDir, "wallet/policy-revocation.json"), {
488
+ policyId: current.policyId,
489
+ revoked: true,
490
+ checkedAtMs: Date.now(),
491
+ });
492
+ if (state.threshold !== 1 || state.owners.length !== 1)
493
+ throw Error(
494
+ "Private renewal requires the sole prepared Anvil owner.",
495
+ );
496
+ if (!state.moduleEnabled)
497
+ await submit({ source: current }, state.owners[0], plan.calls[0]);
498
+ if (!state.policyMatches)
499
+ await submit({ source: current }, state.owners[0], plan.calls[1]);
500
+ }
501
+ await call(dataDir, "renewPolicy", {
502
+ expectedPolicyId: record.sourcePolicyId,
503
+ target: {
504
+ chainId: current.chainId,
505
+ rpcUrl: current.registryRpcUrl,
506
+ module: plan.module,
507
+ safe: current.safe,
508
+ policyId: plan.policyId,
509
+ },
510
+ });
511
+ if (downloaded)
512
+ await syncDeployment({
513
+ dataDir,
514
+ config: downloaded.config,
515
+ authorization: record.config,
516
+ });
517
+ record = { ...record, localCompleted: true };
518
+ await saveState(path, record);
519
+ write(
520
+ "Spending permission renewed and verified by Wallet. Original Order records were kept.",
521
+ );
522
+ await continueMarket();
523
+ write("Next: agentpay status");
524
+ return 0;
525
+ }
526
+ } catch (e) {
527
+ write(
528
+ e.code === "CANCELLED"
529
+ ? "Renewal paused. Your choices are kept."
530
+ : e.code === "METHOD_NOT_ALLOWED"
531
+ ? "Wallet needs the renewal update. Run agentpay repair, then agentpay policy renew."
532
+ : e.message || "Renewal could not finish.",
533
+ );
534
+ write(
535
+ "Retry: agentpay policy renew. Keep your keys and existing Order records.",
536
+ );
537
+ return e.code === "CANCELLED" ? 0 : 1;
538
+ } finally {
539
+ rpc?.destroy();
540
+ }
541
+ }
542
+
543
+ export async function revokePolicy({
544
+ write = console.log,
545
+ ask = askTerminal,
546
+ dataDir,
547
+ ownerSigner,
548
+ readState = read,
549
+ saveState = save,
550
+ rpcFactory = (url) =>
551
+ new JsonRpcProvider(url, undefined, { batchMaxCount: 1 }),
552
+ revoke = revokeOnChain,
553
+ } = {}) {
554
+ let rpc;
555
+ try {
556
+ dataDir ??= await resolveUserDataDir();
557
+ const policy = await readState(join(dataDir, "wallet/policy.json"));
558
+ if (!policy) throw Error("No saved spending permission.");
559
+ const signer = await selectedOwnerSigner({
560
+ dataDir,
561
+ choice: ownerSigner,
562
+ readState,
563
+ });
564
+ checkTestSigner(signer, policy);
565
+ write(
566
+ signer === "anvil"
567
+ ? "Owner approval: private Anvil test signer."
568
+ : "Owner approval: your external Safe wallet. No test account will sign.",
569
+ );
570
+ write("\n> Revoke spending permission");
571
+ write(`Safe: ${policy.safe}\nPolicy ID: ${policy.policyId}`);
572
+ write(
573
+ "Stops new payments under this permission. Existing Orders, queries and support remain available.",
574
+ );
575
+ if (
576
+ (await ask("Type 'confirm' to revoke, or 'q' to return")).trim() !==
577
+ "confirm"
578
+ ) {
579
+ write("Revocation cancelled.");
580
+ return 0;
581
+ }
582
+ rpc = rpcFactory(policy.registryRpcUrl);
583
+ if (Number((await rpc.getNetwork()).chainId) !== policy.chainId)
584
+ throw Error("Wrong chain");
585
+ if (
586
+ !(await revoke({ rpc, policy, write, ownerSigner: signer, dataDir, ask }))
587
+ )
588
+ return 0;
589
+ await saveState(join(dataDir, "wallet/policy-revocation.json"), {
590
+ policyId: policy.policyId,
591
+ revoked: true,
592
+ checkedAtMs: Date.now(),
593
+ });
594
+ write(
595
+ "Spending permission revoked on-chain. Existing Orders were kept.\nNext: agentpay status",
596
+ );
597
+ return 0;
598
+ } catch (e) {
599
+ write(e.message || "Revocation could not be confirmed.");
600
+ write("Retry: agentpay policy revoke");
601
+ return 1;
602
+ } finally {
603
+ rpc?.destroy();
604
+ }
605
+ }