@c4roa0/agentpay-user 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/AGENTS.md +137 -0
  2. package/CONTRIBUTING.md +16 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE.md +20 -0
  5. package/README.md +84 -2
  6. package/SECURITY.md +16 -0
  7. package/doc/README.md +52 -0
  8. package/doc/TODO.md +97 -0
  9. package/doc/architecture.md +96 -0
  10. package/doc/design.md +60 -0
  11. package/doc/develope.md +68 -0
  12. package/doc/plans/user-wallet-distribution.md +91 -0
  13. package/doc/reference/harness-skill-runtime.md +120 -0
  14. package/doc/reference/local-cache-foundation.md +176 -0
  15. package/doc/reference/local-safe-owner-ui.md +25 -0
  16. package/doc/reference/local-update.md +18 -0
  17. package/doc/reference/multi-service-runtime.md +119 -0
  18. package/doc/reference/npm-integration-verification.md +67 -0
  19. package/doc/reference/onboarding-writing.md +48 -0
  20. package/doc/reference/original-order-recovery.md +97 -0
  21. package/doc/reference/private-anvil-onboarding-config.example.json +18 -0
  22. package/doc/reference/public-deployment-settings.md +56 -0
  23. package/doc/reference/random-skill-contract.md +18 -0
  24. package/doc/reference/result-file-cache.md +166 -0
  25. package/doc/reference/runtime-address-recovery.md +42 -0
  26. package/doc/reference/runtime-result-cache.md +157 -0
  27. package/doc/reference/runtime-startup-recovery.md +141 -0
  28. package/doc/reference/skill-input-output-cache.md +93 -0
  29. package/doc/reference/wallet-identity-policy-history.md +23 -0
  30. package/doc/runbooks/commands.md +89 -0
  31. package/doc/runbooks/deployment-authorization-update.md +42 -0
  32. package/doc/runbooks/emergency-payment-stop.md +80 -0
  33. package/doc/runbooks/local-autostart.md +212 -0
  34. package/doc/runbooks/local-cache.md +115 -0
  35. package/doc/runbooks/local-update.md +84 -0
  36. package/doc/runbooks/market-connection.md +73 -0
  37. package/doc/runbooks/npm-publication.md +56 -0
  38. package/doc/runbooks/official-test-package.md +62 -0
  39. package/doc/runbooks/order-cancellation.md +56 -0
  40. package/doc/runbooks/order-support.md +41 -0
  41. package/doc/runbooks/policy-renewal.md +80 -0
  42. package/doc/runbooks/private-anvil-terminal-setup.md +58 -0
  43. package/doc/runbooks/runtime-skill-client.md +155 -0
  44. package/doc/runbooks/safe-owner-web.md +104 -0
  45. package/doc/runbooks/safe-policy-migration.md +36 -0
  46. package/doc/runbooks/safe-policy-setup.md +52 -0
  47. package/doc/runbooks/terminal-authorization-preview.md +38 -0
  48. package/doc/runbooks/unlock-wallet.md +68 -0
  49. package/doc/runbooks/user-onboarding.md +87 -0
  50. package/doc/runbooks/wallet-identity.md +59 -0
  51. package/doc/security/public-source-review-2026-11-06.md +71 -0
  52. package/doc/usage.md +54 -0
  53. package/package.json +49 -4
  54. package/progress.md +178 -0
  55. package/run +29 -0
  56. package/scripts/agentpay-cli.mjs +11 -0
  57. package/scripts/authorize-existing-safe.mjs +211 -0
  58. package/scripts/autostart.mjs +186 -0
  59. package/scripts/cancel-order.mjs +76 -0
  60. package/scripts/check-installation.mjs +26 -0
  61. package/scripts/check-official-test-package.mjs +35 -0
  62. package/scripts/check-purchase-readiness.mjs +275 -0
  63. package/scripts/configure-runtime-client.mjs +16 -0
  64. package/scripts/guide-policy.mjs +64 -0
  65. package/scripts/initialize-caller-credential.mjs +11 -0
  66. package/scripts/install-skill-bundle.mjs +176 -0
  67. package/scripts/install-user.mjs +24 -0
  68. package/scripts/local-service.mjs +60 -0
  69. package/scripts/lock.mjs +11 -0
  70. package/scripts/login-notify.mjs +39 -0
  71. package/scripts/migrate-safe-policy.mjs +165 -0
  72. package/scripts/official-paid-test.mjs +9 -0
  73. package/scripts/poll-unconfirmed-notice.mjs +75 -0
  74. package/scripts/prepare-onboarding.mjs +33 -0
  75. package/scripts/preview-policy.mjs +65 -0
  76. package/scripts/random-model-ack-smoke.mjs +235 -0
  77. package/scripts/random-reject-smoke.mjs +147 -0
  78. package/scripts/renew.mjs +9 -0
  79. package/scripts/repair.mjs +10 -0
  80. package/scripts/revoke.mjs +9 -0
  81. package/scripts/select-skill-bundle.mjs +39 -0
  82. package/scripts/setup-local.mjs +199 -0
  83. package/scripts/status.mjs +13 -0
  84. package/scripts/test-package.mjs +35 -0
  85. package/scripts/unlock.mjs +13 -0
  86. package/scripts/update.mjs +7 -0
  87. package/scripts/user-setup.mjs +219 -0
  88. package/scripts/verify-npm-release.mjs +32 -0
  89. package/src/active-skill.mjs +160 -0
  90. package/src/anvil-onboarding-state.mjs +96 -0
  91. package/src/anvil-onboarding.mjs +1036 -0
  92. package/src/authorization-question-checkpoint.mjs +114 -0
  93. package/src/autostart-files.mjs +50 -0
  94. package/src/cache-management-cli.mjs +302 -0
  95. package/src/cache-management.mjs +47 -0
  96. package/src/caller-auth.mjs +58 -0
  97. package/src/client-request-lock.mjs +44 -0
  98. package/src/connect-market.mjs +408 -0
  99. package/src/demo-unlock-secret.mjs +64 -0
  100. package/src/deployment-status.mjs +198 -0
  101. package/src/file-cache-cli.mjs +83 -0
  102. package/src/file-cache-owner.mjs +74 -0
  103. package/src/file-cache-settings.mjs +50 -0
  104. package/src/file-cache.mjs +790 -0
  105. package/src/file-download.mjs +281 -0
  106. package/src/finalization-disclosure.mjs +16 -0
  107. package/src/guided-setup.mjs +194 -0
  108. package/src/index.mjs +1231 -0
  109. package/src/install-and-setup.mjs +38 -0
  110. package/src/install-preflight.mjs +201 -0
  111. package/src/install-user.mjs +279 -0
  112. package/src/installation-paths.mjs +31 -0
  113. package/src/invocation-result.mjs +148 -0
  114. package/src/invocation-store.mjs +385 -0
  115. package/src/local-data-directory.mjs +117 -0
  116. package/src/local-info.mjs +181 -0
  117. package/src/local-services.mjs +156 -0
  118. package/src/local-time.mjs +36 -0
  119. package/src/local-update.mjs +306 -0
  120. package/src/local-wallet-ipc.mjs +93 -0
  121. package/src/login-reminder.mjs +28 -0
  122. package/src/market-download-url.mjs +21 -0
  123. package/src/node-version.mjs +12 -0
  124. package/src/npm-update.mjs +126 -0
  125. package/src/official-paid-test.mjs +407 -0
  126. package/src/official-test-package.mjs +41 -0
  127. package/src/official-test-update.mjs +162 -0
  128. package/src/order-cancellation.mjs +241 -0
  129. package/src/order-closure.mjs +42 -0
  130. package/src/order-list-panel.mjs +110 -0
  131. package/src/order-review.mjs +424 -0
  132. package/src/order-status-panel.mjs +65 -0
  133. package/src/order-support.mjs +365 -0
  134. package/src/owner-approval-server.mjs +332 -0
  135. package/src/owner-signer.mjs +67 -0
  136. package/src/policy-info.mjs +38 -0
  137. package/src/policy-migration.mjs +84 -0
  138. package/src/policy-operations.mjs +67 -0
  139. package/src/policy-preview-server.mjs +236 -0
  140. package/src/policy-terminal-guide.mjs +377 -0
  141. package/src/prepare-local-services.mjs +143 -0
  142. package/src/prepare-onboarding.mjs +210 -0
  143. package/src/private-journal-files.mjs +29 -0
  144. package/src/readiness-profile.mjs +30 -0
  145. package/src/renew-policy.mjs +605 -0
  146. package/src/repair-market-wallet.mjs +137 -0
  147. package/src/restore-wallet.mjs +255 -0
  148. package/src/result-cache-view.mjs +276 -0
  149. package/src/runtime-client.mjs +1145 -0
  150. package/src/runtime-connection-settings.mjs +101 -0
  151. package/src/runtime-recovery.mjs +667 -0
  152. package/src/runtime-status.mjs +32 -0
  153. package/src/safe-owner-anvil.mjs +115 -0
  154. package/src/safe-policy-plan.mjs +195 -0
  155. package/src/service-reference.mjs +233 -0
  156. package/src/service-url.mjs +52 -0
  157. package/src/settlement-response.mjs +69 -0
  158. package/src/setup-autostart.mjs +197 -0
  159. package/src/setup-demo.mjs +152 -0
  160. package/src/skill-bundle.mjs +142 -0
  161. package/src/skill-client.mjs +198 -0
  162. package/src/skill-install-prompt.mjs +31 -0
  163. package/src/skill-package.mjs +50 -0
  164. package/src/skill-zip.mjs +220 -0
  165. package/src/status-panel.mjs +589 -0
  166. package/src/transport.mjs +48 -0
  167. package/src/uninstall-local.mjs +299 -0
  168. package/src/user-cli.mjs +354 -0
  169. package/src/user-setup-guide.mjs +193 -0
  170. package/src/wallet-control.mjs +182 -0
  171. package/src/wallet-ipc-error.mjs +16 -0
  172. package/src/wallet-package.mjs +22 -0
  173. package/update-pair.json +6 -0
@@ -0,0 +1,197 @@
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 { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { lstat, readFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import {
11
+ readServiceSettings,
12
+ saveServiceSettings,
13
+ listenerPresent,
14
+ } from "./local-services.mjs";
15
+ const execute = promisify(execFile);
16
+ const userDir = fileURLToPath(new URL("..", import.meta.url));
17
+ const walletCli = walletCliPath();
18
+ async function ctl(...args) {
19
+ return execute("systemctl", ["--user", ...args], {
20
+ timeout: 10000,
21
+ maxBuffer: 16384,
22
+ });
23
+ }
24
+ export async function guideOwnsProcess({
25
+ pidFile,
26
+ expected,
27
+ expectedEnv = {},
28
+ inspect = lstat,
29
+ read = readFile,
30
+ }) {
31
+ try {
32
+ const info = await inspect(pidFile);
33
+ if (
34
+ !info.isFile() ||
35
+ info.isSymbolicLink() ||
36
+ info.uid !== process.getuid() ||
37
+ info.mode & 0o077 ||
38
+ info.size > 32
39
+ )
40
+ return false;
41
+ const text = (await read(pidFile, "utf8")).trim();
42
+ if (!/^[1-9][0-9]*$/.test(text) || Number(text) <= 1) return false;
43
+ const proc = await inspect(`/proc/${text}`);
44
+ if (proc.uid !== process.getuid()) return false;
45
+ const args = (await read(`/proc/${text}/cmdline`, "utf8"))
46
+ .split("\0")
47
+ .filter(Boolean);
48
+ if (JSON.stringify(args) !== JSON.stringify(expected)) return false;
49
+ if (Object.keys(expectedEnv).length) {
50
+ const environment = Object.fromEntries(
51
+ (await read(`/proc/${text}/environ`, "utf8"))
52
+ .split("\0")
53
+ .filter(Boolean)
54
+ .map((line) => {
55
+ const i = line.indexOf("=");
56
+ return [line.slice(0, i), line.slice(i + 1)];
57
+ }),
58
+ );
59
+ for (const [key, value] of Object.entries(expectedEnv))
60
+ if (environment[key] !== value) return false;
61
+ }
62
+ // Classification only. Never signal a PID obtained from a stale file.
63
+ const group = await read(`/proc/${text}/cgroup`, "utf8");
64
+ if (group.includes("/system.slice/")) return false;
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ async function ownership(dataDir, settings) {
71
+ const paths = {
72
+ wallet: join(dataDir, "wallet", "wallet.sock"),
73
+ runtime: {
74
+ host: "127.0.0.1",
75
+ port: Number(new URL(settings.runtimeUrl).port),
76
+ },
77
+ };
78
+ for (const role of ["wallet", "runtime"]) {
79
+ if (!(await listenerPresent(paths[role]))) continue;
80
+ try {
81
+ await ctl("is-active", `agentpay-local-${role}.service`);
82
+ continue;
83
+ } catch {}
84
+ const expected =
85
+ role === "wallet"
86
+ ? [process.execPath, walletCli, "serve", join(dataDir, "wallet")]
87
+ : [process.execPath, join(userDir, "src/index.mjs")];
88
+ if (
89
+ !(await guideOwnsProcess({
90
+ pidFile: join(
91
+ dataDir,
92
+ "runtime",
93
+ role === "wallet" ? "wallet-guide.pid" : "user-runtime-guide.pid",
94
+ ),
95
+ expected,
96
+ expectedEnv:
97
+ role === "runtime"
98
+ ? {
99
+ AGENTPAY_USER_STATE_FILE: join(
100
+ dataDir,
101
+ "runtime",
102
+ "invocations.json",
103
+ ),
104
+ }
105
+ : {},
106
+ }))
107
+ )
108
+ return false;
109
+ }
110
+ return true;
111
+ }
112
+ export async function finishLocalSetup({
113
+ dataDir,
114
+ config,
115
+ ask,
116
+ write,
117
+ platform = process.platform,
118
+ save = saveServiceSettings,
119
+ readSettings = readServiceSettings,
120
+ available = () => ctl("list-jobs", "--no-legend", "--no-pager"),
121
+ owns = ownership,
122
+ enable = async () =>
123
+ execute(
124
+ process.execPath,
125
+ [join(userDir, "scripts/autostart.mjs"), "enable"],
126
+ {
127
+ env: { ...process.env, AGENTPAY_USER_DATA_DIR: dataDir },
128
+ timeout: 60000,
129
+ maxBuffer: 16384,
130
+ },
131
+ ),
132
+ }) {
133
+ if (platform !== "linux") {
134
+ write(
135
+ "Automatic startup is not available on this system yet. Your current setup is kept.",
136
+ );
137
+ return { state: "unsupported" };
138
+ }
139
+ try {
140
+ await save({
141
+ dataDir,
142
+ runtimeUrl: config.runtimeUrl,
143
+ settlementUrl: config.settlementUrl,
144
+ });
145
+ const settings = await readSettings(dataDir);
146
+ await available();
147
+ if (!(await owns(dataDir, settings))) {
148
+ write(
149
+ "Existing services are managed outside this installer. Automatic startup was not changed.",
150
+ );
151
+ return { state: "external" };
152
+ }
153
+ if (settings.startupChoice === "disabled") {
154
+ return { state: "declined" };
155
+ }
156
+ if (settings.startupChoice !== "enabled") {
157
+ write(
158
+ "\nStart Wallet and Runtime automatically when you log in? Wallet will stay locked until you unlock it.",
159
+ );
160
+ let answer;
161
+ do {
162
+ answer = String(
163
+ await ask("Type 'yes' to enable, or 'no' to keep manual startup"),
164
+ )
165
+ .trim()
166
+ .toLowerCase();
167
+ } while (!["yes", "no", "q"].includes(answer));
168
+ if (answer !== "yes") {
169
+ if (answer === "no")
170
+ await save({
171
+ dataDir,
172
+ runtimeUrl: config.runtimeUrl,
173
+ settlementUrl: config.settlementUrl,
174
+ startupChoice: "disabled",
175
+ });
176
+ write("Current services are kept. Automatic startup was not enabled.");
177
+ return { state: "declined" };
178
+ }
179
+ }
180
+ await save({
181
+ dataDir,
182
+ runtimeUrl: config.runtimeUrl,
183
+ settlementUrl: config.settlementUrl,
184
+ startupChoice: "enabled",
185
+ });
186
+ await enable();
187
+ write(
188
+ "Automatic startup enabled for your next login. Current services are kept; after a restart, use agentpay wallet unlock.",
189
+ );
190
+ return { state: "enabled" };
191
+ } catch {
192
+ write(
193
+ "Setup is saved, but automatic startup could not be completed. Retry with agentpay service autostart on. Current processes and Wallet data were kept.",
194
+ );
195
+ return { state: "pending" };
196
+ }
197
+ }
@@ -0,0 +1,152 @@
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
+ // UI simulation: callbacks provide only demo initialization and draft collection.
5
+ export async function runSetupDemo({
6
+ ask,
7
+ write,
8
+ check,
9
+ state,
10
+ initialize,
11
+ preview,
12
+ }) {
13
+ let approved = false,
14
+ prepared = false,
15
+ delivered = false,
16
+ complete = false,
17
+ draft = null,
18
+ fields = null,
19
+ advancing = false;
20
+ const section = (title, text) =>
21
+ write(
22
+ `\n------------------------------------------------------------\n${title}\n------------------------------------------------------------\n${text}`,
23
+ );
24
+ const consent = async (text, details) => {
25
+ while (true) {
26
+ const answer = (
27
+ await ask(`${text}\nType 'confirm', 'details', or 'q' to return\n> `)
28
+ )
29
+ .trim()
30
+ .toLowerCase();
31
+ if (answer === "q") return false;
32
+ if (answer === "details") {
33
+ write(JSON.stringify(details, null, 2));
34
+ continue;
35
+ }
36
+ if (answer === "confirm") return true;
37
+ }
38
+ };
39
+ write(
40
+ "Demo only: no real permission, installation, process or payment. This simulation is not saved after exit.",
41
+ );
42
+ while (!complete) {
43
+ if (!(await check()).ok) return { completed: false, stage: "preflight" };
44
+ const local = await state();
45
+ const stage =
46
+ local.stage !== "initialized"
47
+ ? "1/4 Create local Wallet"
48
+ : !approved
49
+ ? "2/4 Set spending permission"
50
+ : !prepared
51
+ ? "3/4 Prepare the official test Skill"
52
+ : "4/4 Run the paid test";
53
+ if (!advancing) {
54
+ section(
55
+ "> Continue setup",
56
+ `${stage}\n * 'continue' Resume setup\n 'details' Inspect demo progress\n 'q' Quit`,
57
+ );
58
+ const answer = (await ask("> ")).trim().toLowerCase();
59
+ if (answer === "q") return { completed: false, mode: "demo" };
60
+ if (answer === "details") {
61
+ write(
62
+ JSON.stringify({ local, approved, prepared, delivered }, null, 2),
63
+ );
64
+ continue;
65
+ }
66
+ if (answer !== "continue") continue;
67
+ }
68
+ advancing = true;
69
+ try {
70
+ if (local.stage !== "initialized") {
71
+ section(stage, "Set a practice unlock secret. No real key is created.");
72
+ await initialize();
73
+ continue;
74
+ }
75
+ if (!approved) {
76
+ if (!draft) {
77
+ const result = await preview(local.delegate, {
78
+ collectOnly: true,
79
+ checkpoint: fields,
80
+ saveInputs: async (value) => {
81
+ fields = structuredClone(value);
82
+ },
83
+ });
84
+ if (!result?.draft) {
85
+ advancing = false;
86
+ continue;
87
+ }
88
+ draft = result.draft;
89
+ }
90
+ section(
91
+ stage,
92
+ `Safe: ${draft.safe}\nBudget: ${draft.budget} ${draft.currency}\nPer-payment limit: ${draft.perPayment} ${draft.currency}\nExpires: ${draft.expiry}\nEligible purchases can spend the whole budget automatically.`,
93
+ );
94
+ if (
95
+ !(await consent(
96
+ "Confirm to simulate owner authorization and independent Wallet binding.",
97
+ { calls: draft.calls },
98
+ ))
99
+ ) {
100
+ advancing = false;
101
+ continue;
102
+ }
103
+ approved = true;
104
+ write("Authorization and Wallet binding complete (demo).");
105
+ continue;
106
+ }
107
+ if (!prepared) {
108
+ section(
109
+ stage,
110
+ "Install and select the official random-number Skill, start Wallet / Runtime and check readiness (simulated). No payment yet.",
111
+ );
112
+ prepared = true;
113
+ write(
114
+ "Skill selected; Wallet / Runtime running; readiness checks passed (demo).",
115
+ );
116
+ continue;
117
+ }
118
+ if (!delivered) {
119
+ section(
120
+ stage,
121
+ "Example test cost: 0.001 demo USDC. After verified delivery, Wallet confirms receipt automatically (demo). No real payment.",
122
+ );
123
+ if (
124
+ !(await consent("Confirm to simulate the paid test.", {
125
+ maximum: "0.001 demo USDC",
126
+ }))
127
+ ) {
128
+ advancing = false;
129
+ continue;
130
+ }
131
+ delivered = true;
132
+ write("Test delivery verified (demo).");
133
+ }
134
+ write(
135
+ "Official test delivery verified; Wallet confirms receipt automatically (demo). Provider payout may happen earlier.",
136
+ );
137
+ complete = true;
138
+ section(
139
+ "Setup demo complete",
140
+ "Connectivity check passed (demo). You can now browse Market and choose Skills you like. This demo has finished; you can close this terminal.",
141
+ );
142
+ } catch (error) {
143
+ advancing = false;
144
+ write(
145
+ error.code === "CANCELLED"
146
+ ? "Paused; completed demo steps are retained until exit."
147
+ : `Step stopped: ${error.message}`,
148
+ );
149
+ }
150
+ }
151
+ return { completed: true, mode: "demo", readyToPurchase: false };
152
+ }
@@ -0,0 +1,142 @@
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 { currentTimeMs } from "./local-time.mjs";
5
+ import { readSkillZip } from "./skill-zip.mjs";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import {
8
+ mkdir,
9
+ lstat,
10
+ readFile,
11
+ writeFile,
12
+ rename,
13
+ rm,
14
+ } from "node:fs/promises";
15
+ import { join, dirname } from "node:path";
16
+ import { normalizeServiceReference } from "@c4roa0/agentpay-protocol";
17
+ export const bundleDigest = (bytes) =>
18
+ createHash("sha256").update(bytes).digest("hex");
19
+ export const skillArchiveLimits = Object.freeze({
20
+ compressed: 2097152,
21
+ expanded: 8388608,
22
+ file: 1048576,
23
+ entries: 256,
24
+ });
25
+ export function parseSkillBundle(bytes) {
26
+ const archive = readSkillZip(bytes, skillArchiveLimits),
27
+ files = Object.fromEntries(archive.files);
28
+ const entries = Object.keys(files).filter(
29
+ (name) => name === "SKILL.md" || name.endsWith("/SKILL.md"),
30
+ );
31
+ if (entries.length !== 1) throw Error("BUNDLE_INSTRUCTIONS_REQUIRED");
32
+ const root = entries[0].slice(0, -8);
33
+ if (
34
+ archive.entries.some(
35
+ (e) => root && e.path !== root.slice(0, -1) && !e.path.startsWith(root),
36
+ )
37
+ )
38
+ throw Error("BUNDLE_ROOT_INVALID");
39
+ if (!files[entries[0]].length || !files[root + "agentpay.json"])
40
+ throw Error("BUNDLE_MANIFEST_INVALID");
41
+ const manifest = JSON.parse(files[root + "agentpay.json"].toString("utf8"));
42
+ if (
43
+ !manifest ||
44
+ typeof manifest !== "object" ||
45
+ Array.isArray(manifest) ||
46
+ typeof manifest.packageName !== "string" ||
47
+ !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,95}$/.test(manifest.packageName) ||
48
+ typeof manifest.packageVersion !== "string" ||
49
+ !/^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}$/.test(manifest.packageVersion)
50
+ )
51
+ throw Error("BUNDLE_MANIFEST_INVALID");
52
+ const services = manifest.services ?? {};
53
+ if (!services || typeof services !== "object" || Array.isArray(services))
54
+ throw Error("BUNDLE_SERVICES_INVALID");
55
+ for (const [alias, reference] of Object.entries(services)) {
56
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(alias))
57
+ throw Error("BUNDLE_SERVICES_INVALID");
58
+ normalizeServiceReference(reference);
59
+ }
60
+ return { manifest, services, root, files, sha256: bundleDigest(bytes) };
61
+ }
62
+ async function privateDir(path) {
63
+ const st = await lstat(path);
64
+ if (
65
+ !st.isDirectory() ||
66
+ st.isSymbolicLink() ||
67
+ st.uid !== process.getuid() ||
68
+ st.mode & 0o077
69
+ )
70
+ throw new Error("PRIVATE_USER_DIRECTORY_REQUIRED");
71
+ }
72
+ export async function installSkillBundle({
73
+ dataDir,
74
+ bytes,
75
+ source,
76
+ publication = null,
77
+ }) {
78
+ const parsed = parseSkillBundle(bytes);
79
+ await privateDir(dataDir);
80
+ const runtime = join(dataDir, "runtime");
81
+ await privateDir(runtime);
82
+ const parent = join(runtime, "skill-bundles");
83
+ await mkdir(parent, { mode: 0o700 }).catch((error) => {
84
+ if (error.code !== "EEXIST") throw error;
85
+ });
86
+ await privateDir(parent);
87
+ const target = join(
88
+ parent,
89
+ `${parsed.manifest.packageName}-${parsed.manifest.packageVersion}-${parsed.sha256}`,
90
+ );
91
+ const exists = await lstat(target).catch((error) => {
92
+ if (error.code === "ENOENT") return null;
93
+ throw error;
94
+ });
95
+ if (exists) {
96
+ await privateDir(target);
97
+ const original = await readFile(join(target, "publisher.zip"));
98
+ if (!original.equals(bytes)) throw new Error("BUNDLE_RELEASE_CONFLICT");
99
+ let locallyModified = false;
100
+ for (const [name, value] of Object.entries(parsed.files)) {
101
+ const file = join(target, name);
102
+ const stat = await lstat(file);
103
+ if (!stat.isFile() || stat.isSymbolicLink())
104
+ throw new Error("BUNDLE_LOCAL_FILE_INVALID");
105
+ if (!(await readFile(file)).equals(value)) locallyModified = true;
106
+ }
107
+ return { directory: target, changed: false, locallyModified };
108
+ }
109
+ // Unique staging directory; only this invocation's incomplete files are removed.
110
+ const staging = join(parent, `.install-${randomUUID()}`);
111
+ await mkdir(staging, { mode: 0o700 });
112
+ try {
113
+ for (const [name, value] of Object.entries(parsed.files)) {
114
+ const destination = join(staging, name);
115
+ await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
116
+ await writeFile(destination, value, { flag: "wx", mode: 0o600 });
117
+ }
118
+ await writeFile(join(staging, "publisher.zip"), bytes, {
119
+ flag: "wx",
120
+ mode: 0o600,
121
+ });
122
+ await writeFile(
123
+ join(staging, "installation.json"),
124
+ JSON.stringify(
125
+ {
126
+ format: "agentpay.skill-installation.v1",
127
+ source,
128
+ sha256: parsed.sha256,
129
+ publication,
130
+ installedAtMs: currentTimeMs(),
131
+ },
132
+ null,
133
+ 2,
134
+ ) + "\n",
135
+ { flag: "wx", mode: 0o600 },
136
+ );
137
+ await rename(staging, target);
138
+ return { directory: target, changed: true, locallyModified: false };
139
+ } finally {
140
+ await rm(staging, { recursive: true, force: true });
141
+ }
142
+ }
@@ -0,0 +1,198 @@
1
+ // Generic Skill CLI glue. Business input/output validation is supplied by the Skill;
2
+ // payment authority and request recovery remain in the installed client/Runtime.
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
+ export const skillClientCapabilities = Object.freeze([
7
+ "input-snapshots-v1",
8
+ "output-files-v1",
9
+ "original-request-v1",
10
+ ]);
11
+ import {
12
+ readServiceOffer,
13
+ RuntimeClientError,
14
+ loadRuntimeConnection,
15
+ userDataDirectory,
16
+ checkRuntime,
17
+ selectInvocation,
18
+ resumeInvocation,
19
+ invokeService,
20
+ completeInvocation,
21
+ cacheResultFile,
22
+ } from "./runtime-client.mjs";
23
+ import { parseServiceReference } from "./service-reference.mjs";
24
+ export async function runSkill({
25
+ service,
26
+ args = process.argv.slice(2),
27
+ env = process.env,
28
+ validateRequest = (request) => request,
29
+ inputFiles,
30
+ prepareRequest,
31
+ outputFiles,
32
+ write = (value) => console.log(JSON.stringify(value)),
33
+ notice = (value) => console.error(value),
34
+ } = {}) {
35
+ let requestId;
36
+ try {
37
+ const reference = parseServiceReference(service),
38
+ values = [...args],
39
+ mode = values.shift();
40
+ const connection = await loadRuntimeConnection({
41
+ dataDir: userDataDirectory(env),
42
+ env,
43
+ });
44
+ if (mode === "--quote" && values.length === 0) {
45
+ write({
46
+ ok: true,
47
+ result: await readServiceOffer(connection, reference),
48
+ });
49
+ return 0;
50
+ }
51
+ if (mode === "--check" && values.length === 0) {
52
+ write(await checkRuntime(connection, reference));
53
+ return 0;
54
+ }
55
+ let checkpoint;
56
+ if (mode === "--resume" && values.length === 1)
57
+ checkpoint = await resumeInvocation(connection, reference, values[0]);
58
+ else if (mode === "--invoke") {
59
+ const inputText = values.shift();
60
+ let force = false,
61
+ units = "1",
62
+ unitsSeen = false,
63
+ acceptedTerms,
64
+ requestId;
65
+ while (values.length) {
66
+ const flag = values.shift();
67
+ if (
68
+ flag === "--accept-terms" &&
69
+ acceptedTerms === undefined &&
70
+ values.length
71
+ ) {
72
+ try {
73
+ acceptedTerms = JSON.parse(values.shift());
74
+ } catch {
75
+ throw new RuntimeClientError(
76
+ "INVALID_ARGUMENTS",
77
+ "Pass --accept-terms the JSON price/duration read from --quote.",
78
+ );
79
+ }
80
+ } else if (flag === "--force" && !force) force = true;
81
+ else if (flag === "--units" && !unitsSeen && values.length) {
82
+ units = values.shift();
83
+ unitsSeen = true;
84
+ } else if (
85
+ flag === "--request-id" &&
86
+ requestId === undefined &&
87
+ values.length
88
+ )
89
+ requestId = values.shift();
90
+ else
91
+ throw new RuntimeClientError(
92
+ "INVALID_ARGUMENTS",
93
+ "Use --invoke <JSON> [--units N] [--request-id ID] [--force], --check, --quote or --resume <request-id>.",
94
+ );
95
+ }
96
+ if (force && requestId !== undefined)
97
+ throw new RuntimeClientError(
98
+ "INVALID_ARGUMENTS",
99
+ "Choose --force or --request-id, not both.",
100
+ );
101
+ let request;
102
+ try {
103
+ request = JSON.parse(inputText);
104
+ } catch {
105
+ throw new RuntimeClientError(
106
+ "INVALID_INPUT",
107
+ "Provide the business input as JSON.",
108
+ );
109
+ }
110
+ try {
111
+ request = await validateRequest(request);
112
+ } catch {
113
+ throw new RuntimeClientError(
114
+ "INVALID_INPUT",
115
+ "Check the business input against this Skill’s instructions.",
116
+ );
117
+ }
118
+ checkpoint = await selectInvocation(
119
+ connection,
120
+ reference,
121
+ {
122
+ request,
123
+ requestedMaxUsageUnits: units,
124
+ ...(acceptedTerms === undefined ? {} : { acceptedTerms }),
125
+ },
126
+ { force, ...(requestId === undefined ? {} : { requestId }) },
127
+ );
128
+ } else
129
+ throw new RuntimeClientError(
130
+ "INVALID_ARGUMENTS",
131
+ "Use --invoke <JSON> [--units N] [--force], --check, --quote or --resume <request-id>.",
132
+ );
133
+ requestId = checkpoint.requestId;
134
+ notice(
135
+ `Request: ${requestId}. Matching calls recover or reuse output; --force starts another paid request.`,
136
+ );
137
+ const result = await invokeService(connection, reference, checkpoint, {
138
+ inputFiles,
139
+ prepareRequest,
140
+ });
141
+ await completeInvocation(connection, reference, checkpoint);
142
+ let files;
143
+ if (outputFiles) {
144
+ try {
145
+ const declarations = await outputFiles(
146
+ structuredClone(result.result.output),
147
+ );
148
+ if (!Array.isArray(declarations) || declarations.length > 20)
149
+ throw Error("Invalid output file declarations");
150
+ files = [];
151
+ for (const file of declarations) {
152
+ if (
153
+ !file ||
154
+ Object.keys(file).some(
155
+ (k) => !["fileKey", "url", "filename", "sha256"].includes(k),
156
+ )
157
+ )
158
+ throw Error("Invalid output file declaration");
159
+ files.push(
160
+ await cacheResultFile(connection, {
161
+ ...file,
162
+ orderId: result.result.orderId,
163
+ }),
164
+ );
165
+ }
166
+ } catch {
167
+ write({
168
+ ...result,
169
+ requestId,
170
+ fileCache: {
171
+ state: "incomplete",
172
+ message:
173
+ "Delivery is saved. Retry this same request to finish saving files; do not use --force.",
174
+ },
175
+ });
176
+ return 1;
177
+ }
178
+ }
179
+ write({ ...result, requestId, ...(files ? { files } : {}) });
180
+ return 0;
181
+ } catch (error) {
182
+ const known =
183
+ error instanceof RuntimeClientError ||
184
+ ["SERVICE_REFERENCE_INVALID"].includes(error.code);
185
+ write({
186
+ ok: false,
187
+ ...(requestId ? { requestId } : {}),
188
+ error: {
189
+ code: known ? error.code : "LOCAL_CLIENT_FAILED",
190
+ message: known
191
+ ? error.message
192
+ : "Inspect local AgentPay setup and preserve existing requests.",
193
+ ...(error.runtimeCode ? { runtimeCode: error.runtimeCode } : {}),
194
+ },
195
+ });
196
+ return 1;
197
+ }
198
+ }