@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.
- package/AGENTS.md +137 -0
- package/CONTRIBUTING.md +16 -0
- package/LICENSE +373 -0
- package/NOTICE.md +20 -0
- package/README.md +84 -2
- package/SECURITY.md +16 -0
- package/doc/README.md +52 -0
- package/doc/TODO.md +97 -0
- package/doc/architecture.md +96 -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 +67 -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 +89 -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 +178 -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 +1231 -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 +306 -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/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/status-panel.mjs +589 -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 +22 -0
- package/update-pair.json +6 -0
|
@@ -0,0 +1,365 @@
|
|
|
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 { showOrderStatus } from "./order-status-panel.mjs";
|
|
5
|
+
import { renderOrderList } from "./order-list-panel.mjs";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import {
|
|
8
|
+
localOrderReviews,
|
|
9
|
+
readPrivateJson,
|
|
10
|
+
findLocalOrder,
|
|
11
|
+
localReviewDetails,
|
|
12
|
+
recordFingerprint,
|
|
13
|
+
observationKey,
|
|
14
|
+
} from "./order-review.mjs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { randomBytes } from "node:crypto";
|
|
17
|
+
import { createConnection } from "node:net";
|
|
18
|
+
import { open } from "node:fs/promises";
|
|
19
|
+
import { constants } from "node:fs";
|
|
20
|
+
import { resolveUserDataDir } from "./local-data-directory.mjs";
|
|
21
|
+
import { readServiceSettings } from "./local-services.mjs";
|
|
22
|
+
import { createInvocationStore } from "./invocation-store.mjs";
|
|
23
|
+
import { invocationResults } from "./invocation-result.mjs";
|
|
24
|
+
import {
|
|
25
|
+
parseServiceReference,
|
|
26
|
+
providerOutput,
|
|
27
|
+
normalizeRuntimeResult,
|
|
28
|
+
} from "./service-reference.mjs";
|
|
29
|
+
import { fetchJsonWithTimeout } from "./transport.mjs";
|
|
30
|
+
import { readSettlementResult } from "./settlement-response.mjs";
|
|
31
|
+
async function privateText(path) {
|
|
32
|
+
const f = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
33
|
+
try {
|
|
34
|
+
const s = await f.stat();
|
|
35
|
+
if (
|
|
36
|
+
!s.isFile() ||
|
|
37
|
+
s.uid !== process.getuid() ||
|
|
38
|
+
s.mode & 0o077 ||
|
|
39
|
+
s.size > 65536
|
|
40
|
+
)
|
|
41
|
+
throw Error("Unsafe local credential");
|
|
42
|
+
return await f.readFile("utf8");
|
|
43
|
+
} finally {
|
|
44
|
+
await f.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function wallet(dataDir, method, params) {
|
|
48
|
+
const token = (await privateText(join(dataDir, "wallet/agent.token"))).trim();
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const socket = createConnection(join(dataDir, "wallet/wallet.sock"));
|
|
51
|
+
let text = "",
|
|
52
|
+
done = false;
|
|
53
|
+
const finish = (e, v) => {
|
|
54
|
+
if (done) return;
|
|
55
|
+
done = true;
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
socket.destroy();
|
|
58
|
+
e ? reject(e) : resolve(v);
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(
|
|
61
|
+
() => finish(Error("Wallet did not respond. Run agentpay status.")),
|
|
62
|
+
40000,
|
|
63
|
+
);
|
|
64
|
+
socket.setEncoding("utf8");
|
|
65
|
+
socket.once("error", (e) => finish(e));
|
|
66
|
+
socket.once("connect", () =>
|
|
67
|
+
socket.write(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
id: randomBytes(8).toString("hex"),
|
|
70
|
+
token,
|
|
71
|
+
method,
|
|
72
|
+
params,
|
|
73
|
+
}) + "\n",
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
socket.on("data", (part) => {
|
|
77
|
+
text += part;
|
|
78
|
+
if (text.length > 8 * 1048576) finish(Error("Wallet response too large"));
|
|
79
|
+
});
|
|
80
|
+
socket.once("end", () => {
|
|
81
|
+
try {
|
|
82
|
+
const result = JSON.parse(text);
|
|
83
|
+
if (!result.ok)
|
|
84
|
+
throw Object.assign(
|
|
85
|
+
Error(result.error?.message || "Wallet rejected this operation"),
|
|
86
|
+
{ code: result.error?.code },
|
|
87
|
+
);
|
|
88
|
+
finish(null, result.result);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
finish(e);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async function askTerminal(message) {
|
|
96
|
+
if (!process.stdin.isTTY)
|
|
97
|
+
throw Error("Run this command in an interactive terminal.");
|
|
98
|
+
const r = createInterface({ input: process.stdin, output: process.stdout });
|
|
99
|
+
try {
|
|
100
|
+
return await r.question(message + "\n> ");
|
|
101
|
+
} finally {
|
|
102
|
+
r.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function runOrderSupport(
|
|
106
|
+
args,
|
|
107
|
+
{
|
|
108
|
+
write = console.log,
|
|
109
|
+
dataDir: chosen,
|
|
110
|
+
ask = askTerminal,
|
|
111
|
+
color = Boolean(process.stdout.isTTY) &&
|
|
112
|
+
process.env.TERM !== "dumb" &&
|
|
113
|
+
!Object.hasOwn(process.env, "NO_COLOR"),
|
|
114
|
+
columns = process.stdout.columns || 80,
|
|
115
|
+
} = {},
|
|
116
|
+
) {
|
|
117
|
+
try {
|
|
118
|
+
const dataDir = chosen || (await resolveUserDataDir()),
|
|
119
|
+
path = join(dataDir, "runtime/invocations.json");
|
|
120
|
+
const store = createInvocationStore(path),
|
|
121
|
+
inventory = await store.inventory();
|
|
122
|
+
const reviews = localOrderReviews(path),
|
|
123
|
+
notes = (await reviews.load()).records;
|
|
124
|
+
const observations =
|
|
125
|
+
(await readPrivateJson(path + ".recovery.json", 16 * 1048576))
|
|
126
|
+
?.observations || {};
|
|
127
|
+
if (args.length === 0) {
|
|
128
|
+
const entries = Object.values(inventory).sort(
|
|
129
|
+
(a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0),
|
|
130
|
+
);
|
|
131
|
+
write(renderOrderList(entries, observations, notes, { color, columns }));
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
const [selector, action = "details"] = args;
|
|
135
|
+
if (
|
|
136
|
+
![
|
|
137
|
+
"details",
|
|
138
|
+
"archive",
|
|
139
|
+
"unarchive",
|
|
140
|
+
"status",
|
|
141
|
+
"case",
|
|
142
|
+
"retrieve",
|
|
143
|
+
].includes(action) ||
|
|
144
|
+
args.length > 2
|
|
145
|
+
) {
|
|
146
|
+
write(
|
|
147
|
+
"Usage: agentpay orders details|status|archive|unarchive|case|retrieve ID",
|
|
148
|
+
);
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
const entry = findLocalOrder(inventory, selector),
|
|
152
|
+
orderId = entry.orderId,
|
|
153
|
+
observation = observations[entry.invocationId],
|
|
154
|
+
review = notes[entry.invocationId];
|
|
155
|
+
if (["details", "archive", "unarchive"].includes(action)) {
|
|
156
|
+
write(
|
|
157
|
+
localReviewDetails(entry, observation, review, {
|
|
158
|
+
selector,
|
|
159
|
+
color,
|
|
160
|
+
columns,
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
if (action === "details") return 0;
|
|
164
|
+
if (action === "unarchive") {
|
|
165
|
+
await reviews.change(entry, observation, { archive: false, note: "" });
|
|
166
|
+
write(
|
|
167
|
+
"Unarchived. Original records and payment permissions are unchanged.",
|
|
168
|
+
);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
write(
|
|
172
|
+
"Archive hides this checked warning, not the record. It does not cancel an Order or stop work already in progress.",
|
|
173
|
+
);
|
|
174
|
+
const note = (
|
|
175
|
+
await ask(
|
|
176
|
+
"Review note (up to 500 characters; no passwords or private content). Type 'q' to return.",
|
|
177
|
+
)
|
|
178
|
+
).trim();
|
|
179
|
+
if (note === "q") return 0;
|
|
180
|
+
if (!note || note.length > 500 || /[\u0000-\u001f\u007f]/.test(note))
|
|
181
|
+
throw Error("Enter a short, single-line review note.");
|
|
182
|
+
if (
|
|
183
|
+
(
|
|
184
|
+
await ask(
|
|
185
|
+
"Type 'confirm' to archive locally, or anything else to return.",
|
|
186
|
+
)
|
|
187
|
+
).trim() !== "confirm"
|
|
188
|
+
)
|
|
189
|
+
return 0;
|
|
190
|
+
const fresh = (await store.inventory())[entry.invocationId],
|
|
191
|
+
latest = (await readPrivateJson(path + ".recovery.json", 16 * 1048576))
|
|
192
|
+
?.observations?.[entry.invocationId];
|
|
193
|
+
if (
|
|
194
|
+
!fresh ||
|
|
195
|
+
recordFingerprint(fresh) !== recordFingerprint(entry) ||
|
|
196
|
+
observationKey(latest) !== observationKey(observation)
|
|
197
|
+
)
|
|
198
|
+
throw Error(
|
|
199
|
+
"The record changed while you were reviewing it. Open details again before archiving.",
|
|
200
|
+
);
|
|
201
|
+
await reviews.change(entry, observation, { archive: true, note });
|
|
202
|
+
write(
|
|
203
|
+
"Archived locally. Same-request execution is blocked until you unarchive it. Status updates on the next local check.",
|
|
204
|
+
);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
if (action === "case") {
|
|
208
|
+
if (!orderId)
|
|
209
|
+
throw Error(
|
|
210
|
+
"No Order ID is saved. Open local details before requesting support.",
|
|
211
|
+
);
|
|
212
|
+
write(
|
|
213
|
+
`Connect this Wallet to your Market account:\n agentpay market connect\nThen open Market → Account → My Orders and choose Order ${orderId}.\nThe connection covers this Wallet authorization; no one-Order access code is needed.`,
|
|
214
|
+
);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (action === "status")
|
|
218
|
+
return showOrderStatus({
|
|
219
|
+
entry,
|
|
220
|
+
observation,
|
|
221
|
+
review,
|
|
222
|
+
selector,
|
|
223
|
+
write,
|
|
224
|
+
read: async () => {
|
|
225
|
+
const settings = await readServiceSettings(dataDir);
|
|
226
|
+
const w = await wallet(dataDir, "status", {}),
|
|
227
|
+
c = entry.paymentPayload?.payload?.commitment;
|
|
228
|
+
const recoveryScope =
|
|
229
|
+
entry.recoveryScope ||
|
|
230
|
+
(c?.policyId && w.recoveryScope
|
|
231
|
+
? {
|
|
232
|
+
...w.recoveryScope,
|
|
233
|
+
policyId: c.policyId,
|
|
234
|
+
safe: c.safe,
|
|
235
|
+
network: entry.paymentPayload.accepted?.network,
|
|
236
|
+
}
|
|
237
|
+
: null);
|
|
238
|
+
const result = readSettlementResult(
|
|
239
|
+
await wallet(dataDir, "readSettlementOrder", {
|
|
240
|
+
settlementUrl: settings.settlementUrl,
|
|
241
|
+
...(orderId
|
|
242
|
+
? { orderId }
|
|
243
|
+
: { orderRequestId: entry.orderRequestId }),
|
|
244
|
+
...(recoveryScope ? { recoveryScope } : {}),
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
return result;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
const settings = await readServiceSettings(dataDir),
|
|
251
|
+
params = { settlementUrl: settings.settlementUrl, orderId };
|
|
252
|
+
if (!orderId)
|
|
253
|
+
throw Error(
|
|
254
|
+
"No Order ID is saved. Open local details; do not start a replacement purchase.",
|
|
255
|
+
);
|
|
256
|
+
const response = await wallet(dataDir, "requestSettlementResult", {
|
|
257
|
+
...params,
|
|
258
|
+
operationId: "0x" + randomBytes(32).toString("hex"),
|
|
259
|
+
}),
|
|
260
|
+
permission = response?.result;
|
|
261
|
+
if (
|
|
262
|
+
!response?.ok ||
|
|
263
|
+
permission?.orderId !== orderId ||
|
|
264
|
+
permission.requestHash !== entry.requestHash
|
|
265
|
+
)
|
|
266
|
+
throw Error("Result retrieval identity mismatch");
|
|
267
|
+
if (entry.routingVersion === "agentpay.runtime-call.v1") {
|
|
268
|
+
const binding = JSON.parse(entry.serviceBinding),
|
|
269
|
+
reference = parseServiceReference(binding.reference);
|
|
270
|
+
if (permission.endpoint !== reference.profile.paymentEndpoint)
|
|
271
|
+
throw Error("Retrieval endpoint differs from the original service");
|
|
272
|
+
}
|
|
273
|
+
if (typeof permission.endpoint !== "string" || !permission.endpoint)
|
|
274
|
+
throw Error(
|
|
275
|
+
"Original Provider endpoint is unavailable. Contact the Provider with this Order ID; no replacement purchase was started.",
|
|
276
|
+
);
|
|
277
|
+
const endpoint = new URL(permission.endpoint);
|
|
278
|
+
if (
|
|
279
|
+
!["http:", "https:"].includes(endpoint.protocol) ||
|
|
280
|
+
endpoint.username ||
|
|
281
|
+
endpoint.password ||
|
|
282
|
+
endpoint.search ||
|
|
283
|
+
endpoint.hash
|
|
284
|
+
)
|
|
285
|
+
throw Error("Provider retrieval endpoint is unsafe");
|
|
286
|
+
const fetched = await fetchJsonWithTimeout(
|
|
287
|
+
endpoint,
|
|
288
|
+
{
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
authorization:
|
|
292
|
+
"AgentPayResult " +
|
|
293
|
+
Buffer.from(JSON.stringify(permission)).toString("base64url"),
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
35000,
|
|
297
|
+
);
|
|
298
|
+
if (!fetched.response.ok || !fetched.result?.ok)
|
|
299
|
+
throw Error(
|
|
300
|
+
fetched.result?.error?.message ||
|
|
301
|
+
"Original result is unavailable; open a case before its deadline.",
|
|
302
|
+
);
|
|
303
|
+
const status = readSettlementResult(
|
|
304
|
+
await wallet(dataDir, "readSettlementOrder", params),
|
|
305
|
+
),
|
|
306
|
+
evidence = status.deliveryEvidence;
|
|
307
|
+
const output = fetched.result.result;
|
|
308
|
+
if (
|
|
309
|
+
!evidence ||
|
|
310
|
+
output?.orderId !== orderId ||
|
|
311
|
+
output.requestHash !== entry.requestHash ||
|
|
312
|
+
output.outputHash !== evidence.outputHash ||
|
|
313
|
+
String(output.usageUnits) !== String(evidence.usageUnits)
|
|
314
|
+
)
|
|
315
|
+
throw Error("Recovered result does not match original delivery evidence");
|
|
316
|
+
if (entry.routingVersion !== "agentpay.runtime-call.v1")
|
|
317
|
+
throw Error(
|
|
318
|
+
"Original result was returned, but this older request needs a reviewed format adapter. No replacement purchase was made.",
|
|
319
|
+
);
|
|
320
|
+
const binding = JSON.parse(entry.serviceBinding),
|
|
321
|
+
reference = parseServiceReference(binding.reference),
|
|
322
|
+
context = {
|
|
323
|
+
reference,
|
|
324
|
+
skillId: reference.skillId,
|
|
325
|
+
profileHash: reference.profileHash,
|
|
326
|
+
requestedMaxUsageUnits: binding.requestedMaxUsageUnits,
|
|
327
|
+
};
|
|
328
|
+
const result = providerOutput(
|
|
329
|
+
fetched.result,
|
|
330
|
+
context,
|
|
331
|
+
null,
|
|
332
|
+
orderId,
|
|
333
|
+
entry.requestHash,
|
|
334
|
+
);
|
|
335
|
+
const restored = normalizeRuntimeResult(
|
|
336
|
+
{
|
|
337
|
+
version: "agentpay.runtime-result.v1",
|
|
338
|
+
orderId,
|
|
339
|
+
requestHash: entry.requestHash,
|
|
340
|
+
service: { skillId: context.skillId, profileHash: context.profileHash },
|
|
341
|
+
output: result.output,
|
|
342
|
+
outputHash: result.outputHash,
|
|
343
|
+
usageUnits: result.usageUnits,
|
|
344
|
+
deliveryEvidenceVerified: true,
|
|
345
|
+
finalization: status.finalization,
|
|
346
|
+
deadlines: status.deadlines,
|
|
347
|
+
},
|
|
348
|
+
context,
|
|
349
|
+
entry.requestHash,
|
|
350
|
+
orderId,
|
|
351
|
+
);
|
|
352
|
+
await invocationResults(path).save(entry, restored);
|
|
353
|
+
write(
|
|
354
|
+
"Original result restored. Resume the original Skill request to read it. No new service execution or payment was requested.",
|
|
355
|
+
);
|
|
356
|
+
return 0;
|
|
357
|
+
} catch (e) {
|
|
358
|
+
write(
|
|
359
|
+
["WALLET_LOCKED", "LOCKED"].includes(e.code)
|
|
360
|
+
? "Wallet is locked. Run agentpay wallet unlock, then repeat this command."
|
|
361
|
+
: String(e.message || e),
|
|
362
|
+
);
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Temporary loopback approval UI for a fixed public plan, not a generic signer.
|
|
2
|
+
// Exported pages must exclude credentials and still verify chain execution.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
5
|
+
// If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { readFile, mkdtemp, writeFile, mkdir, open } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { Interface, isAddress, isHexString } from "ethers";
|
|
14
|
+
|
|
15
|
+
export function validateOwnerPlan({ source, calls }) {
|
|
16
|
+
if (
|
|
17
|
+
!Number.isSafeInteger(source.chainId) ||
|
|
18
|
+
source.chainId < 1 ||
|
|
19
|
+
!isAddress(source.safe) ||
|
|
20
|
+
!isAddress(source.module) ||
|
|
21
|
+
!Array.isArray(calls) ||
|
|
22
|
+
!calls.length ||
|
|
23
|
+
calls.length > 4
|
|
24
|
+
)
|
|
25
|
+
throw Error("Invalid owner plan.");
|
|
26
|
+
if (source.previousModule && !isAddress(source.previousModule))
|
|
27
|
+
throw Error("Invalid previous Module.");
|
|
28
|
+
const module = new Interface([
|
|
29
|
+
"function createPolicy(bytes32,address,address,address,address,uint256,uint256,uint64,uint64)",
|
|
30
|
+
"function revokePolicy(bytes32)",
|
|
31
|
+
]);
|
|
32
|
+
const safe = new Interface(["function enableModule(address)"]);
|
|
33
|
+
for (const c of calls) {
|
|
34
|
+
if (c.value !== "0" || c.operation !== 0 || !isHexString(c.data))
|
|
35
|
+
throw Error("Unsupported owner operation.");
|
|
36
|
+
if (c.to.toLowerCase() === source.safe.toLowerCase()) {
|
|
37
|
+
if (
|
|
38
|
+
safe.decodeFunctionData("enableModule", c.data)[0].toLowerCase() !==
|
|
39
|
+
source.module.toLowerCase()
|
|
40
|
+
)
|
|
41
|
+
throw Error("Module mismatch.");
|
|
42
|
+
} else if (
|
|
43
|
+
c.to.toLowerCase() === source.module.toLowerCase() ||
|
|
44
|
+
(source.previousModule &&
|
|
45
|
+
isAddress(source.previousModule) &&
|
|
46
|
+
c.to.toLowerCase() === source.previousModule.toLowerCase())
|
|
47
|
+
) {
|
|
48
|
+
const parsed = module.parseTransaction({ data: c.data });
|
|
49
|
+
if (
|
|
50
|
+
c.to.toLowerCase() !== source.module.toLowerCase() &&
|
|
51
|
+
parsed?.name !== "revokePolicy"
|
|
52
|
+
)
|
|
53
|
+
throw Error("Previous Module only allows revocation.");
|
|
54
|
+
if (!["createPolicy", "revokePolicy"].includes(parsed?.name))
|
|
55
|
+
throw Error("Unsupported Module operation.");
|
|
56
|
+
} else throw Error("Unsupported owner target.");
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
version: 1,
|
|
60
|
+
chainId: source.chainId,
|
|
61
|
+
safe: source.safe,
|
|
62
|
+
module: source.module,
|
|
63
|
+
...(source.previousModule ? { previousModule: source.previousModule } : {}),
|
|
64
|
+
calls: calls.map((c) => ({
|
|
65
|
+
purpose: c.purpose,
|
|
66
|
+
to: c.to,
|
|
67
|
+
value: "0",
|
|
68
|
+
operation: 0,
|
|
69
|
+
data: c.data,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function ownerAssets(input) {
|
|
74
|
+
const plan = validateOwnerPlan(input);
|
|
75
|
+
const assets = new Map([
|
|
76
|
+
[
|
|
77
|
+
"index.html",
|
|
78
|
+
await readFile(new URL("../web/owner-approval.html", import.meta.url)),
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
"owner-approval.js",
|
|
82
|
+
await readFile(new URL("../web/owner-approval.js", import.meta.url)),
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
"ethers.js",
|
|
86
|
+
await readFile(
|
|
87
|
+
new URL(
|
|
88
|
+
"../node_modules/ethers/dist/ethers.umd.min.js",
|
|
89
|
+
import.meta.url,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"plan.js",
|
|
95
|
+
Buffer.from(
|
|
96
|
+
"window.agentpayOwnerPlan=" +
|
|
97
|
+
JSON.stringify(plan).replace(/</g, "\\u003c") +
|
|
98
|
+
";",
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
]);
|
|
102
|
+
assets.operationCount = plan.calls.length;
|
|
103
|
+
return assets;
|
|
104
|
+
}
|
|
105
|
+
export function createOwnerServer(assets, { saveJournal } = {}) {
|
|
106
|
+
const prefix = "/" + randomBytes(24).toString("hex") + "/";
|
|
107
|
+
const server = createServer(async (req, res) => {
|
|
108
|
+
const host = "127.0.0.1:" + server.address().port;
|
|
109
|
+
res.setHeader("cache-control", "no-store");
|
|
110
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
111
|
+
res.setHeader(
|
|
112
|
+
"content-security-policy",
|
|
113
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
|
|
114
|
+
);
|
|
115
|
+
if (
|
|
116
|
+
req.url === prefix + "journal" &&
|
|
117
|
+
req.method === "POST" &&
|
|
118
|
+
saveJournal
|
|
119
|
+
) {
|
|
120
|
+
if (
|
|
121
|
+
req.headers.host !== host ||
|
|
122
|
+
req.headers.origin !== "http://" + host ||
|
|
123
|
+
req.headers["content-type"] !== "application/json"
|
|
124
|
+
) {
|
|
125
|
+
res.writeHead(403);
|
|
126
|
+
return res.end();
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
let raw = "";
|
|
130
|
+
for await (const chunk of req) {
|
|
131
|
+
raw += chunk;
|
|
132
|
+
if (raw.length > 2048) throw Error();
|
|
133
|
+
}
|
|
134
|
+
const value = JSON.parse(raw);
|
|
135
|
+
if (value !== null) {
|
|
136
|
+
const count = assets.operationCount;
|
|
137
|
+
if (
|
|
138
|
+
value.callIndex !== undefined &&
|
|
139
|
+
(!Number.isInteger(value.callIndex) ||
|
|
140
|
+
value.callIndex < 0 ||
|
|
141
|
+
value.callIndex >= count)
|
|
142
|
+
)
|
|
143
|
+
throw Error();
|
|
144
|
+
if (value.kind === "intent") {
|
|
145
|
+
if (!Number.isInteger(value.callIndex)) throw Error();
|
|
146
|
+
} else if (
|
|
147
|
+
!["approve", "execute"].includes(value.kind) ||
|
|
148
|
+
!/^0x[0-9a-fA-F]{64}$/.test(value.transactionHash) ||
|
|
149
|
+
(value.kind === "execute" &&
|
|
150
|
+
!/^0x[0-9a-fA-F]{64}$/.test(value.safeHash))
|
|
151
|
+
)
|
|
152
|
+
throw Error();
|
|
153
|
+
}
|
|
154
|
+
await saveJournal(value);
|
|
155
|
+
res.end("ok");
|
|
156
|
+
} catch {
|
|
157
|
+
res.writeHead(400);
|
|
158
|
+
res.end();
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
req.headers.host !== host ||
|
|
164
|
+
req.headers["sec-fetch-site"] === "cross-site" ||
|
|
165
|
+
req.method !== "GET" ||
|
|
166
|
+
!req.url.startsWith(prefix)
|
|
167
|
+
) {
|
|
168
|
+
res.writeHead(403);
|
|
169
|
+
return res.end("Open the authorization address shown in your terminal.");
|
|
170
|
+
}
|
|
171
|
+
const name = req.url.slice(prefix.length) || "index.html",
|
|
172
|
+
body = assets.get(name);
|
|
173
|
+
if (!body) {
|
|
174
|
+
res.writeHead(404);
|
|
175
|
+
return res.end();
|
|
176
|
+
}
|
|
177
|
+
res.setHeader(
|
|
178
|
+
"content-type",
|
|
179
|
+
name.endsWith(".js")
|
|
180
|
+
? "application/javascript"
|
|
181
|
+
: "text/html; charset=utf-8",
|
|
182
|
+
);
|
|
183
|
+
res.end(body);
|
|
184
|
+
});
|
|
185
|
+
return { server, prefix };
|
|
186
|
+
}
|
|
187
|
+
export async function exportOwnerPage(assets, dataDir) {
|
|
188
|
+
const parent = join(dataDir, "runtime");
|
|
189
|
+
// The caller's private data directory is established by normal User setup.
|
|
190
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
191
|
+
const folder = await mkdtemp(join(parent, "safe-authorization-"));
|
|
192
|
+
for (const [name, original] of assets) {
|
|
193
|
+
const body =
|
|
194
|
+
name === "plan.js"
|
|
195
|
+
? Buffer.from(
|
|
196
|
+
original
|
|
197
|
+
.toString()
|
|
198
|
+
.replace("window.agentpayJournalServer=true;", ""),
|
|
199
|
+
)
|
|
200
|
+
: original;
|
|
201
|
+
await writeFile(join(folder, name), body, { mode: 0o600, flag: "wx" });
|
|
202
|
+
}
|
|
203
|
+
const script = `import {createServer} from 'node:http';\nimport {readFile} from 'node:fs/promises';\nconst names=new Set(['index.html','plan.js','ethers.js','owner-approval.js']);\nconst s=createServer(async(q,r)=>{const host='127.0.0.1:'+s.address().port;const n=q.url==='/'?'index.html':q.url.slice(1);if(q.method!=='GET'||q.headers.host!==host||q.headers['sec-fetch-site']==='cross-site'||!names.has(n)){r.writeHead(403);return r.end();}r.setHeader('cache-control','no-store');r.setHeader('x-content-type-options','nosniff');r.setHeader('content-type',n.endsWith('.js')?'application/javascript':'text/html; charset=utf-8');r.end(await readFile(new URL(n,import.meta.url)));});\ns.listen(0,'127.0.0.1',()=>console.log('Open http://127.0.0.1:'+s.address().port));\n`;
|
|
204
|
+
await writeFile(join(folder, "serve.mjs"), script, {
|
|
205
|
+
mode: 0o600,
|
|
206
|
+
flag: "wx",
|
|
207
|
+
});
|
|
208
|
+
return folder;
|
|
209
|
+
}
|
|
210
|
+
export function tryBrowser(
|
|
211
|
+
url,
|
|
212
|
+
{ env = process.env, platform = process.platform } = {},
|
|
213
|
+
) {
|
|
214
|
+
const command =
|
|
215
|
+
platform === "darwin"
|
|
216
|
+
? "open"
|
|
217
|
+
: env.DISPLAY || env.WAYLAND_DISPLAY
|
|
218
|
+
? "xdg-open"
|
|
219
|
+
: null;
|
|
220
|
+
if (!command) return false;
|
|
221
|
+
try {
|
|
222
|
+
const child = spawn(command, [url], { stdio: "ignore", detached: true });
|
|
223
|
+
child.on("error", () => {});
|
|
224
|
+
child.unref();
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export async function approveOwnerOperations({
|
|
231
|
+
source,
|
|
232
|
+
calls,
|
|
233
|
+
dataDir,
|
|
234
|
+
ask,
|
|
235
|
+
write = console.log,
|
|
236
|
+
check,
|
|
237
|
+
onSubmitted,
|
|
238
|
+
openBrowser = tryBrowser,
|
|
239
|
+
}) {
|
|
240
|
+
const assets = await ownerAssets({ source, calls });
|
|
241
|
+
const digest = createHash("sha256")
|
|
242
|
+
.update(assets.get("plan.js"))
|
|
243
|
+
.digest("hex");
|
|
244
|
+
const folder = join(dataDir, "runtime");
|
|
245
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
246
|
+
const journalPath = join(folder, "owner-operation-" + digest + ".json");
|
|
247
|
+
let journal = null,
|
|
248
|
+
h;
|
|
249
|
+
try {
|
|
250
|
+
h = await open(journalPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
251
|
+
const st = await h.stat();
|
|
252
|
+
if (
|
|
253
|
+
!st.isFile() ||
|
|
254
|
+
st.uid !== process.getuid() ||
|
|
255
|
+
st.mode & 0o077 ||
|
|
256
|
+
st.size > 2048
|
|
257
|
+
)
|
|
258
|
+
throw Error("Unsafe owner operation journal.");
|
|
259
|
+
journal = JSON.parse(await h.readFile("utf8"));
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (e.code !== "ENOENT") throw e;
|
|
262
|
+
} finally {
|
|
263
|
+
await h?.close();
|
|
264
|
+
}
|
|
265
|
+
const persist = async (value) => {
|
|
266
|
+
if (value) await onSubmitted?.(value);
|
|
267
|
+
const { rename } = await import("node:fs/promises");
|
|
268
|
+
const temp = journalPath + "." + randomBytes(8).toString("hex");
|
|
269
|
+
await writeFile(temp, JSON.stringify(value), { mode: 0o600, flag: "wx" });
|
|
270
|
+
await rename(temp, journalPath);
|
|
271
|
+
};
|
|
272
|
+
assets.set(
|
|
273
|
+
"plan.js",
|
|
274
|
+
Buffer.concat([
|
|
275
|
+
assets.get("plan.js"),
|
|
276
|
+
Buffer.from(
|
|
277
|
+
"window.agentpayOwnerJournal=" +
|
|
278
|
+
JSON.stringify(journal).replace(/</g, "\\u003c") +
|
|
279
|
+
";window.agentpayJournalServer=true;",
|
|
280
|
+
),
|
|
281
|
+
]),
|
|
282
|
+
);
|
|
283
|
+
const { server, prefix } = createOwnerServer(assets, {
|
|
284
|
+
saveJournal: persist,
|
|
285
|
+
});
|
|
286
|
+
await new Promise((resolve, reject) => {
|
|
287
|
+
server.once("error", reject);
|
|
288
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
289
|
+
});
|
|
290
|
+
const port = server.address().port,
|
|
291
|
+
url = "http://127.0.0.1:" + port + prefix;
|
|
292
|
+
write("\n> Approve with your Safe owner wallet\nOpen: " + url);
|
|
293
|
+
openBrowser(url);
|
|
294
|
+
const remoteHelp = `On a headless server, forward port ${port} through SSH, then open the same address on your computer.\nExample: ssh -N -L ${port}:127.0.0.1:${port} USER@SERVER`;
|
|
295
|
+
try {
|
|
296
|
+
for (;;) {
|
|
297
|
+
const choice = (
|
|
298
|
+
await ask(
|
|
299
|
+
"After execution, enter 'check'. Enter 'help' for remote access, 'export' for a portable page, or 'q' to pause.",
|
|
300
|
+
)
|
|
301
|
+
).trim();
|
|
302
|
+
if (choice === "q") return false;
|
|
303
|
+
if (choice === "help") {
|
|
304
|
+
write(remoteHelp);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (choice === "export") {
|
|
308
|
+
await onSubmitted?.({ kind: "export" });
|
|
309
|
+
const folder = await exportOwnerPage(assets, dataDir);
|
|
310
|
+
write(
|
|
311
|
+
`Portable page: ${folder}\nCopy this directory to a trusted computer. In that directory run:\n node serve.mjs\nOr:\n python3 -m http.server 8765 --bind 127.0.0.1\nThen open http://127.0.0.1:8765 (Python) or the address printed by Node.\nKeep the files until this authorization is complete; remove the exported directory afterward.`,
|
|
312
|
+
);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (choice !== "check") continue;
|
|
316
|
+
try {
|
|
317
|
+
if (await check()) {
|
|
318
|
+
write("On-chain operations verified. Continuing.");
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
write(
|
|
322
|
+
"Not complete yet. Finish the Safe operations, then check again.",
|
|
323
|
+
);
|
|
324
|
+
} catch {
|
|
325
|
+
write("Could not verify yet. Check your RPC connection, then retry.");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} finally {
|
|
329
|
+
server.closeAllConnections();
|
|
330
|
+
await new Promise((resolve) => server.close(resolve));
|
|
331
|
+
}
|
|
332
|
+
}
|