@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,1036 @@
|
|
|
1
|
+
import { walletCliPath } from "./wallet-package.mjs";
|
|
2
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
4
|
+
// If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
|
|
5
|
+
import { approveOwnerOperations } from "./owner-approval-server.mjs";
|
|
6
|
+
import { validatePublicDeployment } from "./prepare-onboarding.mjs";
|
|
7
|
+
import { ownerSignerKind, checkTestSigner } from "./owner-signer.mjs";
|
|
8
|
+
import {
|
|
9
|
+
OFFICIAL_TEST_PACKAGE,
|
|
10
|
+
verifyOfficialTestSelection,
|
|
11
|
+
} from "./official-test-package.mjs";
|
|
12
|
+
import { restoreExistingPolicy } from "./restore-wallet.mjs";
|
|
13
|
+
import { finishLocalSetup } from "./setup-autostart.mjs";
|
|
14
|
+
import { saveServiceSettings } from "./local-services.mjs";
|
|
15
|
+
import { saveRuntimeConnection } from "./runtime-client.mjs";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import {
|
|
18
|
+
lstat,
|
|
19
|
+
open,
|
|
20
|
+
readFile,
|
|
21
|
+
writeFile,
|
|
22
|
+
link,
|
|
23
|
+
unlink,
|
|
24
|
+
rename,
|
|
25
|
+
} from "node:fs/promises";
|
|
26
|
+
import { randomUUID } from "node:crypto";
|
|
27
|
+
import { join, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { getAddress, formatUnits, JsonRpcProvider, Contract } from "ethers";
|
|
30
|
+
import { Phase0Wallet } from "@c4roa0/agentpay-wallet/local";
|
|
31
|
+
import { buildSafePolicyPlan, inspectSafePolicy } from "./safe-policy-plan.mjs";
|
|
32
|
+
import { executeAnvilOwnerCall } from "./safe-owner-anvil.mjs";
|
|
33
|
+
import { checkLocalInstallation } from "./install-preflight.mjs";
|
|
34
|
+
import { inspectLocalUserSetup } from "./user-setup-guide.mjs";
|
|
35
|
+
import {
|
|
36
|
+
runPolicyTerminalGuide,
|
|
37
|
+
showAuthorizationSummary,
|
|
38
|
+
} from "./policy-terminal-guide.mjs";
|
|
39
|
+
import { loadActiveSkill } from "./active-skill.mjs";
|
|
40
|
+
import {
|
|
41
|
+
sameWalletPolicy,
|
|
42
|
+
validateGuideTestCheckpoint,
|
|
43
|
+
guideNextAction,
|
|
44
|
+
} from "./anvil-onboarding-state.mjs";
|
|
45
|
+
|
|
46
|
+
const line = "------------------------------------------------------------";
|
|
47
|
+
const userDir = fileURLToPath(new URL("..", import.meta.url));
|
|
48
|
+
const walletCli = walletCliPath();
|
|
49
|
+
const runtimeEntry = fileURLToPath(new URL("./index.mjs", import.meta.url));
|
|
50
|
+
const ownEnv = Object.fromEntries(
|
|
51
|
+
["HOME", "PATH", "TMPDIR", "LANG", "LC_ALL", "TZ"]
|
|
52
|
+
.filter((key) => process.env[key])
|
|
53
|
+
.map((key) => [key, process.env[key]]),
|
|
54
|
+
);
|
|
55
|
+
const localUrl = (value) => {
|
|
56
|
+
const url = new URL(value);
|
|
57
|
+
if (
|
|
58
|
+
url.username ||
|
|
59
|
+
url.password ||
|
|
60
|
+
url.hash ||
|
|
61
|
+
url.search ||
|
|
62
|
+
url.protocol !== "http:" ||
|
|
63
|
+
!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
|
|
64
|
+
)
|
|
65
|
+
throw Error("ANVIL_LOOPBACK_URL_REQUIRED");
|
|
66
|
+
return url.toString();
|
|
67
|
+
};
|
|
68
|
+
export function validateConfig(config) {
|
|
69
|
+
if (!config || !["private-anvil", "production"].includes(config.mode))
|
|
70
|
+
throw Error("DEPLOYMENT_CONFIG_REQUIRED");
|
|
71
|
+
const allowed = new Set([
|
|
72
|
+
"mode",
|
|
73
|
+
"chainId",
|
|
74
|
+
"source",
|
|
75
|
+
"rpcUrl",
|
|
76
|
+
"module",
|
|
77
|
+
"token",
|
|
78
|
+
"factory",
|
|
79
|
+
"registry",
|
|
80
|
+
"tokenDecimals",
|
|
81
|
+
"tokenSymbol",
|
|
82
|
+
"safe",
|
|
83
|
+
"ownerAddress",
|
|
84
|
+
"ownerSigner",
|
|
85
|
+
"settlementUrl",
|
|
86
|
+
"runtimeUrl",
|
|
87
|
+
"marketBundleMetadataUrl",
|
|
88
|
+
"bundleSha256",
|
|
89
|
+
"maxUsageUnits",
|
|
90
|
+
]);
|
|
91
|
+
if (Object.keys(config).some((key) => !allowed.has(key)))
|
|
92
|
+
throw Error("UNEXPECTED_CONFIG_FIELD");
|
|
93
|
+
const { safe, ownerAddress, ownerSigner, runtimeUrl, ...publicConfig } =
|
|
94
|
+
config;
|
|
95
|
+
validatePublicDeployment({
|
|
96
|
+
...publicConfig,
|
|
97
|
+
tokenSymbol: publicConfig.tokenSymbol || "tokens",
|
|
98
|
+
});
|
|
99
|
+
checkTestSigner(ownerSignerKind(ownerSigner), config);
|
|
100
|
+
if (new URL(config.marketBundleMetadataUrl).search)
|
|
101
|
+
throw Error("MARKET_METADATA_URL_QUERY_NOT_ALLOWED");
|
|
102
|
+
if (
|
|
103
|
+
!new URL(localUrl(config.runtimeUrl)).port ||
|
|
104
|
+
new URL(config.runtimeUrl).hostname !== "127.0.0.1"
|
|
105
|
+
)
|
|
106
|
+
throw Error("RUNTIME_LOOPBACK_PORT_REQUIRED");
|
|
107
|
+
ownerSignerKind(config.ownerSigner);
|
|
108
|
+
getAddress(config.ownerAddress);
|
|
109
|
+
if (config.safe) getAddress(config.safe);
|
|
110
|
+
if (!/^[a-f0-9]{64}$/.test(config.bundleSha256 || ""))
|
|
111
|
+
throw Error("BUNDLE_SHA256_REQUIRED");
|
|
112
|
+
if (
|
|
113
|
+
!Number.isInteger(config.tokenDecimals) ||
|
|
114
|
+
config.tokenDecimals < 0 ||
|
|
115
|
+
config.tokenDecimals > 36
|
|
116
|
+
)
|
|
117
|
+
throw Error("TOKEN_DECIMALS_INVALID");
|
|
118
|
+
if (config.maxUsageUnits !== 1)
|
|
119
|
+
throw Error("RANDOM_TEST_REQUIRES_ONE_USAGE_UNIT");
|
|
120
|
+
for (const key of ["module", "token", "factory", "registry"])
|
|
121
|
+
getAddress(config[key]);
|
|
122
|
+
if (typeof config.source !== "string" || !config.source)
|
|
123
|
+
throw Error("DEPLOYMENT_SOURCE_REQUIRED");
|
|
124
|
+
return config;
|
|
125
|
+
}
|
|
126
|
+
async function privateJson(path) {
|
|
127
|
+
const info = await lstat(path).catch((error) => {
|
|
128
|
+
if (error.code === "ENOENT") return null;
|
|
129
|
+
throw error;
|
|
130
|
+
});
|
|
131
|
+
if (!info) return null;
|
|
132
|
+
if (
|
|
133
|
+
!info.isFile() ||
|
|
134
|
+
info.isSymbolicLink() ||
|
|
135
|
+
info.uid !== process.getuid() ||
|
|
136
|
+
info.mode & 0o077
|
|
137
|
+
)
|
|
138
|
+
throw Error("PRIVATE_DRAFT_FILE_UNSAFE");
|
|
139
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
140
|
+
}
|
|
141
|
+
async function runNode(script, args, { env = ownEnv, stdio = "inherit" } = {}) {
|
|
142
|
+
const child = spawn(process.execPath, [script, ...args], { env, stdio });
|
|
143
|
+
const code = await new Promise((resolveExit, reject) => {
|
|
144
|
+
child.once("error", reject);
|
|
145
|
+
child.once("exit", resolveExit);
|
|
146
|
+
});
|
|
147
|
+
if (code !== 0)
|
|
148
|
+
throw Error(`STEP_FAILED: ${script.split("/").at(-1)} (${code})`);
|
|
149
|
+
}
|
|
150
|
+
export function childFailureSummary(stderr) {
|
|
151
|
+
const message = String(stderr).match(/^(?:[A-Za-z]*Error): (.+)$/m)?.[1];
|
|
152
|
+
return message
|
|
153
|
+
? message.slice(0, 400)
|
|
154
|
+
: "The subprocess failed without a readable error message.";
|
|
155
|
+
}
|
|
156
|
+
async function captureNode(script, args) {
|
|
157
|
+
const child = spawn(process.execPath, [script, ...args], {
|
|
158
|
+
env: ownEnv,
|
|
159
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
160
|
+
});
|
|
161
|
+
let output = "",
|
|
162
|
+
error = "";
|
|
163
|
+
for (const [stream, append] of [
|
|
164
|
+
[child.stdout, (part) => (output += part)],
|
|
165
|
+
[child.stderr, (part) => (error += part)],
|
|
166
|
+
])
|
|
167
|
+
stream.on("data", (part) => append(String(part)));
|
|
168
|
+
const code = await new Promise((resolveExit, reject) => {
|
|
169
|
+
child.once("error", reject);
|
|
170
|
+
child.once("exit", resolveExit);
|
|
171
|
+
});
|
|
172
|
+
if (code !== 0)
|
|
173
|
+
throw Error(
|
|
174
|
+
`STEP_FAILED: ${script.split("/").at(-1)} (${code}): ${childFailureSummary(error)}`,
|
|
175
|
+
);
|
|
176
|
+
return JSON.parse(output);
|
|
177
|
+
}
|
|
178
|
+
async function withRpc(config, work) {
|
|
179
|
+
const rpc = new JsonRpcProvider(config.rpcUrl, undefined, {
|
|
180
|
+
batchMaxCount: 1,
|
|
181
|
+
});
|
|
182
|
+
let timer;
|
|
183
|
+
try {
|
|
184
|
+
return await Promise.race([
|
|
185
|
+
work(rpc),
|
|
186
|
+
new Promise((_, reject) => {
|
|
187
|
+
timer = setTimeout(() => reject(Error("ANVIL_RPC_TIMEOUT")), 5000);
|
|
188
|
+
}),
|
|
189
|
+
]);
|
|
190
|
+
} finally {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
rpc.destroy();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function chainState(config, plan) {
|
|
196
|
+
return withRpc(config, (rpc) => inspectSafePolicy(rpc, plan));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function persistPid(path, pid) {
|
|
200
|
+
const info = await lstat(path).catch((error) => {
|
|
201
|
+
if (error.code === "ENOENT") return null;
|
|
202
|
+
throw error;
|
|
203
|
+
});
|
|
204
|
+
if (
|
|
205
|
+
info &&
|
|
206
|
+
(!info.isFile() ||
|
|
207
|
+
info.isSymbolicLink() ||
|
|
208
|
+
info.uid !== process.getuid() ||
|
|
209
|
+
info.mode & 0o077)
|
|
210
|
+
)
|
|
211
|
+
throw Error("LOCAL_PID_FILE_UNSAFE");
|
|
212
|
+
await writeFile(path, `${pid}\n`, { flag: info ? "w" : "wx", mode: 0o600 });
|
|
213
|
+
}
|
|
214
|
+
async function startLocal(script, args, env, probe, pidPath) {
|
|
215
|
+
const logPath = pidPath.replace(/\.pid$/, ".log");
|
|
216
|
+
const previous = await lstat(logPath).catch((error) => {
|
|
217
|
+
if (error.code === "ENOENT") return null;
|
|
218
|
+
throw error;
|
|
219
|
+
});
|
|
220
|
+
if (
|
|
221
|
+
previous &&
|
|
222
|
+
(!previous.isFile() ||
|
|
223
|
+
previous.isSymbolicLink() ||
|
|
224
|
+
previous.uid !== process.getuid() ||
|
|
225
|
+
previous.mode & 0o077)
|
|
226
|
+
)
|
|
227
|
+
throw Error("LOCAL_LOG_FILE_UNSAFE");
|
|
228
|
+
const log = await open(logPath, "a", 0o600);
|
|
229
|
+
let child, started;
|
|
230
|
+
try {
|
|
231
|
+
child = spawn(process.execPath, [script, ...args], {
|
|
232
|
+
env,
|
|
233
|
+
stdio: ["ignore", log.fd, log.fd],
|
|
234
|
+
detached: true,
|
|
235
|
+
});
|
|
236
|
+
started = childReady(child, probe);
|
|
237
|
+
started.catch(() => {});
|
|
238
|
+
} finally {
|
|
239
|
+
await log.close();
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
await started;
|
|
243
|
+
await persistPid(pidPath, child.pid);
|
|
244
|
+
child.unref();
|
|
245
|
+
return child.pid;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (child.exitCode === null) child.kill("SIGTERM");
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function childReady(child, probe) {
|
|
252
|
+
await new Promise((resolveSpawn, reject) => {
|
|
253
|
+
child.once("spawn", resolveSpawn);
|
|
254
|
+
child.once("error", reject);
|
|
255
|
+
});
|
|
256
|
+
for (let i = 0; i < 30; i++) {
|
|
257
|
+
if (child.exitCode !== null) throw Error("LOCAL_PROCESS_EXITED");
|
|
258
|
+
if (await probe().catch(() => false)) return;
|
|
259
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 200));
|
|
260
|
+
}
|
|
261
|
+
throw Error("LOCAL_PROCESS_START_TIMEOUT");
|
|
262
|
+
}
|
|
263
|
+
async function walletStatus(dataDir) {
|
|
264
|
+
return captureNode(walletCli, ["status", join(dataDir, "wallet")]);
|
|
265
|
+
}
|
|
266
|
+
async function runtimeHealth(url, skill) {
|
|
267
|
+
const response = await fetch(new URL("/health", url), {
|
|
268
|
+
redirect: "error",
|
|
269
|
+
signal: AbortSignal.timeout(1500),
|
|
270
|
+
});
|
|
271
|
+
const body = await response.json();
|
|
272
|
+
if (
|
|
273
|
+
body?.ok !== true ||
|
|
274
|
+
!response.ok ||
|
|
275
|
+
body?.result?.role !== "user-runtime"
|
|
276
|
+
)
|
|
277
|
+
throw Error("USER_RUNTIME_HEALTH_INVALID");
|
|
278
|
+
if (!body.result.capabilities?.includes("agentpay.runtime-call.v1"))
|
|
279
|
+
throw Error("USER_RUNTIME_UPGRADE_REQUIRED");
|
|
280
|
+
return body.result;
|
|
281
|
+
}
|
|
282
|
+
async function promptChoice(ask, prompt, choices) {
|
|
283
|
+
while (true) {
|
|
284
|
+
const value = (await ask(`${prompt ? prompt + "\n" : ""}> `))
|
|
285
|
+
.trim()
|
|
286
|
+
.toLowerCase();
|
|
287
|
+
if (choices.includes(value)) return value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export async function saveNewDraft(path, value) {
|
|
291
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
292
|
+
try {
|
|
293
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
294
|
+
try {
|
|
295
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
296
|
+
await handle.sync();
|
|
297
|
+
} finally {
|
|
298
|
+
await handle.close();
|
|
299
|
+
}
|
|
300
|
+
// Publish only complete bytes; an existing draft is never overwritten.
|
|
301
|
+
await link(temporary, path);
|
|
302
|
+
} finally {
|
|
303
|
+
await unlink(temporary).catch((error) => {
|
|
304
|
+
if (error.code !== "ENOENT") throw error;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export async function saveAuthorizationInputs(path, value) {
|
|
309
|
+
const temporary = `${path}.${randomUUID()}.next`;
|
|
310
|
+
try {
|
|
311
|
+
await privateJson(path);
|
|
312
|
+
await saveNewDraft(temporary, value);
|
|
313
|
+
await rename(temporary, path);
|
|
314
|
+
} finally {
|
|
315
|
+
await unlink(temporary).catch((error) => {
|
|
316
|
+
if (error.code !== "ENOENT") throw error;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function bindViaWallet(dataDir, policy) {
|
|
321
|
+
const temporary = join(
|
|
322
|
+
dataDir,
|
|
323
|
+
"runtime",
|
|
324
|
+
`wallet-bind-${randomUUID()}.json`,
|
|
325
|
+
);
|
|
326
|
+
try {
|
|
327
|
+
await writeFile(temporary, `${JSON.stringify(policy)}\n`, {
|
|
328
|
+
flag: "wx",
|
|
329
|
+
mode: 0o600,
|
|
330
|
+
});
|
|
331
|
+
await captureNode(walletCli, ["bind", join(dataDir, "wallet"), temporary]);
|
|
332
|
+
} finally {
|
|
333
|
+
await unlink(temporary).catch((error) => {
|
|
334
|
+
if (error.code !== "ENOENT") throw error;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export async function runAnvilOnboarding({
|
|
339
|
+
dataDir,
|
|
340
|
+
config,
|
|
341
|
+
ask,
|
|
342
|
+
write,
|
|
343
|
+
operations = {},
|
|
344
|
+
}) {
|
|
345
|
+
// Dependencies allow focused orchestration tests; the CLI uses these real defaults.
|
|
346
|
+
const ops = {
|
|
347
|
+
approveOwnerOperations,
|
|
348
|
+
checkLocalInstallation,
|
|
349
|
+
inspectLocalUserSetup,
|
|
350
|
+
privateJson,
|
|
351
|
+
loadActiveSkill,
|
|
352
|
+
openWallet: (path) => Phase0Wallet.open(path),
|
|
353
|
+
walletStatus,
|
|
354
|
+
runtimeHealth,
|
|
355
|
+
chainState,
|
|
356
|
+
clearDraft: async (path) => {
|
|
357
|
+
await privateJson(path);
|
|
358
|
+
await unlink(path);
|
|
359
|
+
},
|
|
360
|
+
restoreExistingPolicy,
|
|
361
|
+
runNode,
|
|
362
|
+
captureNode,
|
|
363
|
+
startLocal,
|
|
364
|
+
executeAnvilOwnerCall,
|
|
365
|
+
saveServiceSettings,
|
|
366
|
+
finishLocalSetup,
|
|
367
|
+
saveNewDraft,
|
|
368
|
+
saveAuthorizationInputs,
|
|
369
|
+
bindViaWallet,
|
|
370
|
+
saveRuntimeConnection,
|
|
371
|
+
networkFacts: async (config) =>
|
|
372
|
+
withRpc(config, async (rpc) => {
|
|
373
|
+
const chainId = await rpc.send("eth_chainId", []);
|
|
374
|
+
if (BigInt(chainId) !== BigInt(config.chainId))
|
|
375
|
+
throw Error("CHAIN_ID_MISMATCH");
|
|
376
|
+
const token = new Contract(
|
|
377
|
+
config.token,
|
|
378
|
+
[
|
|
379
|
+
"function decimals() view returns(uint8)",
|
|
380
|
+
"function symbol() view returns(string)",
|
|
381
|
+
],
|
|
382
|
+
rpc,
|
|
383
|
+
);
|
|
384
|
+
return {
|
|
385
|
+
decimals: Number(await token.decimals()),
|
|
386
|
+
symbol: await token.symbol(),
|
|
387
|
+
};
|
|
388
|
+
}),
|
|
389
|
+
...operations,
|
|
390
|
+
};
|
|
391
|
+
validateConfig(config);
|
|
392
|
+
dataDir = resolve(dataDir);
|
|
393
|
+
const draftPath = join(dataDir, "runtime", "authorization-draft.json");
|
|
394
|
+
const walletDir = join(dataDir, "wallet");
|
|
395
|
+
const step = (title, message) =>
|
|
396
|
+
write(`\n${line}\n${title}\n${line}${message ? "\n" + message : ""}`);
|
|
397
|
+
const script = (name) =>
|
|
398
|
+
fileURLToPath(new URL(`../scripts/${name}`, import.meta.url));
|
|
399
|
+
const initial = await ops.checkLocalInstallation({ userDir, dataDir });
|
|
400
|
+
if (!initial.ok) {
|
|
401
|
+
for (const item of initial.checks.filter(
|
|
402
|
+
(item) => item.status === "failed",
|
|
403
|
+
))
|
|
404
|
+
write(`${item.message}. ${item.fix || ""}`);
|
|
405
|
+
return { completed: false, stage: "preflight" };
|
|
406
|
+
}
|
|
407
|
+
let tokenFacts;
|
|
408
|
+
try {
|
|
409
|
+
tokenFacts = await ops.networkFacts(config);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const codes = [
|
|
412
|
+
error.code,
|
|
413
|
+
error.cause?.code,
|
|
414
|
+
error.error?.code,
|
|
415
|
+
error.message,
|
|
416
|
+
];
|
|
417
|
+
if (
|
|
418
|
+
!codes.some((code) =>
|
|
419
|
+
[
|
|
420
|
+
"ECONNREFUSED",
|
|
421
|
+
"ETIMEDOUT",
|
|
422
|
+
"EHOSTUNREACH",
|
|
423
|
+
"ENETUNREACH",
|
|
424
|
+
"ANVIL_RPC_TIMEOUT",
|
|
425
|
+
].includes(code),
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
throw error;
|
|
429
|
+
const rpc = new URL(config.rpcUrl);
|
|
430
|
+
step(
|
|
431
|
+
"Setup paused · Network unavailable",
|
|
432
|
+
`Cannot reach the configured RPC at ${rpc.origin}.\nCheck your network and RPC settings, then run the same Install command again.\nYour saved settings are kept.`,
|
|
433
|
+
);
|
|
434
|
+
return { completed: false, stage: "network_unavailable" };
|
|
435
|
+
}
|
|
436
|
+
if (
|
|
437
|
+
tokenFacts.decimals !== config.tokenDecimals ||
|
|
438
|
+
(config.tokenSymbol && tokenFacts.symbol !== config.tokenSymbol)
|
|
439
|
+
)
|
|
440
|
+
throw Error("TOKEN_METADATA_MISMATCH");
|
|
441
|
+
step(
|
|
442
|
+
"AgentPay · Wallet setup",
|
|
443
|
+
`Connected to network eip155:${config.chainId}.`,
|
|
444
|
+
);
|
|
445
|
+
let checkedFor = null,
|
|
446
|
+
advancing = false;
|
|
447
|
+
while (true) {
|
|
448
|
+
const preflight = await ops.checkLocalInstallation({ userDir, dataDir });
|
|
449
|
+
if (!preflight.ok) {
|
|
450
|
+
for (const item of preflight.checks.filter(
|
|
451
|
+
(item) => item.status === "failed",
|
|
452
|
+
))
|
|
453
|
+
write(`${item.message}. ${item.fix}`);
|
|
454
|
+
return { completed: false, stage: "preflight" };
|
|
455
|
+
}
|
|
456
|
+
const local = await ops.inspectLocalUserSetup(dataDir);
|
|
457
|
+
const saved = await ops.privateJson(draftPath);
|
|
458
|
+
if (
|
|
459
|
+
saved &&
|
|
460
|
+
(saved.version !== 1 ||
|
|
461
|
+
saved.delegate?.toLowerCase() !== local.delegate?.toLowerCase() ||
|
|
462
|
+
saved.chainId !== config.chainId ||
|
|
463
|
+
saved.ownerAddress?.toLowerCase() !==
|
|
464
|
+
config.ownerAddress.toLowerCase() ||
|
|
465
|
+
["module", "token", "factory", "registry"].some(
|
|
466
|
+
(key) =>
|
|
467
|
+
String(saved[key]).toLowerCase() !== config[key].toLowerCase(),
|
|
468
|
+
) ||
|
|
469
|
+
new URL(saved.rpcUrl).href !== new URL(config.rpcUrl).href)
|
|
470
|
+
) {
|
|
471
|
+
step(
|
|
472
|
+
"Setup paused",
|
|
473
|
+
"The saved draft belongs to different deployment or Wallet data. Restore its matching configuration; preserve the draft and keys.",
|
|
474
|
+
);
|
|
475
|
+
return { completed: false, stage: "draft_conflict" };
|
|
476
|
+
}
|
|
477
|
+
let plan = null;
|
|
478
|
+
if (saved)
|
|
479
|
+
try {
|
|
480
|
+
plan = buildSafePolicyPlan(saved, local.delegate);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
step(
|
|
483
|
+
"Setup paused",
|
|
484
|
+
`Saved authorization draft cannot be used: ${error.message}. Preserve it and review renewal or the matching configuration.`,
|
|
485
|
+
);
|
|
486
|
+
return { completed: false, stage: "draft_invalid" };
|
|
487
|
+
}
|
|
488
|
+
let chain = null,
|
|
489
|
+
chainProblem = null;
|
|
490
|
+
if (plan)
|
|
491
|
+
try {
|
|
492
|
+
chain = await ops.chainState(config, plan);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
chainProblem = error.message;
|
|
495
|
+
}
|
|
496
|
+
const wallet = local.delegate ? await ops.openWallet(walletDir) : null;
|
|
497
|
+
const bound = Boolean(
|
|
498
|
+
wallet?.policy &&
|
|
499
|
+
plan &&
|
|
500
|
+
sameWalletPolicy(wallet.policy, plan.walletPolicy),
|
|
501
|
+
);
|
|
502
|
+
if (wallet?.policy && !bound) {
|
|
503
|
+
step(
|
|
504
|
+
"Setup paused",
|
|
505
|
+
"Wallet already has a Policy that does not match a saved guide draft. Preserve the key and binding. Restore the original matching draft, or use the documented Policy migration path; initialization will not replace it.",
|
|
506
|
+
);
|
|
507
|
+
return { completed: false, stage: "policy_conflict" };
|
|
508
|
+
}
|
|
509
|
+
if (saved?.restoring && (!chain?.moduleEnabled || !chain?.policyMatches)) {
|
|
510
|
+
step(
|
|
511
|
+
"Restore paused",
|
|
512
|
+
"The original Policy could not be verified. Check its network, expiry and revocation status. No new authorization will be created.",
|
|
513
|
+
);
|
|
514
|
+
return { completed: false, stage: "restore_unavailable" };
|
|
515
|
+
}
|
|
516
|
+
const selectedPath = join(dataDir, "runtime", "active-skill.json");
|
|
517
|
+
const installed = await ops.loadActiveSkill(selectedPath).catch((error) => {
|
|
518
|
+
if (error.code === "ENOENT") return null;
|
|
519
|
+
throw error;
|
|
520
|
+
});
|
|
521
|
+
const liveWallet = await ops.walletStatus(dataDir).catch(() => null);
|
|
522
|
+
const liveRuntime = await ops
|
|
523
|
+
.runtimeHealth(config.runtimeUrl, installed)
|
|
524
|
+
.catch((error) => {
|
|
525
|
+
if (error.message === "USER_RUNTIME_SKILL_MISMATCH") throw error;
|
|
526
|
+
return null;
|
|
527
|
+
});
|
|
528
|
+
const ready =
|
|
529
|
+
bound && liveWallet && !liveWallet.locked && liveRuntime && installed;
|
|
530
|
+
const checkpoint = await ops.privateJson(
|
|
531
|
+
join(dataDir, "runtime", "model-ack-smoke.json"),
|
|
532
|
+
);
|
|
533
|
+
verifyOfficialTestSelection(installed, checkpoint, config.bundleSha256);
|
|
534
|
+
let completed = null;
|
|
535
|
+
if (checkpoint) {
|
|
536
|
+
try {
|
|
537
|
+
completed = validateGuideTestCheckpoint(
|
|
538
|
+
checkpoint,
|
|
539
|
+
await ops.privateJson(join(dataDir, "runtime", "invocations.json")),
|
|
540
|
+
installed,
|
|
541
|
+
);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
step(
|
|
544
|
+
"Test recovery paused",
|
|
545
|
+
`${error.message}. Preserve the test checkpoint and invocation history; restore their matching Skill before retrying. No replacement purchase was started.`,
|
|
546
|
+
);
|
|
547
|
+
return { completed: false, stage: "test_conflict" };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const readinessIdentity = JSON.stringify({
|
|
551
|
+
policy: plan?.policyId,
|
|
552
|
+
bundle: installed?.bundle,
|
|
553
|
+
unlocked: Boolean(liveWallet && !liveWallet.locked),
|
|
554
|
+
runtime: Boolean(liveRuntime),
|
|
555
|
+
});
|
|
556
|
+
const next = guideNextAction({
|
|
557
|
+
initialized: local.stage === "initialized",
|
|
558
|
+
plan,
|
|
559
|
+
chain,
|
|
560
|
+
bound,
|
|
561
|
+
installed,
|
|
562
|
+
liveWallet,
|
|
563
|
+
liveRuntime,
|
|
564
|
+
checked: checkedFor === readinessIdentity,
|
|
565
|
+
test: completed,
|
|
566
|
+
});
|
|
567
|
+
if (!advancing)
|
|
568
|
+
step(
|
|
569
|
+
"Your progress",
|
|
570
|
+
` Wallet: ${local.stage === "initialized" ? "Initialized" : "Not initialized"}\n Chain Policy: ${chain?.policyMatches ? "Verified" : chain?.moduleEnabled ? "Module enabled; Policy pending" : plan ? "Draft saved; authorization pending" : "Not configured"}\n Local binding: ${bound ? "Verified" : "Pending"}\n Official Skill: ${installed ? "Installed" : "Pending"}\n Wallet / Runtime: ${liveWallet && !liveWallet.locked ? "Wallet unlocked" : "Not ready"} / ${liveRuntime ? "Running" : "Not running"}`,
|
|
571
|
+
);
|
|
572
|
+
if (chainProblem)
|
|
573
|
+
write(
|
|
574
|
+
`Chain status unavailable: ${chainProblem}. Check your RPC connection before authorization or binding.`,
|
|
575
|
+
);
|
|
576
|
+
const stage = ["initialize"].includes(next)
|
|
577
|
+
? "1/4 Create local Wallet"
|
|
578
|
+
: ["limits", "approve", "bind"].includes(next)
|
|
579
|
+
? "2/4 Set spending permission"
|
|
580
|
+
: ["install", "start", "check"].includes(next)
|
|
581
|
+
? "3/4 Prepare the official test Skill"
|
|
582
|
+
: completed?.confirmed
|
|
583
|
+
? "4/4 Verify the completed test"
|
|
584
|
+
: completed?.hasOrder
|
|
585
|
+
? "4/4 Continue the existing test"
|
|
586
|
+
: "4/4 Run the paid test";
|
|
587
|
+
if (!advancing) {
|
|
588
|
+
step(
|
|
589
|
+
"> Continue setup",
|
|
590
|
+
`${stage}\n * 'continue' Resume the next incomplete step\n 'details' Inspect current state\n 'q' Quit`,
|
|
591
|
+
);
|
|
592
|
+
const choice = await promptChoice(ask, "", ["continue", "details", "q"]);
|
|
593
|
+
if (choice === "q") return { completed: false, stage: "quit", next };
|
|
594
|
+
if (choice === "details") {
|
|
595
|
+
write(
|
|
596
|
+
JSON.stringify(
|
|
597
|
+
{
|
|
598
|
+
config,
|
|
599
|
+
delegate: local.delegate,
|
|
600
|
+
policy: saved,
|
|
601
|
+
chain,
|
|
602
|
+
installed,
|
|
603
|
+
liveWallet,
|
|
604
|
+
liveRuntime,
|
|
605
|
+
},
|
|
606
|
+
null,
|
|
607
|
+
2,
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const action = next;
|
|
614
|
+
advancing = true;
|
|
615
|
+
try {
|
|
616
|
+
if (action === "initialize") {
|
|
617
|
+
if (local.stage === "initialized") {
|
|
618
|
+
write("Existing Wallet preserved.");
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
step(
|
|
622
|
+
"1/4 Create local Wallet",
|
|
623
|
+
"Wallet will ask for an unlock secret twice, with hidden input.",
|
|
624
|
+
);
|
|
625
|
+
await ops.runNode(script("setup-local.mjs"), ["install", dataDir]);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (action === "limits") {
|
|
629
|
+
if (!local.delegate) {
|
|
630
|
+
write("Initialize Wallet first.");
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (saved) {
|
|
634
|
+
write(
|
|
635
|
+
"An authorization draft already exists. It is preserved for recovery.",
|
|
636
|
+
);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const inputPath = join(dataDir, "runtime", "authorization-fields.json");
|
|
640
|
+
const inputs = await ops.privateJson(inputPath);
|
|
641
|
+
if (
|
|
642
|
+
!inputs &&
|
|
643
|
+
(await ops.privateJson(
|
|
644
|
+
join(dataDir, "runtime", "authorization-inputs.json"),
|
|
645
|
+
))
|
|
646
|
+
) {
|
|
647
|
+
write(
|
|
648
|
+
"An older unfinished questionnaire was found. Its file is preserved. Re-enter Safe and limits once to use resumable fields.",
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const result = await runPolicyTerminalGuide({
|
|
652
|
+
deployment: config,
|
|
653
|
+
delegate: local.delegate,
|
|
654
|
+
collectOnly: true,
|
|
655
|
+
checkpoint: inputs,
|
|
656
|
+
restoreExisting: (target) =>
|
|
657
|
+
ops.restoreExistingPolicy({
|
|
658
|
+
dataDir,
|
|
659
|
+
target,
|
|
660
|
+
ask,
|
|
661
|
+
write,
|
|
662
|
+
expectedDeployment: config,
|
|
663
|
+
previewOnly: true,
|
|
664
|
+
}),
|
|
665
|
+
saveInputs: (value) => ops.saveAuthorizationInputs(inputPath, value),
|
|
666
|
+
ask,
|
|
667
|
+
write,
|
|
668
|
+
inspect: async (plan) => ops.chainState(config, plan),
|
|
669
|
+
});
|
|
670
|
+
if (result.cancelled || result.needsSafe || result.chainError) {
|
|
671
|
+
advancing = false;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const p = result.policyConfig;
|
|
675
|
+
const state = await ops.chainState(
|
|
676
|
+
config,
|
|
677
|
+
buildSafePolicyPlan(p, local.delegate),
|
|
678
|
+
);
|
|
679
|
+
if (
|
|
680
|
+
!result.restored &&
|
|
681
|
+
(!state.owners
|
|
682
|
+
.map((value) => value.toLowerCase())
|
|
683
|
+
.includes(config.ownerAddress.toLowerCase()) ||
|
|
684
|
+
state.threshold !== 1)
|
|
685
|
+
)
|
|
686
|
+
throw Error("ANVIL_SAFE_OWNER_OR_THRESHOLD_MISMATCH");
|
|
687
|
+
await ops.saveNewDraft(draftPath, {
|
|
688
|
+
...p,
|
|
689
|
+
version: 1,
|
|
690
|
+
delegate: local.delegate,
|
|
691
|
+
ownerAddress: config.ownerAddress,
|
|
692
|
+
...(result.restored ? { restoring: true } : {}),
|
|
693
|
+
});
|
|
694
|
+
write("Draft saved. Review your spending permission next.");
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (!plan) {
|
|
698
|
+
write("Choose authorization limits first.");
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (action === "approve") {
|
|
702
|
+
if (saved?.restoring)
|
|
703
|
+
throw Error("RESTORE_CANNOT_CREATE_AUTHORIZATION");
|
|
704
|
+
if (chainProblem) throw Error("CHAIN_STATE_UNAVAILABLE");
|
|
705
|
+
if (chain?.policyExists && !chain.policyMatches)
|
|
706
|
+
throw Error("EXISTING_CHAIN_POLICY_CONFLICT");
|
|
707
|
+
const work = [
|
|
708
|
+
...(!chain?.moduleEnabled ? [plan.calls[0]] : []),
|
|
709
|
+
...(!chain?.policyMatches ? [plan.calls[1]] : []),
|
|
710
|
+
];
|
|
711
|
+
if (!work.length) {
|
|
712
|
+
write("Chain Policy already verified.");
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
step("2/4 Review spending permission", "");
|
|
716
|
+
showAuthorizationSummary(plan, {
|
|
717
|
+
write,
|
|
718
|
+
currency: config.tokenSymbol || "tokens",
|
|
719
|
+
decimals: config.tokenDecimals,
|
|
720
|
+
});
|
|
721
|
+
write(
|
|
722
|
+
"\nConfirm the spending authorization operations.\nWallet will verify the authorization before using it.\n",
|
|
723
|
+
);
|
|
724
|
+
let consent;
|
|
725
|
+
do {
|
|
726
|
+
consent = await promptChoice(
|
|
727
|
+
ask,
|
|
728
|
+
"Type 'confirm' to authorize, 'back' to change the draft, 'details' for transaction data, or 'q' to return",
|
|
729
|
+
["confirm", "back", "details", "q"],
|
|
730
|
+
);
|
|
731
|
+
if (consent === "details")
|
|
732
|
+
write(
|
|
733
|
+
JSON.stringify({ policyId: plan.policyId, calls: work }, null, 2),
|
|
734
|
+
);
|
|
735
|
+
} while (consent === "details");
|
|
736
|
+
if (consent === "back") {
|
|
737
|
+
const latest = await ops.chainState(config, plan);
|
|
738
|
+
if (latest.policyExists) {
|
|
739
|
+
write(
|
|
740
|
+
"This Policy already exists on-chain. Its limits cannot be edited.",
|
|
741
|
+
);
|
|
742
|
+
advancing = false;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const inputPath = join(
|
|
746
|
+
dataDir,
|
|
747
|
+
"runtime",
|
|
748
|
+
"authorization-fields.json",
|
|
749
|
+
);
|
|
750
|
+
const inputs = await ops.privateJson(inputPath);
|
|
751
|
+
if (inputs) {
|
|
752
|
+
const { days, ...previous } = inputs.fields;
|
|
753
|
+
await ops.saveAuthorizationInputs(inputPath, {
|
|
754
|
+
...inputs,
|
|
755
|
+
fields: previous,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
await ops.clearDraft(draftPath);
|
|
759
|
+
advancing = true;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (consent === "q") {
|
|
763
|
+
advancing = false;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const signer = ownerSignerKind(
|
|
767
|
+
process.env.AGENTPAY_OWNER_SIGNER ?? config.ownerSigner,
|
|
768
|
+
);
|
|
769
|
+
checkTestSigner(signer, config);
|
|
770
|
+
await ops.saveAuthorizationInputs(
|
|
771
|
+
join(dataDir, "runtime", "owner-signer.json"),
|
|
772
|
+
{ version: 1, kind: signer },
|
|
773
|
+
);
|
|
774
|
+
if (signer === "external") {
|
|
775
|
+
const done = await ops.approveOwnerOperations({
|
|
776
|
+
source: {
|
|
777
|
+
chainId: config.chainId,
|
|
778
|
+
safe: plan.safe,
|
|
779
|
+
module: config.module,
|
|
780
|
+
},
|
|
781
|
+
calls: work,
|
|
782
|
+
dataDir,
|
|
783
|
+
ask,
|
|
784
|
+
write,
|
|
785
|
+
check: async () => {
|
|
786
|
+
const state = await ops.chainState(config, plan);
|
|
787
|
+
return state.moduleEnabled && state.policyMatches;
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
if (!done) return { completed: false, stage: "owner_approval" };
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
for (const call of work) {
|
|
794
|
+
await ops.executeAnvilOwnerCall(
|
|
795
|
+
{
|
|
796
|
+
source: {
|
|
797
|
+
chainId: config.chainId,
|
|
798
|
+
registryRpcUrl: config.rpcUrl,
|
|
799
|
+
safe: plan.safe,
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
config.ownerAddress,
|
|
803
|
+
call,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const verified = await ops.chainState(config, plan);
|
|
807
|
+
if (!verified.moduleEnabled || !verified.policyMatches)
|
|
808
|
+
throw Error("CHAIN_POLICY_NOT_VERIFIED");
|
|
809
|
+
write("Spending permission verified on-chain.");
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (action === "bind") {
|
|
813
|
+
if (!chain?.moduleEnabled || !chain.policyMatches) {
|
|
814
|
+
advancing = false;
|
|
815
|
+
write("Chain Policy must be verified before Wallet binding.");
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (bound) {
|
|
819
|
+
write("Local Wallet already matches this Policy.");
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (!liveWallet)
|
|
823
|
+
await ops.startLocal(
|
|
824
|
+
walletCli,
|
|
825
|
+
["serve", walletDir],
|
|
826
|
+
ownEnv,
|
|
827
|
+
() => ops.walletStatus(dataDir),
|
|
828
|
+
join(dataDir, "runtime", "wallet-guide.pid"),
|
|
829
|
+
);
|
|
830
|
+
if (saved?.restoring)
|
|
831
|
+
await ops.restoreExistingPolicy({
|
|
832
|
+
dataDir,
|
|
833
|
+
target: {
|
|
834
|
+
chainId: saved.chainId,
|
|
835
|
+
rpcUrl: saved.rpcUrl,
|
|
836
|
+
module: saved.module,
|
|
837
|
+
safe: saved.safe,
|
|
838
|
+
policyId: saved.policyId,
|
|
839
|
+
},
|
|
840
|
+
ask,
|
|
841
|
+
write,
|
|
842
|
+
expectedDeployment: config,
|
|
843
|
+
});
|
|
844
|
+
else await ops.bindViaWallet(dataDir, plan.walletPolicy);
|
|
845
|
+
write("Wallet bound to the verified chain Policy.");
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (!bound) {
|
|
849
|
+
write("Complete chain authorization and local binding first.");
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (action === "install" && config.chainId !== 31337) {
|
|
853
|
+
write(
|
|
854
|
+
"Wallet authorization is ready. The official connectivity package for this network is not configured yet. Your binding is kept.",
|
|
855
|
+
);
|
|
856
|
+
return { completed: false, stage: "official_test_unavailable" };
|
|
857
|
+
}
|
|
858
|
+
if (action === "install") {
|
|
859
|
+
if (installed) {
|
|
860
|
+
write("Official Skill already installed.");
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
step(
|
|
864
|
+
"3/4 Prepare the official test Skill",
|
|
865
|
+
"Preparing the official test Skill automatically. Source and package checks must pass; payment requires your confirmation later.",
|
|
866
|
+
);
|
|
867
|
+
await ops.runNode(script("install-skill-bundle.mjs"), [
|
|
868
|
+
"--prepare-official-test",
|
|
869
|
+
dataDir,
|
|
870
|
+
config.marketBundleMetadataUrl,
|
|
871
|
+
OFFICIAL_TEST_PACKAGE.sha256,
|
|
872
|
+
"--market",
|
|
873
|
+
]);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (action === "start") {
|
|
877
|
+
await ops.saveServiceSettings({
|
|
878
|
+
dataDir,
|
|
879
|
+
runtimeUrl: config.runtimeUrl,
|
|
880
|
+
settlementUrl: config.settlementUrl,
|
|
881
|
+
});
|
|
882
|
+
step(
|
|
883
|
+
"Start and unlock Wallet / Runtime",
|
|
884
|
+
"Wallet asks for your unlock secret with hidden input.",
|
|
885
|
+
);
|
|
886
|
+
if (!liveWallet) {
|
|
887
|
+
await ops.startLocal(
|
|
888
|
+
walletCli,
|
|
889
|
+
["serve", walletDir],
|
|
890
|
+
ownEnv,
|
|
891
|
+
() => ops.walletStatus(dataDir),
|
|
892
|
+
join(dataDir, "runtime", "wallet-guide.pid"),
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (!(await ops.walletStatus(dataDir)).locked)
|
|
896
|
+
write("Wallet is already unlocked.");
|
|
897
|
+
else await ops.runNode(walletCli, ["unlock", walletDir]);
|
|
898
|
+
if (!liveRuntime) {
|
|
899
|
+
const env = {
|
|
900
|
+
...ownEnv,
|
|
901
|
+
AGENTPAY_USER_HOST: "127.0.0.1",
|
|
902
|
+
AGENTPAY_USER_PORT: String(new URL(config.runtimeUrl).port),
|
|
903
|
+
AGENTPAY_WALLET_SOCKET: join(walletDir, "wallet.sock"),
|
|
904
|
+
AGENTPAY_WALLET_AGENT_TOKEN_FILE: join(walletDir, "agent.token"),
|
|
905
|
+
AGENTPAY_USER_CALLER_TOKEN_FILE: join(
|
|
906
|
+
dataDir,
|
|
907
|
+
"runtime",
|
|
908
|
+
"caller.token",
|
|
909
|
+
),
|
|
910
|
+
AGENTPAY_USER_STATE_FILE: join(
|
|
911
|
+
dataDir,
|
|
912
|
+
"runtime",
|
|
913
|
+
"invocations.json",
|
|
914
|
+
),
|
|
915
|
+
AGENTPAY_SETTLEMENT_URL: config.settlementUrl,
|
|
916
|
+
};
|
|
917
|
+
await ops.startLocal(
|
|
918
|
+
runtimeEntry,
|
|
919
|
+
[],
|
|
920
|
+
env,
|
|
921
|
+
() => ops.runtimeHealth(config.runtimeUrl, installed),
|
|
922
|
+
join(dataDir, "runtime", "user-runtime-guide.pid"),
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
await ops.saveRuntimeConnection({
|
|
926
|
+
dataDir,
|
|
927
|
+
runtimeUrl: config.runtimeUrl,
|
|
928
|
+
});
|
|
929
|
+
write("Wallet and Runtime are ready.");
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (action === "check" || action === "test") {
|
|
933
|
+
if (!ready) {
|
|
934
|
+
advancing = false;
|
|
935
|
+
write(
|
|
936
|
+
"Start Wallet and Runtime, then unlock Wallet. Runtime has no separate password.",
|
|
937
|
+
);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
await ops.saveRuntimeConnection({
|
|
941
|
+
dataDir,
|
|
942
|
+
runtimeUrl: config.runtimeUrl,
|
|
943
|
+
});
|
|
944
|
+
if (action === "test" && completed) {
|
|
945
|
+
step(
|
|
946
|
+
"Continue the same test",
|
|
947
|
+
completed.confirmed
|
|
948
|
+
? "Rechecking the recorded result without another purchase."
|
|
949
|
+
: "Resuming the saved request; keep its original identifier.",
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (action === "test" && completed?.confirmed) {
|
|
953
|
+
await ops.captureNode(script("random-model-ack-smoke.mjs"), [
|
|
954
|
+
"confirm",
|
|
955
|
+
dataDir,
|
|
956
|
+
config.settlementUrl,
|
|
957
|
+
config.runtimeUrl,
|
|
958
|
+
]);
|
|
959
|
+
await ops.finishLocalSetup({ dataDir, config, ask, write });
|
|
960
|
+
step(
|
|
961
|
+
"Setup complete",
|
|
962
|
+
"Recorded connectivity test verified. You can now browse Market and choose other Skills. Wallet and Runtime remain running. This guide has finished; you can close this terminal.",
|
|
963
|
+
);
|
|
964
|
+
return { completed: true, stage: "tested" };
|
|
965
|
+
}
|
|
966
|
+
const result =
|
|
967
|
+
completed?.hasOrder && action === "test"
|
|
968
|
+
? null
|
|
969
|
+
: await ops.captureNode(script("check-purchase-readiness.mjs"), [
|
|
970
|
+
dataDir,
|
|
971
|
+
draftPath,
|
|
972
|
+
config.settlementUrl,
|
|
973
|
+
config.runtimeUrl,
|
|
974
|
+
String(config.maxUsageUnits),
|
|
975
|
+
]);
|
|
976
|
+
if (result && !result.readyToPurchase) {
|
|
977
|
+
checkedFor = null;
|
|
978
|
+
advancing = false;
|
|
979
|
+
write(`Purchase not ready: ${(result.blockers || []).join(", ")}`);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const max = result
|
|
983
|
+
? formatUnits(
|
|
984
|
+
BigInt(result.facts.estimatedPrepaidMaximumAtomic),
|
|
985
|
+
config.tokenDecimals,
|
|
986
|
+
)
|
|
987
|
+
: null;
|
|
988
|
+
if (action === "check") {
|
|
989
|
+
checkedFor = readinessIdentity;
|
|
990
|
+
write("Connection checks passed.");
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (!completed?.hasOrder) {
|
|
994
|
+
step(
|
|
995
|
+
"4/4 Run the paid test",
|
|
996
|
+
`Current test quote: up to ${max} ${config.tokenSymbol || "tokens"}.\n\nUse the official random-number Skill to test your setup.\nThe price may change before purchase, within your spending limits.\n\nAfter a successful test, Wallet confirms receipt automatically, allowing payment to the Provider.\n`,
|
|
997
|
+
);
|
|
998
|
+
if (
|
|
999
|
+
(await promptChoice(
|
|
1000
|
+
ask,
|
|
1001
|
+
"Type 'confirm' to run the paid test, or 'q' to return",
|
|
1002
|
+
["confirm", "q"],
|
|
1003
|
+
)) === "q"
|
|
1004
|
+
) {
|
|
1005
|
+
advancing = false;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
await ops.captureNode(script("random-model-ack-smoke.mjs"), [
|
|
1010
|
+
"invoke",
|
|
1011
|
+
dataDir,
|
|
1012
|
+
config.settlementUrl,
|
|
1013
|
+
config.runtimeUrl,
|
|
1014
|
+
]);
|
|
1015
|
+
write("Test result verified. Confirming receipt…");
|
|
1016
|
+
await ops.captureNode(script("random-model-ack-smoke.mjs"), [
|
|
1017
|
+
"confirm",
|
|
1018
|
+
dataDir,
|
|
1019
|
+
config.settlementUrl,
|
|
1020
|
+
config.runtimeUrl,
|
|
1021
|
+
]);
|
|
1022
|
+
await ops.finishLocalSetup({ dataDir, config, ask, write });
|
|
1023
|
+
step(
|
|
1024
|
+
"Setup complete",
|
|
1025
|
+
"Connectivity check passed. You can now browse Market and choose other Skills. Wallet and Runtime remain running. This guide has finished; you can close this terminal.",
|
|
1026
|
+
);
|
|
1027
|
+
return { completed: true, stage: "tested" };
|
|
1028
|
+
}
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
advancing = false;
|
|
1031
|
+
write(
|
|
1032
|
+
`Step stopped: ${error.message}. Resolve the cause, then choose 'continue' to resume the saved step; no replacement request is needed.`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|