@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,667 @@
1
+ // Read-only startup and periodic reconciliation. Keep financial state separate from
2
+ // local result availability; unresolved original scopes remain visible for review.
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 { localOrderReviews, reviewApplies } from "./order-review.mjs";
7
+ import { isUnpaidExpiry } from "./order-closure.mjs";
8
+ import { constants } from "node:fs";
9
+ import { open, rename, unlink, readdir, lstat } from "node:fs/promises";
10
+ import { dirname } from "node:path";
11
+ import { createHash, randomUUID } from "node:crypto";
12
+ import { readSettlementResult } from "./settlement-response.mjs";
13
+ import {
14
+ providerOutput,
15
+ parseServiceReference,
16
+ normalizeRuntimeResult,
17
+ } from "./service-reference.mjs";
18
+ const fail = (code) => Object.assign(Error(code), { code });
19
+ const fingerprint = (v) =>
20
+ createHash("sha256").update(JSON.stringify(v)).digest("hex");
21
+ const terminal = new Set(["Released", "Refunded", "Cancelled"]);
22
+ const resultIssue = (o) =>
23
+ o?.resultState === "invalid"
24
+ ? "RECOVERY_RESULT_INVALID"
25
+ : o?.resultState === "legacy_unverified"
26
+ ? "LEGACY_RESULT_UNVERIFIED"
27
+ : o?.resultState === "pending"
28
+ ? "RESULT_EVIDENCE_PENDING"
29
+ : o?.financialState === "terminal" &&
30
+ o.vaultState === "Released" &&
31
+ o.resultState === "missing"
32
+ ? "SETTLED_RESULT_UNAVAILABLE"
33
+ : null;
34
+ const knownCodes = new Set([
35
+ "LOCKED",
36
+ "POLICY_UNBOUND",
37
+ "POLICY_MIGRATION_PENDING",
38
+ "ORDER_NOT_FOUND",
39
+ "ORDER_IDENTITY_MISMATCH",
40
+ "SETTLEMENT_RESPONSE_INVALID",
41
+ "RECOVERY_RESULT_INVALID",
42
+ "ORDER_STATE_CONFLICT",
43
+ "RECOVERY_SCOPE_INVALID",
44
+ "RECOVERY_POLICY_UNAVAILABLE",
45
+ "RESULT_STORAGE_UNAVAILABLE",
46
+ ]);
47
+ // Only authenticated read methods are injected. No payment or Provider operation.
48
+ export function createRuntimeRecovery({
49
+ store,
50
+ results,
51
+ statePath,
52
+ settlementUrl,
53
+ readWallet,
54
+ readOrder,
55
+ readRequest = null,
56
+ now = Date.now,
57
+ }) {
58
+ let inventoryState = "checking",
59
+ inventoryIssue = null,
60
+ inventory = {},
61
+ observations = {},
62
+ walletState = "not_checked",
63
+ lastCheckedAtMs = null,
64
+ storageError = false,
65
+ active = null,
66
+ timer = null,
67
+ stopped = false,
68
+ cursor = 0,
69
+ loaded = false,
70
+ loading = null;
71
+ const path = statePath + ".recovery.json";
72
+ const reviews = localOrderReviews(statePath);
73
+ let reviewNotes = {};
74
+ async function readObservations() {
75
+ let h;
76
+ try {
77
+ h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
78
+ const s = await h.stat();
79
+ if (
80
+ !s.isFile() ||
81
+ s.uid !== process.getuid() ||
82
+ s.mode & 0o077 ||
83
+ s.size > 16 * 1048576
84
+ )
85
+ throw fail("RECOVERY_STATE_INVALID");
86
+ const doc = JSON.parse(await h.readFile("utf8"));
87
+ if (
88
+ doc.version !== "agentpay.runtime-recovery.v1" ||
89
+ !doc.observations ||
90
+ typeof doc.observations !== "object" ||
91
+ Array.isArray(doc.observations)
92
+ )
93
+ throw fail("RECOVERY_STATE_INVALID");
94
+ for (const [id, v] of Object.entries(doc.observations))
95
+ if (
96
+ !/^0x[0-9a-f]{64}$/.test(id) ||
97
+ !v ||
98
+ !/^([0-9a-f]{64})$/.test(v.fingerprint) ||
99
+ !Number.isSafeInteger(v.checkedAtMs) ||
100
+ !Number.isSafeInteger(v.nextCheckAtMs) ||
101
+ !Number.isInteger(v.attempts) ||
102
+ v.attempts < 0 ||
103
+ v.attempts > 8 ||
104
+ typeof v.source !== "string" ||
105
+ !["unknown", "pending", "closing", "terminal"].includes(
106
+ v.financialState,
107
+ ) ||
108
+ ![
109
+ "missing",
110
+ "verified",
111
+ "invalid",
112
+ "legacy_unverified",
113
+ "pending",
114
+ ].includes(v.resultState)
115
+ )
116
+ throw fail("RECOVERY_STATE_INVALID");
117
+ observations = doc.observations;
118
+ } catch (e) {
119
+ if (e.code !== "ENOENT") throw e;
120
+ } finally {
121
+ await h?.close();
122
+ }
123
+ loaded = true;
124
+ }
125
+ async function load() {
126
+ if (loaded) return;
127
+ if (!loading)
128
+ loading = readObservations().finally(() => {
129
+ loading = null;
130
+ });
131
+ return loading;
132
+ }
133
+ async function persist() {
134
+ const parent = await lstat(dirname(path));
135
+ if (
136
+ !parent.isDirectory() ||
137
+ parent.isSymbolicLink() ||
138
+ parent.uid !== process.getuid() ||
139
+ parent.mode & 0o077
140
+ )
141
+ throw fail("RECOVERY_STATE_INVALID");
142
+ const temp = path + "." + randomUUID() + ".tmp";
143
+ let h;
144
+ try {
145
+ h = await open(temp, "wx", 0o600);
146
+ await h.writeFile(
147
+ JSON.stringify({
148
+ version: "agentpay.runtime-recovery.v1",
149
+ observations,
150
+ }) + "\n",
151
+ );
152
+ await h.sync();
153
+ await h.close();
154
+ h = null;
155
+ await rename(temp, path);
156
+ const d = await open(dirname(path), "r");
157
+ try {
158
+ await d.sync();
159
+ } finally {
160
+ await d.close();
161
+ }
162
+ } finally {
163
+ await h?.close();
164
+ await unlink(temp).catch((e) => {
165
+ if (e.code !== "ENOENT") throw e;
166
+ });
167
+ }
168
+ }
169
+ async function inspect() {
170
+ try {
171
+ await load();
172
+ const entries = await store.inventory();
173
+ if (Object.keys(entries).length > 10000) throw fail("HISTORY_TOO_LARGE");
174
+ if (Object.keys(observations).some((id) => !Object.hasOwn(entries, id)))
175
+ throw fail("HISTORY_MISSING");
176
+ const names = await readdir(statePath + ".results").catch((e) => {
177
+ if (e.code === "ENOENT") return [];
178
+ throw e;
179
+ });
180
+ if (
181
+ names.some(
182
+ (name) =>
183
+ /^0x[0-9a-f]{64}\.json$/.test(name) &&
184
+ !Object.hasOwn(entries, name.slice(0, -5)),
185
+ )
186
+ )
187
+ throw fail("HISTORY_MISSING");
188
+ const pendingNames = await readdir(statePath + ".pending-results").catch(
189
+ (e) => {
190
+ if (e.code === "ENOENT") return [];
191
+ throw e;
192
+ },
193
+ );
194
+ if (
195
+ pendingNames.some(
196
+ (name) =>
197
+ /^0x[0-9a-f]{64}\.json$/.test(name) &&
198
+ !Object.hasOwn(entries, name.slice(0, -5)),
199
+ )
200
+ )
201
+ throw fail("HISTORY_MISSING");
202
+ reviewNotes = (await reviews.load()).records;
203
+ inventory = entries;
204
+ inventoryState = "complete";
205
+ inventoryIssue = null;
206
+ return true;
207
+ } catch (e) {
208
+ inventoryState = "blocked";
209
+ inventoryIssue = [
210
+ "HISTORY_MISSING",
211
+ "HISTORY_TOO_LARGE",
212
+ "RECOVERY_STATE_INVALID",
213
+ ].includes(e.code)
214
+ ? e.code
215
+ : "HISTORY_INVALID_OR_UNREADABLE";
216
+ return false;
217
+ }
218
+ }
219
+ function status() {
220
+ const entries = Object.entries(inventory);
221
+ let checked = 0,
222
+ settled = 0,
223
+ expired = 0,
224
+ missingResult = 0,
225
+ waiting = 0,
226
+ review = 0,
227
+ archived = 0;
228
+ for (const [id, v] of entries) {
229
+ const o = observations[id],
230
+ hidden =
231
+ reviewApplies(reviewNotes[id], v, o) &&
232
+ (!o || o.source === settlementUrl);
233
+ if (hidden) archived++;
234
+ if (
235
+ !o ||
236
+ o.fingerprint !== fingerprint(v) ||
237
+ o.source !== settlementUrl
238
+ ) {
239
+ if (!hidden) waiting++;
240
+ continue;
241
+ }
242
+ checked++;
243
+ if (o.financialState === "terminal") {
244
+ if (o.expiredUnpaid) expired++;
245
+ else settled++;
246
+ }
247
+ if (hidden) continue;
248
+ if (resultIssue(o) === "SETTLED_RESULT_UNAVAILABLE") missingResult++;
249
+ if (
250
+ o.errorCode ||
251
+ ["invalid", "legacy_unverified"].includes(o.resultState)
252
+ )
253
+ review++;
254
+ else if (o.financialState !== "terminal" || o.resultState === "pending")
255
+ waiting++;
256
+ }
257
+ const issues = entries
258
+ .flatMap(([id, v]) => {
259
+ const o = observations[id];
260
+ if (
261
+ reviewApplies(reviewNotes[id], v, o) &&
262
+ (!o || o.source === settlementUrl)
263
+ )
264
+ return [];
265
+ if (
266
+ !o ||
267
+ o.fingerprint !== fingerprint(v) ||
268
+ o.source !== settlementUrl
269
+ )
270
+ return [];
271
+ return o.errorCode || resultIssue(o)
272
+ ? [
273
+ {
274
+ invocationId: id,
275
+ orderId: v.orderId,
276
+ code: o.errorCode || resultIssue(o),
277
+ checkedAtMs: o.checkedAtMs,
278
+ },
279
+ ]
280
+ : [];
281
+ })
282
+ .slice(0, 20);
283
+ return {
284
+ inventory: inventoryState,
285
+ inventoryIssue,
286
+ issues,
287
+ wallet: walletState,
288
+ paymentGate:
289
+ inventoryState !== "complete" || storageError
290
+ ? "blocked"
291
+ : "normal_wallet_and_order_checks_required",
292
+ purchaseReadinessChecked: false,
293
+ orders: {
294
+ total: entries.length,
295
+ legacy: entries.filter(
296
+ ([, v]) => v.routingVersion !== "agentpay.runtime-call.v1",
297
+ ).length,
298
+ checked,
299
+ settled,
300
+ expired,
301
+ missingResult,
302
+ waiting,
303
+ review,
304
+ archived,
305
+ },
306
+ lastCheckedAtMs,
307
+ storageError,
308
+ nextAction:
309
+ inventoryState === "blocked" || storageError
310
+ ? "Preserve local history and repair its storage; new paid calls are paused."
311
+ : walletState === "locked"
312
+ ? "Run agentpay wallet unlock; Order checks will continue automatically."
313
+ : walletState === "unavailable"
314
+ ? "Start Wallet or inspect agentpay service logs wallet."
315
+ : walletState === "unbound"
316
+ ? "Complete Wallet authorization before Order checks."
317
+ : walletState === "migration_pending"
318
+ ? "Complete the existing Policy migration."
319
+ : review
320
+ ? "Some Orders require review; their original requests are preserved."
321
+ : missingResult
322
+ ? "Some settled Orders have no verified local result. Do not repeat their purchases."
323
+ : "Order checks run in the background; this is not proof that a new purchase is eligible.",
324
+ };
325
+ }
326
+ async function cycle() {
327
+ if (!(await inspect())) return;
328
+ let w;
329
+ try {
330
+ w = await readWallet();
331
+ if (typeof w?.locked !== "boolean" || typeof w?.bound !== "boolean")
332
+ throw Error();
333
+ walletState = w.policyMigrationPending
334
+ ? "migration_pending"
335
+ : w.locked
336
+ ? "locked"
337
+ : !w.bound
338
+ ? "unbound"
339
+ : "available";
340
+ } catch {
341
+ walletState = "unavailable";
342
+ return;
343
+ }
344
+ if (walletState !== "available") return;
345
+ const entries = Object.entries(inventory);
346
+ let changed = false;
347
+ for (let n = 0; n < Math.min(entries.length, 4) && !stopped; n++) {
348
+ const [id, entry] = entries[cursor++ % entries.length],
349
+ hash = fingerprint(entry),
350
+ old = observations[id];
351
+ if (
352
+ old?.fingerprint === hash &&
353
+ old.source === settlementUrl &&
354
+ old.nextCheckAtMs > now()
355
+ )
356
+ continue;
357
+ let row = {
358
+ fingerprint: hash,
359
+ source: settlementUrl,
360
+ checkedAtMs: now(),
361
+ financialState: "unknown",
362
+ resultState: "missing",
363
+ orderId: entry.orderId,
364
+ attempts: Math.min((old?.attempts || 0) + 1, 8),
365
+ };
366
+ // Keep terminal evidence even when this attempt fails before receiving a response.
367
+ if (old?.fingerprint === hash && old.source === settlementUrl) {
368
+ const prior =
369
+ old.previousTerminalState ||
370
+ (old.financialState === "terminal" && terminal.has(old.vaultState)
371
+ ? old.vaultState
372
+ : null);
373
+ if (prior) {
374
+ row.previousTerminalState = prior;
375
+ row.previousCheckedAtMs = old.previousCheckedAtMs || old.checkedAtMs;
376
+ }
377
+ }
378
+ try {
379
+ let context = null;
380
+ if (entry.routingVersion === "agentpay.runtime-call.v1") {
381
+ const binding = JSON.parse(entry.serviceBinding);
382
+ if (binding.version !== "agentpay.runtime-service.v1")
383
+ throw fail("ORDER_IDENTITY_MISMATCH");
384
+ const reference = parseServiceReference(binding.reference);
385
+ context = {
386
+ reference,
387
+ skillId: reference.skillId,
388
+ profileHash: reference.profileHash,
389
+ requestedMaxUsageUnits: binding.requestedMaxUsageUnits,
390
+ };
391
+ }
392
+ try {
393
+ const result = await results.read(entry);
394
+ if (result) {
395
+ if (context) {
396
+ normalizeRuntimeResult(
397
+ result,
398
+ context,
399
+ entry.requestHash,
400
+ entry.orderId,
401
+ );
402
+ row.resultState = "verified";
403
+ } else row.resultState = "legacy_unverified";
404
+ }
405
+ } catch {
406
+ row.resultState = "invalid";
407
+ }
408
+ let pending = null;
409
+ if (row.resultState === "missing" && results.readPending) {
410
+ try {
411
+ pending = await results.readPending(entry);
412
+ if (pending) {
413
+ if (!context) row.resultState = "legacy_unverified";
414
+ else {
415
+ pending = providerOutput(
416
+ { result: pending },
417
+ context,
418
+ null,
419
+ entry.orderId,
420
+ entry.requestHash,
421
+ );
422
+ row.resultState = "pending";
423
+ }
424
+ }
425
+ } catch {
426
+ row.resultState = "invalid";
427
+ }
428
+ }
429
+ const c = entry.paymentPayload?.payload?.commitment;
430
+ const recoveryScope =
431
+ entry.recoveryScope ||
432
+ (c?.policyId && w.recoveryScope
433
+ ? {
434
+ ...w.recoveryScope,
435
+ policyId: c.policyId,
436
+ safe: c.safe,
437
+ network: entry.paymentPayload.accepted?.network,
438
+ }
439
+ : null);
440
+ if (
441
+ entry.recoveryScope &&
442
+ c?.policyId &&
443
+ String(entry.recoveryScope.policyId).toLowerCase() !==
444
+ String(c.policyId).toLowerCase()
445
+ )
446
+ throw fail("ORDER_IDENTITY_MISMATCH");
447
+ let lookup = null;
448
+ if (!entry.orderId && readRequest) {
449
+ lookup = readSettlementResult(
450
+ await readRequest(entry.orderRequestId, recoveryScope),
451
+ );
452
+ if (
453
+ lookup.orderRequestId !== entry.orderRequestId ||
454
+ typeof lookup.found !== "boolean"
455
+ )
456
+ throw fail("SETTLEMENT_RESPONSE_INVALID");
457
+ if (!lookup.found)
458
+ row.errorCode = recoveryScope
459
+ ? "ORDER_REQUEST_NOT_FOUND"
460
+ : "ORIGINAL_POLICY_UNKNOWN";
461
+ }
462
+ if (!entry.orderId && !lookup?.found) {
463
+ row.errorCode ||= "ORDER_ID_UNRESOLVED";
464
+ } else {
465
+ const response =
466
+ lookup ||
467
+ readSettlementResult(
468
+ await readOrder(entry.orderId, recoveryScope),
469
+ ),
470
+ o = response.order;
471
+ const resolvedOrderId = entry.orderId || o?.orderId;
472
+ if (
473
+ !o ||
474
+ !/^0x[0-9a-f]{64}$/.test(resolvedOrderId || "") ||
475
+ o.orderId !== resolvedOrderId ||
476
+ o.requestHash !== entry.requestHash
477
+ )
478
+ throw fail("ORDER_IDENTITY_MISMATCH");
479
+ const lower = (v) => String(v).toLowerCase();
480
+ if (
481
+ c &&
482
+ (c.orderId !== o.orderId ||
483
+ lower(c.vault) !== lower(o.vaultAddress) ||
484
+ lower(c.safe) !== lower(o.buyerAddress) ||
485
+ lower(c.token) !== lower(o.assetAddress) ||
486
+ String(c.amount) !== String(o.prepaidAmountAtomic))
487
+ )
488
+ throw fail("ORDER_IDENTITY_MISMATCH");
489
+ if (
490
+ context &&
491
+ (o.skillId !== context.skillId ||
492
+ o.profileHash !== context.profileHash ||
493
+ o.providerId !== context.reference.profile.providerId ||
494
+ `eip155:${o.chainId}` !== context.reference.profile.network ||
495
+ lower(o.assetAddress) !== lower(context.reference.profile.asset))
496
+ )
497
+ throw fail("ORDER_IDENTITY_MISMATCH");
498
+ if (!context && !c) throw fail("ORDER_IDENTITY_MISMATCH");
499
+ const network = entry.paymentPayload?.accepted?.network;
500
+ if (network && network !== `eip155:${o.chainId}`)
501
+ throw fail("ORDER_IDENTITY_MISMATCH");
502
+ const closure = response.closure?.state,
503
+ expiredUnpaid = isUnpaidExpiry(response);
504
+ if (
505
+ (closure && o.vaultState === "Released") ||
506
+ (closure === "refunded" && o.vaultState !== "Refunded") ||
507
+ (closure === "cancelled" &&
508
+ o.vaultState !== "Cancelled" &&
509
+ !expiredUnpaid)
510
+ )
511
+ throw fail("ORDER_IDENTITY_MISMATCH");
512
+ if (
513
+ row.previousTerminalState &&
514
+ o.vaultState !== row.previousTerminalState
515
+ )
516
+ throw fail("ORDER_STATE_CONFLICT");
517
+ if (!entry.orderId) {
518
+ if (!store.attachRecoveredOrder)
519
+ throw fail("ORDER_IDENTITY_MISMATCH");
520
+ // Attach only after all original request/service/commitment checks passed.
521
+ await store.attachRecoveredOrder(
522
+ entry,
523
+ resolvedOrderId,
524
+ recoveryScope || w.recoveryScope,
525
+ );
526
+ continue; // Next cycle checks the new fingerprint; never pay from recovery.
527
+ }
528
+ if (
529
+ pending &&
530
+ context &&
531
+ row.resultState === "pending" &&
532
+ response.deliveryEvidence
533
+ ) {
534
+ const evidence = response.deliveryEvidence;
535
+ if (
536
+ String(evidence.outputHash).toLowerCase() !==
537
+ pending.outputHash ||
538
+ String(evidence.usageUnits) !== String(pending.usageUnits)
539
+ )
540
+ throw fail("RECOVERY_RESULT_INVALID");
541
+ const result = normalizeRuntimeResult(
542
+ {
543
+ version: "agentpay.runtime-result.v1",
544
+ orderId: entry.orderId,
545
+ requestHash: entry.requestHash,
546
+ service: {
547
+ skillId: context.skillId,
548
+ profileHash: context.profileHash,
549
+ },
550
+ output: pending.output,
551
+ outputHash: pending.outputHash,
552
+ usageUnits: pending.usageUnits,
553
+ deliveryEvidenceVerified: true,
554
+ finalization: response.finalization,
555
+ deadlines: response.deadlines,
556
+ },
557
+ context,
558
+ entry.requestHash,
559
+ entry.orderId,
560
+ );
561
+ try {
562
+ await results.save(entry, result);
563
+ } catch {
564
+ throw fail("RESULT_STORAGE_UNAVAILABLE");
565
+ }
566
+ row.resultState = "verified";
567
+ }
568
+ row.expiredUnpaid = expiredUnpaid;
569
+ row.vaultState = o.vaultState;
570
+ row.financialState =
571
+ terminal.has(o.vaultState) || expiredUnpaid
572
+ ? "terminal"
573
+ : closure
574
+ ? "closing"
575
+ : "pending";
576
+ row.attempts = 0;
577
+ }
578
+ } catch (e) {
579
+ const code = e.settlementCode || e.code;
580
+ row.errorCode = knownCodes.has(code) ? code : "ORDER_CHECK_UNAVAILABLE";
581
+ }
582
+ if (row.resultState === "invalid" && !row.errorCode)
583
+ row.errorCode = "RECOVERY_RESULT_INVALID";
584
+ row.nextCheckAtMs =
585
+ now() +
586
+ (row.financialState === "terminal" && row.resultState !== "pending"
587
+ ? 3600000
588
+ : row.errorCode
589
+ ? Math.min(300000, 5000 * 2 ** row.attempts)
590
+ : 30000);
591
+ if (stopped) return;
592
+ // A foreground call may have advanced while the read was in flight.
593
+ const fresh = await store.inventory();
594
+ if (!fresh[id] || fingerprint(fresh[id]) !== hash) continue;
595
+ observations[id] = row;
596
+ changed = true;
597
+ }
598
+ if (changed || storageError) {
599
+ try {
600
+ await persist();
601
+ storageError = false;
602
+ } catch {
603
+ storageError = true;
604
+ }
605
+ }
606
+ lastCheckedAtMs = now();
607
+ }
608
+ function refresh() {
609
+ if (active) return active;
610
+ active = cycle()
611
+ .catch(() => {
612
+ inventoryState = "blocked";
613
+ })
614
+ .finally(() => {
615
+ active = null;
616
+ });
617
+ return active;
618
+ }
619
+ return {
620
+ status,
621
+ refresh,
622
+ assertInvocation(entry) {
623
+ const observation = observations[entry.invocationId];
624
+ if (
625
+ observation?.fingerprint === fingerprint(entry) &&
626
+ observation.source === settlementUrl &&
627
+ [
628
+ "ORDER_IDENTITY_MISMATCH",
629
+ "ORDER_STATE_CONFLICT",
630
+ "RECOVERY_RESULT_INVALID",
631
+ "SETTLEMENT_RESPONSE_INVALID",
632
+ ].includes(observation.errorCode)
633
+ )
634
+ throw Object.assign(
635
+ Error(
636
+ "This original request needs review. Preserve its history; do not create a replacement purchase.",
637
+ ),
638
+ { code: "RUNTIME_RECOVERY_REQUIRED", status: 409 },
639
+ );
640
+ },
641
+ async assertLocalHistory() {
642
+ if (!(await inspect()) || storageError)
643
+ throw Object.assign(
644
+ Error(
645
+ "Local history needs review. Runtime remains available; preserve existing requests and check agentpay status.",
646
+ ),
647
+ { code: "RUNTIME_RECOVERY_REQUIRED", status: 409 },
648
+ );
649
+ },
650
+ start() {
651
+ stopped = false;
652
+ const tick = async () => {
653
+ await refresh();
654
+ if (!stopped) {
655
+ timer = setTimeout(tick, 10000 + Math.floor(Math.random() * 1000));
656
+ timer.unref();
657
+ }
658
+ };
659
+ void tick();
660
+ },
661
+ async stop() {
662
+ stopped = true;
663
+ clearTimeout(timer);
664
+ await active;
665
+ },
666
+ };
667
+ }
@@ -0,0 +1,32 @@
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 { join } from "node:path";
5
+ import { resolveUserDataDir } from "./local-data-directory.mjs";
6
+ import { readServiceSettings } from "./local-services.mjs";
7
+ import { readCallerToken } from "./caller-auth.mjs";
8
+ export async function readRuntimeStatus(dataDir = undefined) {
9
+ dataDir ??= await resolveUserDataDir();
10
+ const settings = await readServiceSettings(dataDir),
11
+ token = await readCallerToken(join(dataDir, "runtime/caller.token"));
12
+ const response = await fetch(settings.runtimeUrl + "/status", {
13
+ headers: { authorization: "Bearer " + token },
14
+ redirect: "error",
15
+ signal: AbortSignal.timeout(2500),
16
+ });
17
+ if (response.status === 404)
18
+ throw Object.assign(Error("Runtime status requires a newer version."), {
19
+ code: "STATUS_UNSUPPORTED",
20
+ });
21
+ if (!response.ok) throw Error("Runtime status unavailable");
22
+ const body = await response.json(),
23
+ s = body.result;
24
+ if (
25
+ body.ok !== true ||
26
+ s?.role !== "user-runtime" ||
27
+ !["checking", "complete", "blocked"].includes(s.inventory) ||
28
+ !s.orders
29
+ )
30
+ throw Error("Invalid Runtime status");
31
+ return s;
32
+ }