@c4roa0/agentpay-user 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +137 -0
- package/CONTRIBUTING.md +16 -0
- package/LICENSE +373 -0
- package/NOTICE.md +20 -0
- package/README.md +85 -2
- package/SECURITY.md +16 -0
- package/doc/README.md +52 -0
- package/doc/TODO.md +107 -0
- package/doc/architecture.md +99 -0
- package/doc/design.md +60 -0
- package/doc/develope.md +68 -0
- package/doc/plans/user-wallet-distribution.md +91 -0
- package/doc/reference/harness-skill-runtime.md +120 -0
- package/doc/reference/local-cache-foundation.md +176 -0
- package/doc/reference/local-safe-owner-ui.md +25 -0
- package/doc/reference/local-update.md +18 -0
- package/doc/reference/multi-service-runtime.md +119 -0
- package/doc/reference/npm-integration-verification.md +103 -0
- package/doc/reference/onboarding-writing.md +48 -0
- package/doc/reference/original-order-recovery.md +97 -0
- package/doc/reference/private-anvil-onboarding-config.example.json +18 -0
- package/doc/reference/public-deployment-settings.md +56 -0
- package/doc/reference/random-skill-contract.md +18 -0
- package/doc/reference/result-file-cache.md +166 -0
- package/doc/reference/runtime-address-recovery.md +42 -0
- package/doc/reference/runtime-result-cache.md +157 -0
- package/doc/reference/runtime-startup-recovery.md +141 -0
- package/doc/reference/skill-input-output-cache.md +93 -0
- package/doc/reference/wallet-identity-policy-history.md +23 -0
- package/doc/runbooks/commands.md +103 -0
- package/doc/runbooks/deployment-authorization-update.md +42 -0
- package/doc/runbooks/emergency-payment-stop.md +80 -0
- package/doc/runbooks/local-autostart.md +212 -0
- package/doc/runbooks/local-cache.md +115 -0
- package/doc/runbooks/local-update.md +84 -0
- package/doc/runbooks/market-connection.md +73 -0
- package/doc/runbooks/npm-publication.md +56 -0
- package/doc/runbooks/official-test-package.md +62 -0
- package/doc/runbooks/order-cancellation.md +56 -0
- package/doc/runbooks/order-support.md +41 -0
- package/doc/runbooks/policy-renewal.md +80 -0
- package/doc/runbooks/private-anvil-terminal-setup.md +58 -0
- package/doc/runbooks/runtime-skill-client.md +155 -0
- package/doc/runbooks/safe-owner-web.md +104 -0
- package/doc/runbooks/safe-policy-migration.md +36 -0
- package/doc/runbooks/safe-policy-setup.md +52 -0
- package/doc/runbooks/terminal-authorization-preview.md +38 -0
- package/doc/runbooks/unlock-wallet.md +68 -0
- package/doc/runbooks/user-onboarding.md +87 -0
- package/doc/runbooks/wallet-identity.md +59 -0
- package/doc/security/public-source-review-2026-11-06.md +71 -0
- package/doc/usage.md +54 -0
- package/package.json +49 -4
- package/progress.md +210 -0
- package/run +29 -0
- package/scripts/agentpay-cli.mjs +11 -0
- package/scripts/authorize-existing-safe.mjs +211 -0
- package/scripts/autostart.mjs +186 -0
- package/scripts/cancel-order.mjs +76 -0
- package/scripts/check-installation.mjs +26 -0
- package/scripts/check-official-test-package.mjs +35 -0
- package/scripts/check-purchase-readiness.mjs +275 -0
- package/scripts/configure-runtime-client.mjs +16 -0
- package/scripts/guide-policy.mjs +64 -0
- package/scripts/initialize-caller-credential.mjs +11 -0
- package/scripts/install-skill-bundle.mjs +176 -0
- package/scripts/install-user.mjs +24 -0
- package/scripts/local-service.mjs +60 -0
- package/scripts/lock.mjs +11 -0
- package/scripts/login-notify.mjs +39 -0
- package/scripts/migrate-safe-policy.mjs +165 -0
- package/scripts/official-paid-test.mjs +9 -0
- package/scripts/poll-unconfirmed-notice.mjs +75 -0
- package/scripts/prepare-onboarding.mjs +33 -0
- package/scripts/preview-policy.mjs +65 -0
- package/scripts/random-model-ack-smoke.mjs +235 -0
- package/scripts/random-reject-smoke.mjs +147 -0
- package/scripts/renew.mjs +9 -0
- package/scripts/repair.mjs +10 -0
- package/scripts/revoke.mjs +9 -0
- package/scripts/select-skill-bundle.mjs +39 -0
- package/scripts/setup-local.mjs +199 -0
- package/scripts/status.mjs +13 -0
- package/scripts/test-package.mjs +35 -0
- package/scripts/unlock.mjs +13 -0
- package/scripts/update.mjs +7 -0
- package/scripts/user-setup.mjs +219 -0
- package/scripts/verify-npm-release.mjs +32 -0
- package/src/active-skill.mjs +160 -0
- package/src/anvil-onboarding-state.mjs +96 -0
- package/src/anvil-onboarding.mjs +1036 -0
- package/src/authorization-question-checkpoint.mjs +114 -0
- package/src/autostart-files.mjs +50 -0
- package/src/cache-management-cli.mjs +302 -0
- package/src/cache-management.mjs +47 -0
- package/src/caller-auth.mjs +58 -0
- package/src/client-request-lock.mjs +44 -0
- package/src/connect-market.mjs +408 -0
- package/src/demo-unlock-secret.mjs +64 -0
- package/src/deployment-status.mjs +198 -0
- package/src/file-cache-cli.mjs +83 -0
- package/src/file-cache-owner.mjs +74 -0
- package/src/file-cache-settings.mjs +50 -0
- package/src/file-cache.mjs +790 -0
- package/src/file-download.mjs +281 -0
- package/src/finalization-disclosure.mjs +16 -0
- package/src/guided-setup.mjs +194 -0
- package/src/index.mjs +1233 -0
- package/src/install-and-setup.mjs +38 -0
- package/src/install-preflight.mjs +201 -0
- package/src/install-user.mjs +279 -0
- package/src/installation-paths.mjs +31 -0
- package/src/invocation-result.mjs +148 -0
- package/src/invocation-store.mjs +385 -0
- package/src/local-data-directory.mjs +117 -0
- package/src/local-info.mjs +181 -0
- package/src/local-services.mjs +156 -0
- package/src/local-time.mjs +36 -0
- package/src/local-update.mjs +316 -0
- package/src/local-wallet-ipc.mjs +93 -0
- package/src/login-reminder.mjs +28 -0
- package/src/market-download-url.mjs +21 -0
- package/src/node-version.mjs +12 -0
- package/src/npm-update.mjs +126 -0
- package/src/official-paid-test.mjs +407 -0
- package/src/official-test-package.mjs +41 -0
- package/src/official-test-update.mjs +162 -0
- package/src/order-cancellation.mjs +241 -0
- package/src/order-closure.mjs +42 -0
- package/src/order-list-panel.mjs +110 -0
- package/src/order-review.mjs +424 -0
- package/src/order-status-panel.mjs +65 -0
- package/src/order-support.mjs +365 -0
- package/src/owner-approval-server.mjs +332 -0
- package/src/owner-signer.mjs +67 -0
- package/src/policy-info.mjs +38 -0
- package/src/policy-migration.mjs +84 -0
- package/src/policy-operations.mjs +67 -0
- package/src/policy-preview-server.mjs +236 -0
- package/src/policy-terminal-guide.mjs +377 -0
- package/src/prepare-local-services.mjs +143 -0
- package/src/prepare-onboarding.mjs +210 -0
- package/src/private-journal-files.mjs +29 -0
- package/src/protocol-package.mjs +25 -0
- package/src/readiness-profile.mjs +30 -0
- package/src/renew-policy.mjs +605 -0
- package/src/repair-market-wallet.mjs +137 -0
- package/src/restore-wallet.mjs +255 -0
- package/src/result-cache-view.mjs +276 -0
- package/src/runtime-client.mjs +1145 -0
- package/src/runtime-connection-settings.mjs +101 -0
- package/src/runtime-recovery.mjs +667 -0
- package/src/runtime-status.mjs +32 -0
- package/src/safe-owner-anvil.mjs +115 -0
- package/src/safe-policy-plan.mjs +195 -0
- package/src/service-reference.mjs +233 -0
- package/src/service-url.mjs +52 -0
- package/src/settlement-response.mjs +69 -0
- package/src/setup-autostart.mjs +197 -0
- package/src/setup-demo.mjs +152 -0
- package/src/skill-bundle.mjs +142 -0
- package/src/skill-client.mjs +198 -0
- package/src/skill-install-prompt.mjs +31 -0
- package/src/skill-package.mjs +50 -0
- package/src/skill-zip.mjs +220 -0
- package/src/software-versions.mjs +48 -0
- package/src/status-panel.mjs +603 -0
- package/src/transport.mjs +48 -0
- package/src/uninstall-local.mjs +299 -0
- package/src/user-cli.mjs +354 -0
- package/src/user-setup-guide.mjs +193 -0
- package/src/wallet-control.mjs +182 -0
- package/src/wallet-ipc-error.mjs +16 -0
- package/src/wallet-package.mjs +28 -0
- 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
|
+
}
|