@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.
Files changed (175) 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 +85 -2
  6. package/SECURITY.md +16 -0
  7. package/doc/README.md +52 -0
  8. package/doc/TODO.md +107 -0
  9. package/doc/architecture.md +99 -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 +103 -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 +103 -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 +210 -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 +1233 -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 +316 -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/protocol-package.mjs +25 -0
  145. package/src/readiness-profile.mjs +30 -0
  146. package/src/renew-policy.mjs +605 -0
  147. package/src/repair-market-wallet.mjs +137 -0
  148. package/src/restore-wallet.mjs +255 -0
  149. package/src/result-cache-view.mjs +276 -0
  150. package/src/runtime-client.mjs +1145 -0
  151. package/src/runtime-connection-settings.mjs +101 -0
  152. package/src/runtime-recovery.mjs +667 -0
  153. package/src/runtime-status.mjs +32 -0
  154. package/src/safe-owner-anvil.mjs +115 -0
  155. package/src/safe-policy-plan.mjs +195 -0
  156. package/src/service-reference.mjs +233 -0
  157. package/src/service-url.mjs +52 -0
  158. package/src/settlement-response.mjs +69 -0
  159. package/src/setup-autostart.mjs +197 -0
  160. package/src/setup-demo.mjs +152 -0
  161. package/src/skill-bundle.mjs +142 -0
  162. package/src/skill-client.mjs +198 -0
  163. package/src/skill-install-prompt.mjs +31 -0
  164. package/src/skill-package.mjs +50 -0
  165. package/src/skill-zip.mjs +220 -0
  166. package/src/software-versions.mjs +48 -0
  167. package/src/status-panel.mjs +603 -0
  168. package/src/transport.mjs +48 -0
  169. package/src/uninstall-local.mjs +299 -0
  170. package/src/user-cli.mjs +354 -0
  171. package/src/user-setup-guide.mjs +193 -0
  172. package/src/wallet-control.mjs +182 -0
  173. package/src/wallet-ipc-error.mjs +16 -0
  174. package/src/wallet-package.mjs +28 -0
  175. package/update-pair.json +6 -0
@@ -0,0 +1,1145 @@
1
+ // Shared Skill transport and recovery. Resolve local credentials privately; endpoint
2
+ // changes must not replace the original purchase identity or bypass retained checkpoints.
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 { withClientRequestLock } from "./client-request-lock.mjs";
7
+ import { normalizeServiceReference, hashPrivateRequest } from "@c4roa0/agentpay-protocol";
8
+ import {
9
+ normalizeAcceptedTerms,
10
+ normalizeRuntimeResult,
11
+ serviceRequest,
12
+ usageCap,
13
+ } from "./service-reference.mjs";
14
+ // Local harness transport only. Wallet and Runtime remain the payment authority.
15
+ import { constants } from "node:fs";
16
+ import {
17
+ open,
18
+ mkdir,
19
+ link,
20
+ unlink,
21
+ rename,
22
+ rmdir,
23
+ readdir,
24
+ } from "node:fs/promises";
25
+ import { join, resolve, dirname, isAbsolute } from "node:path";
26
+ import { homedir } from "node:os";
27
+ import { randomUUID, createHash } from "node:crypto";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ const FORMAT = "agentpay.runtime-connection.v1";
31
+ const ID = /^[A-Za-z0-9._:-]{16,128}$/;
32
+ export class RuntimeClientError extends Error {
33
+ constructor(code, message, extra = {}) {
34
+ super(message);
35
+ this.code = code;
36
+ Object.assign(this, extra);
37
+ }
38
+ }
39
+ const fail = (code, message) => new RuntimeClientError(code, message);
40
+ export function userDataDirectory(env = process.env) {
41
+ return resolve(
42
+ env.AGENTPAY_USER_DATA_DIR || join(homedir(), ".local/share/agentpay/user"),
43
+ );
44
+ }
45
+ export function runtimeBase(value) {
46
+ let url;
47
+ try {
48
+ url = new URL(value);
49
+ } catch {
50
+ throw fail(
51
+ "LOCAL_RUNTIME_URL_REQUIRED",
52
+ "Use a loopback HTTP Runtime address.",
53
+ );
54
+ }
55
+ if (
56
+ url.protocol !== "http:" ||
57
+ !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) ||
58
+ url.username ||
59
+ url.password ||
60
+ url.search ||
61
+ url.hash ||
62
+ url.pathname !== "/"
63
+ )
64
+ throw fail(
65
+ "LOCAL_RUNTIME_URL_REQUIRED",
66
+ "Use a loopback HTTP Runtime address.",
67
+ );
68
+ return url.href;
69
+ }
70
+ async function privateDirectory(path) {
71
+ await mkdir(path, { recursive: true, mode: 0o700 });
72
+ const handle = await open(
73
+ path,
74
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
75
+ );
76
+ try {
77
+ const st = await handle.stat();
78
+ if (st.uid !== process.getuid() || st.mode & 0o077)
79
+ throw fail(
80
+ "LOCAL_STATE_UNSAFE",
81
+ "Use an owner-held private AgentPay data directory.",
82
+ );
83
+ } finally {
84
+ await handle.close();
85
+ }
86
+ // Persist a newly created directory's name before any paid request can use it.
87
+ await syncDirectory(dirname(path));
88
+ }
89
+ export async function readPrivateFile(path, max = 16384) {
90
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
91
+ try {
92
+ const st = await handle.stat();
93
+ if (
94
+ !st.isFile() ||
95
+ st.uid !== process.getuid() ||
96
+ st.mode & 0o077 ||
97
+ st.size > max
98
+ )
99
+ throw fail(
100
+ "LOCAL_STATE_UNSAFE",
101
+ "Inspect the local AgentPay file permissions and size.",
102
+ );
103
+ return await handle.readFile("utf8");
104
+ } finally {
105
+ await handle.close();
106
+ }
107
+ }
108
+ async function syncDirectory(path) {
109
+ const h = await open(path, constants.O_RDONLY | constants.O_DIRECTORY);
110
+ try {
111
+ await h.sync();
112
+ } finally {
113
+ await h.close();
114
+ }
115
+ }
116
+ async function publishJson(path, value, { replace = false } = {}) {
117
+ const temp = join(dirname(path), `.client-${randomUUID()}.tmp`);
118
+ const h = await open(temp, "wx", 0o600);
119
+ try {
120
+ try {
121
+ await h.writeFile(JSON.stringify(value) + "\n");
122
+ await h.sync();
123
+ } finally {
124
+ await h.close();
125
+ }
126
+ if (replace) await rename(temp, path);
127
+ else await link(temp, path);
128
+ await syncDirectory(dirname(path));
129
+ } finally {
130
+ await unlink(temp).catch((error) => {
131
+ if (error.code !== "ENOENT") throw error;
132
+ });
133
+ }
134
+ }
135
+ export async function saveRuntimeConnection({ dataDir, runtimeUrl }) {
136
+ dataDir = resolve(dataDir);
137
+ await privateDirectory(dataDir);
138
+ await privateDirectory(join(dataDir, "runtime"));
139
+ const path = join(dataDir, "runtime", "connection.json");
140
+ const value = {
141
+ version: FORMAT,
142
+ runtimeUrl: runtimeBase(runtimeUrl),
143
+ callerTokenFile: join(dataDir, "runtime", "caller.token"),
144
+ clientModule: fileURLToPath(import.meta.url),
145
+ };
146
+ let old;
147
+ try {
148
+ old = JSON.parse(await readPrivateFile(path));
149
+ } catch (error) {
150
+ if (error.code !== "ENOENT")
151
+ throw fail(
152
+ "LOCAL_SETUP_REQUIRED",
153
+ "Inspect the saved local connection file before refreshing setup.",
154
+ );
155
+ }
156
+ if (JSON.stringify(old) !== JSON.stringify(value))
157
+ await publishJson(path, value, { replace: old !== undefined });
158
+ return value;
159
+ }
160
+ export async function loadRuntimeConnection({
161
+ dataDir = userDataDirectory(),
162
+ env = process.env,
163
+ } = {}) {
164
+ dataDir = resolve(dataDir);
165
+ let value;
166
+ try {
167
+ value = JSON.parse(
168
+ await readPrivateFile(join(dataDir, "runtime", "connection.json")),
169
+ );
170
+ } catch (error) {
171
+ if (error.code === "ENOENT")
172
+ throw fail(
173
+ "LOCAL_SETUP_REQUIRED",
174
+ "Local Runtime connection is missing. Run agentpay repair, then retry the original request.",
175
+ );
176
+ throw fail(
177
+ "LOCAL_SETUP_REQUIRED",
178
+ "Local Runtime connection could not be read safely. Run agentpay repair for diagnostics; keep your local files.",
179
+ );
180
+ }
181
+ if (
182
+ value?.version !== FORMAT ||
183
+ !isAbsolute(value.callerTokenFile || "") ||
184
+ !isAbsolute(value.clientModule || "")
185
+ )
186
+ throw fail(
187
+ "LOCAL_SETUP_REQUIRED",
188
+ "Run AgentPay setup to refresh the local Runtime connection.",
189
+ );
190
+ return {
191
+ dataDir,
192
+ runtimeUrl: runtimeBase(env.AGENTPAY_USER_URL || value.runtimeUrl),
193
+ callerTokenFile:
194
+ env.AGENTPAY_USER_CALLER_TOKEN_FILE || value.callerTokenFile,
195
+ };
196
+ }
197
+ function identity(service) {
198
+ try {
199
+ return normalizeServiceReference(service);
200
+ } catch {
201
+ throw fail(
202
+ "SKILL_SERVICE_INVALID",
203
+ "Use a registered public service reference. Preserve older requests for recovery review.",
204
+ );
205
+ }
206
+ }
207
+ async function requestJson(url, options, timeoutMs) {
208
+ const signal = AbortSignal.timeout(timeoutMs);
209
+ try {
210
+ const response = await fetch(url, {
211
+ ...options,
212
+ redirect: "error",
213
+ signal,
214
+ });
215
+ const chunks = [];
216
+ let length = 0;
217
+ if (response.body)
218
+ for await (const chunk of response.body) {
219
+ length += chunk.length;
220
+ if (length > 2 * 1048576)
221
+ throw fail(
222
+ "RUNTIME_RESPONSE_TOO_LARGE",
223
+ "Runtime response exceeded the local size limit. Keep the original request and inspect Runtime logs; do not create a replacement purchase.",
224
+ );
225
+ chunks.push(chunk);
226
+ }
227
+ let body;
228
+ try {
229
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
230
+ } catch {
231
+ throw fail(
232
+ "RUNTIME_RESPONSE_INVALID",
233
+ "Runtime returned unreadable data. Check agentpay service logs runtime; keep the original request and do not create a replacement purchase.",
234
+ );
235
+ }
236
+ return { response, body };
237
+ } catch (error) {
238
+ if (error instanceof RuntimeClientError) throw error;
239
+ if (signal.aborted)
240
+ throw fail(
241
+ "RUNTIME_TIMEOUT",
242
+ "Runtime did not finish responding in time. The request may still be running. Resume the same request; do not create a replacement purchase.",
243
+ );
244
+ throw fail(
245
+ "RUNTIME_UNAVAILABLE",
246
+ "Could not finish communicating with Runtime. Check agentpay status and agentpay service logs runtime, then resume the same request. Do not create a replacement purchase.",
247
+ );
248
+ }
249
+ }
250
+ async function callerToken(connection) {
251
+ let token;
252
+ try {
253
+ token = (await readPrivateFile(connection.callerTokenFile, 256)).trim();
254
+ } catch {
255
+ throw fail(
256
+ "LOCAL_CREDENTIAL_REQUIRED",
257
+ "Complete local AgentPay setup to repair the caller credential. Never paste credentials into chat.",
258
+ );
259
+ }
260
+ if (!/^[0-9a-f]{64}$/.test(token))
261
+ throw fail(
262
+ "LOCAL_CREDENTIAL_REQUIRED",
263
+ "Complete local AgentPay setup to repair the caller credential.",
264
+ );
265
+ return token;
266
+ }
267
+ export async function checkRuntime(
268
+ connection,
269
+ service,
270
+ { timeoutMs = 5000 } = {},
271
+ ) {
272
+ const expected = identity(service),
273
+ token = await callerToken(connection);
274
+ const { response, body } = await requestJson(
275
+ new URL("/services/check", runtimeBase(connection.runtimeUrl)),
276
+ {
277
+ method: "POST",
278
+ headers: {
279
+ "content-type": "application/json",
280
+ authorization: `Bearer ${token}`,
281
+ },
282
+ body: JSON.stringify({ service: expected }),
283
+ },
284
+ timeoutMs,
285
+ );
286
+ if (response.status === 401)
287
+ throw fail(
288
+ "UNAUTHORIZED",
289
+ "Complete local AgentPay setup to repair the caller credential.",
290
+ );
291
+ let genericMatches = false;
292
+ try {
293
+ genericMatches =
294
+ JSON.stringify(normalizeServiceReference(body.result?.service)) ===
295
+ JSON.stringify(expected);
296
+ } catch {}
297
+ if (
298
+ !response.ok ||
299
+ body?.ok !== true ||
300
+ body.result?.role !== "user-runtime" ||
301
+ !genericMatches
302
+ )
303
+ throw fail(
304
+ "RUNTIME_SERVICE_IDENTITY_MISMATCH",
305
+ "Runtime must support the service reference supplied by this Skill.",
306
+ );
307
+ return {
308
+ ok: true,
309
+ runtimeIdentityMatches: true,
310
+ purchaseReadinessChecked: false,
311
+ orderCreated: false,
312
+ originalRequestRecovery:
313
+ body.result?.capabilities?.includes("agentpay.original-request.v1") ===
314
+ true,
315
+ };
316
+ }
317
+ function callInput(input, service) {
318
+ identity(service);
319
+ try {
320
+ if (
321
+ !input ||
322
+ Object.keys(input)
323
+ .filter((k) => k !== "acceptedTerms")
324
+ .sort()
325
+ .join(",") !== "request,requestedMaxUsageUnits"
326
+ )
327
+ throw Error();
328
+ return {
329
+ request: serviceRequest(input.request),
330
+ requestedMaxUsageUnits: usageCap(input.requestedMaxUsageUnits),
331
+ ...(input.acceptedTerms === undefined
332
+ ? {}
333
+ : { acceptedTerms: normalizeAcceptedTerms(input.acceptedTerms) }),
334
+ };
335
+ } catch {
336
+ throw fail(
337
+ "INVALID_INPUT",
338
+ "Use bounded JSON input and a positive maximum usage string.",
339
+ );
340
+ }
341
+ }
342
+ function checkpointPath(connection, id) {
343
+ if (typeof id !== "string" || !ID.test(id))
344
+ throw fail(
345
+ "INVALID_REQUEST_ID",
346
+ "Use a request identifier of 16–128 letters, digits, dots, colons, underscores or hyphens.",
347
+ );
348
+ return join(
349
+ connection.dataDir,
350
+ "runtime",
351
+ "client-requests",
352
+ createHash("sha256").update(id).digest("hex") + ".json",
353
+ );
354
+ }
355
+ export async function prepareInvocation(
356
+ connection,
357
+ service,
358
+ input,
359
+ requestId = randomUUID(),
360
+ ) {
361
+ input = callInput(input, service);
362
+ const path = checkpointPath(connection, requestId);
363
+ await privateDirectory(connection.dataDir);
364
+ await privateDirectory(join(connection.dataDir, "runtime"));
365
+ await privateDirectory(dirname(path));
366
+ const checkpoint = {
367
+ version: "agentpay.runtime-client-request.v1",
368
+ requestId,
369
+ runtimeUrl: runtimeBase(connection.runtimeUrl),
370
+ service: identity(service),
371
+ input,
372
+ submissionState: "prepared",
373
+ createdAtMs: Date.now(),
374
+ };
375
+ try {
376
+ await publishJson(path, checkpoint);
377
+ } catch (error) {
378
+ if (error.code !== "EEXIST") throw error;
379
+ }
380
+ // A racing creator may have linked the file but not yet synced the directory.
381
+ await syncDirectory(dirname(path));
382
+ return resumeInvocation(connection, service, requestId, input);
383
+ }
384
+ export async function resumeInvocation(
385
+ connection,
386
+ service,
387
+ requestId,
388
+ expectedInput,
389
+ ) {
390
+ const path = checkpointPath(connection, requestId);
391
+ let saved;
392
+ try {
393
+ saved = JSON.parse(await readPrivateFile(path, 2 * 1048576));
394
+ } catch {
395
+ throw fail(
396
+ "REQUEST_NOT_FOUND",
397
+ "The saved request cannot be read. Preserve local state; do not create a replacement purchase.",
398
+ );
399
+ }
400
+ if (
401
+ saved?.version !== "agentpay.runtime-client-request.v1" ||
402
+ saved.requestId !== requestId ||
403
+ JSON.stringify(saved.service) !== JSON.stringify(identity(service)) ||
404
+ !Number.isSafeInteger(saved.createdAtMs)
405
+ )
406
+ throw fail(
407
+ "REQUEST_CONFLICT",
408
+ "Restore the original Skill and request input before resuming this request.",
409
+ );
410
+ let input = callInput(saved.input, service);
411
+ try {
412
+ if (saved.fileDeclarations !== undefined)
413
+ validateFileDeclarations(saved.fileDeclarations);
414
+ if (saved.preparedRequest !== undefined) {
415
+ if (!saved.fileDeclarations?.length) throw Error();
416
+ serviceRequest(saved.preparedRequest);
417
+ }
418
+ if (
419
+ saved.fileDeclarations?.length &&
420
+ saved.submissionState !== "prepared" &&
421
+ saved.preparedRequest === undefined
422
+ )
423
+ throw Error();
424
+ if (
425
+ saved.originalOrderRequestId !== undefined &&
426
+ !/^0x[0-9a-f]{64}$/.test(saved.originalOrderRequestId)
427
+ )
428
+ throw Error();
429
+ } catch {
430
+ throw fail(
431
+ "REQUEST_STATE_INVALID",
432
+ "Original input snapshot metadata needs review. No replacement purchase was started.",
433
+ );
434
+ }
435
+ if (
436
+ saved.submissionState !== undefined &&
437
+ !["prepared", "possible"].includes(saved.submissionState)
438
+ )
439
+ throw fail(
440
+ "REQUEST_STATE_INVALID",
441
+ "Preserve the original request for review.",
442
+ );
443
+ if (
444
+ expectedInput &&
445
+ JSON.stringify(input) !== JSON.stringify(callInput(expectedInput, service))
446
+ ) {
447
+ const next = callInput(expectedInput, service);
448
+ if (
449
+ saved.quoteAcceptanceRejected === true &&
450
+ JSON.stringify(input.request) === JSON.stringify(next.request) &&
451
+ input.requestedMaxUsageUnits === next.requestedMaxUsageUnits &&
452
+ !(await cachedResult(connection, saved))
453
+ ) {
454
+ saved = { ...saved, input: next, quoteAcceptanceRejected: false };
455
+ await publishJson(path, saved, { replace: true });
456
+ input = next;
457
+ } else
458
+ throw fail(
459
+ "REQUEST_CONFLICT",
460
+ "This request identifier already belongs to different input.",
461
+ );
462
+ }
463
+ return { ...saved, input };
464
+ }
465
+ function verifiedResult(body, input, service) {
466
+ try {
467
+ if (body?.ok !== true) throw Error();
468
+ return {
469
+ ok: true,
470
+ result: normalizeRuntimeResult(
471
+ body.result,
472
+ {
473
+ reference: service,
474
+ skillId: service.skillId,
475
+ profileHash: service.profileHash,
476
+ requestedMaxUsageUnits: input.requestedMaxUsageUnits,
477
+ },
478
+ hashPrivateRequest(input.request),
479
+ ),
480
+ };
481
+ } catch {
482
+ throw fail(
483
+ "RUNTIME_RESPONSE_INVALID",
484
+ "Runtime did not return verified delivery for this service and input. Retry the same call.",
485
+ );
486
+ }
487
+ }
488
+ async function cachedResult(connection, saved) {
489
+ const path = checkpointPath(connection, saved.requestId).replace(
490
+ /\.json$/,
491
+ ".result.json",
492
+ );
493
+ let value;
494
+ try {
495
+ value = JSON.parse(await readPrivateFile(path, 2 * 1048576));
496
+ } catch (error) {
497
+ if (error.code === "ENOENT") return null;
498
+ throw fail(
499
+ "REQUEST_STATE_INVALID",
500
+ "Preserve the saved result for inspection; no replacement purchase was started.",
501
+ );
502
+ }
503
+ if (
504
+ value?.version !== "agentpay.runtime-client-result.v1" ||
505
+ value.requestId !== saved.requestId
506
+ )
507
+ throw fail(
508
+ "REQUEST_STATE_INVALID",
509
+ "Preserve the saved result for inspection; no replacement purchase was started.",
510
+ );
511
+ return verifiedResult(value.body, effectiveInput(saved), saved.service);
512
+ }
513
+ function effectiveInput(saved) {
514
+ return {
515
+ ...saved.input,
516
+ ...(saved.preparedRequest === undefined
517
+ ? {}
518
+ : { request: serviceRequest(saved.preparedRequest) }),
519
+ };
520
+ }
521
+ export async function invokeService(
522
+ connection,
523
+ service,
524
+ checkpoint,
525
+ options = {},
526
+ ) {
527
+ const path = checkpointPath(connection, checkpoint.requestId);
528
+ // A missing checkpoint must not create directories or acquire a replacement identity.
529
+ await resumeInvocation(
530
+ connection,
531
+ service,
532
+ checkpoint.requestId,
533
+ checkpoint.input,
534
+ );
535
+ try {
536
+ return await withClientRequestLock(path + ".lock", () =>
537
+ invokeSaved(connection, service, checkpoint, options),
538
+ );
539
+ } catch (error) {
540
+ if (error.code === "REQUEST_BUSY") throw fail(error.code, error.message);
541
+ throw error;
542
+ }
543
+ }
544
+ async function invokeSaved(
545
+ connection,
546
+ service,
547
+ checkpoint,
548
+ { timeoutMs = 300000, inputFiles, prepareRequest } = {},
549
+ ) {
550
+ // Re-read durable state: no POST is allowed for an unpersisted or changed request.
551
+ let saved = await resumeInvocation(
552
+ connection,
553
+ service,
554
+ checkpoint.requestId,
555
+ checkpoint.input,
556
+ );
557
+ const cached = await cachedResult(connection, saved);
558
+ if (cached) return { ...cached, reusedResult: true };
559
+ const checked = await checkRuntime(connection, service);
560
+ if (!checked.originalRequestRecovery)
561
+ throw fail(
562
+ "RUNTIME_UPDATE_REQUIRED",
563
+ "Update and restart User Runtime before continuing this original request.",
564
+ );
565
+ const token = await callerToken(connection);
566
+ let actualInput = effectiveInput(saved),
567
+ originalOrderRequestId;
568
+ if (saved.submissionState !== "prepared") {
569
+ const history = await requestJson(
570
+ new URL("/services/history", connection.runtimeUrl),
571
+ {
572
+ method: "POST",
573
+ headers: {
574
+ "content-type": "application/json",
575
+ authorization: `Bearer ${token}`,
576
+ "idempotency-key": saved.requestId,
577
+ },
578
+ body: JSON.stringify({
579
+ version: "agentpay.runtime-call.v1",
580
+ service: saved.service,
581
+ ...actualInput,
582
+ }),
583
+ },
584
+ 10000,
585
+ );
586
+ const found = history.body?.result;
587
+ if (
588
+ !history.response.ok ||
589
+ history.body?.ok !== true ||
590
+ !/^0x[0-9a-f]{64}$/.test(found?.orderRequestId || "") ||
591
+ (saved.originalOrderRequestId &&
592
+ saved.originalOrderRequestId !== found.orderRequestId)
593
+ )
594
+ throw fail(
595
+ "ORIGINAL_REQUEST_NOT_FOUND",
596
+ "Original Runtime history is missing or differs. Keep this request; do not start a replacement purchase.",
597
+ );
598
+ originalOrderRequestId = found.orderRequestId;
599
+ if (found.delivery) {
600
+ const verified = verifiedResult(
601
+ { ok: true, result: found.delivery },
602
+ actualInput,
603
+ saved.service,
604
+ );
605
+ await persistClientResult(connection, saved, verified);
606
+ return { ...(await cachedResult(connection, saved)), reusedResult: true };
607
+ }
608
+ }
609
+ saved = await prepareInputSnapshot(connection, saved, {
610
+ inputFiles,
611
+ prepareRequest,
612
+ });
613
+ actualInput = effectiveInput(saved);
614
+ saved = {
615
+ ...saved,
616
+ submissionState: "possible",
617
+ ...(originalOrderRequestId ? { originalOrderRequestId } : {}),
618
+ };
619
+ await publishJson(checkpointPath(connection, saved.requestId), saved, {
620
+ replace: true,
621
+ });
622
+ const { response, body } = await requestJson(
623
+ new URL("/services/invoke", connection.runtimeUrl),
624
+ {
625
+ method: "POST",
626
+ headers: {
627
+ "content-type": "application/json",
628
+ authorization: `Bearer ${token}`,
629
+ "idempotency-key": saved.requestId,
630
+ ...(originalOrderRequestId
631
+ ? { "x-agentpay-original-request": originalOrderRequestId }
632
+ : {}),
633
+ },
634
+ body: JSON.stringify({
635
+ version: "agentpay.runtime-call.v1",
636
+ service: saved.service,
637
+ ...actualInput,
638
+ }),
639
+ },
640
+ timeoutMs,
641
+ );
642
+ if (!response.ok || body?.ok !== true) {
643
+ if (body?.error?.code === "QUOTE_TERMS_CHANGED")
644
+ await publishJson(
645
+ checkpointPath(connection, saved.requestId),
646
+ { ...saved, quoteAcceptanceRejected: true },
647
+ { replace: true },
648
+ );
649
+ const known = {
650
+ ORIGINAL_REQUEST_NOT_FOUND:
651
+ "Original Runtime history changed or disappeared. Preserve the request; no replacement purchase was created.",
652
+ ORDER_EXPIRED:
653
+ "This Order expired without payment. No result is expected. The original request will not make another payment.",
654
+ ORDER_TERMS_MISMATCH:
655
+ "The prepared Order differs from accepted conditions. No payment was signed. Run agentpay orders list and preserve this Order for support; do not replace it.",
656
+ QUOTE_TERMS_CHANGED:
657
+ "Price or delivery time changed before Order creation. Read --quote, reevaluate the terms and continue this same request; do not use --force.",
658
+ QUOTE_TERMS_CONFLICT:
659
+ "This request already accepted other terms. Preserve its original identifier and check its Order.",
660
+ ORDER_ALREADY_SETTLED:
661
+ "This Order is settled but its local result is unavailable. Preserve the original request; do not repeat the purchase.",
662
+ RUNTIME_RECOVERY_REQUIRED:
663
+ "Runtime is running but local history needs review. Run agentpay status; do not delete history or create replacement purchases.",
664
+ ORDER_CLOSING:
665
+ "Order closure is pending. Query this Order; do not submit another payment.",
666
+ ORDER_CANCELLED: "This Order was cancelled. Preserve its saved request.",
667
+ ORDER_REFUNDED:
668
+ "This Order was refunded. Cumulative Policy spend is not restored.",
669
+ LOCKED:
670
+ "From the User repository, run agentpay wallet unlock, then resume this request.",
671
+ POLICY_UNBOUND:
672
+ "Complete spending authorization and Wallet binding in AgentPay setup, then resume this request.",
673
+ POLICY_MIGRATION_PENDING:
674
+ "Finish the pending Policy migration in your terminal, then resume this request.",
675
+ UNAUTHORIZED: "Run AgentPay setup to repair the local caller credential.",
676
+ WALLET_LOCKED:
677
+ "From the User repository, run agentpay wallet unlock, then resume this request.",
678
+ SERVICE_MAINTENANCE:
679
+ "Service is under maintenance. Resume this request later.",
680
+ SERVICE_NOT_READY:
681
+ "Service is not ready. Check its status, then resume this request.",
682
+ };
683
+ const raw = body?.error?.code;
684
+ const code = Object.hasOwn(known, raw) ? raw : "RUNTIME_REQUEST_FAILED";
685
+ throw new RuntimeClientError(
686
+ code,
687
+ known[code] ||
688
+ "Runtime could not finish this request. Check local Wallet/Runtime status, then resume the same request.",
689
+ {
690
+ ...(typeof raw === "string" &&
691
+ /^[A-Z][A-Z0-9_]{0,79}$/.test(raw) &&
692
+ !raw.includes(token)
693
+ ? { runtimeCode: raw }
694
+ : {}),
695
+ },
696
+ );
697
+ }
698
+ const verified = verifiedResult(body, actualInput, saved.service);
699
+ await persistClientResult(connection, saved, verified);
700
+ return { ...(await cachedResult(connection, saved)), reusedResult: false };
701
+ }
702
+
703
+ async function persistClientResult(connection, saved, verified) {
704
+ const resultPath = checkpointPath(connection, saved.requestId).replace(
705
+ /\.json$/,
706
+ ".result.json",
707
+ );
708
+ try {
709
+ await publishJson(resultPath, {
710
+ version: "agentpay.runtime-client-result.v1",
711
+ requestId: saved.requestId,
712
+ body: verified,
713
+ });
714
+ } catch (error) {
715
+ if (error.code !== "EEXIST") throw error;
716
+ }
717
+ await syncDirectory(dirname(resultPath));
718
+ }
719
+
720
+ // Serialize only request selection/state changes; network calls never hold this lock.
721
+ // A crash in this short section leaves a lock and fails closed for operator review.
722
+ async function withPendingRequests(connection, service, input, work) {
723
+ input = callInput(input, service);
724
+ const reference = identity(service);
725
+ // Adapter/cap edits are not a new purchase intent: find the old record and let
726
+ // its full immutable binding reject changes unless the caller explicitly forces.
727
+ const scopeService = {
728
+ version: reference.version,
729
+ skillId: reference.skillId,
730
+ profileHash: reference.profileHash,
731
+ profile: reference.profile,
732
+ };
733
+ const binding = { service: scopeService, input: input.request };
734
+ const key = createHash("sha256")
735
+ .update(JSON.stringify(binding))
736
+ .digest("hex");
737
+ const parent = join(connection.dataDir, "runtime", "client-pending");
738
+ await privateDirectory(connection.dataDir);
739
+ await privateDirectory(join(connection.dataDir, "runtime"));
740
+ await privateDirectory(parent);
741
+ const lock = join(parent, key + ".lock");
742
+ try {
743
+ await mkdir(lock, { mode: 0o700 });
744
+ } catch (error) {
745
+ if (error.code === "EEXIST")
746
+ throw fail(
747
+ "REQUEST_SELECTION_BUSY",
748
+ "Local request recovery is busy. Retry the same call; preserve state for inspection if this persists.",
749
+ );
750
+ throw error;
751
+ }
752
+ try {
753
+ const path = join(parent, key + ".json");
754
+ let saved;
755
+ try {
756
+ saved = JSON.parse(await readPrivateFile(path, 524288));
757
+ } catch (error) {
758
+ if (error.code !== "ENOENT")
759
+ throw fail(
760
+ "REQUEST_STATE_INVALID",
761
+ "Preserve local request state for inspection; no new purchase was started.",
762
+ );
763
+ }
764
+ if (saved === undefined) {
765
+ const names = await readdir(parent);
766
+ if (names.some((n) => n.endsWith(".lock") && n !== key + ".lock"))
767
+ throw fail(
768
+ "REQUEST_SELECTION_BUSY",
769
+ "Another request selection is active or needs review. Preserve the old selection records.",
770
+ );
771
+ if (names.length > 20000)
772
+ throw fail(
773
+ "REQUEST_STATE_INVALID",
774
+ "Local request selection history needs review.",
775
+ );
776
+ const matches = [];
777
+ for (const name of names.filter((n) => /^[a-f0-9]{64}\.json$/.test(n))) {
778
+ if (name === key + ".json") continue;
779
+ let old;
780
+ try {
781
+ old = JSON.parse(await readPrivateFile(join(parent, name), 524288));
782
+ } catch {
783
+ throw fail(
784
+ "REQUEST_STATE_INVALID",
785
+ "Existing request selection could not be read. Preserve history.",
786
+ );
787
+ }
788
+ if (
789
+ old?.version === "agentpay.runtime-client-pending.v1" &&
790
+ JSON.stringify({
791
+ service: old.binding?.service,
792
+ input: old.binding?.input,
793
+ }) === JSON.stringify(binding)
794
+ ) {
795
+ if (
796
+ !Array.isArray(old.pending) ||
797
+ old.pending.some((id) => typeof id !== "string" || !ID.test(id)) ||
798
+ (old.currentId !== undefined &&
799
+ (typeof old.currentId !== "string" || !ID.test(old.currentId)))
800
+ )
801
+ throw fail(
802
+ "REQUEST_STATE_INVALID",
803
+ "Original request selection needs review.",
804
+ );
805
+ matches.push(old);
806
+ }
807
+ }
808
+ if (matches.length) {
809
+ const ids = new Set(
810
+ matches.flatMap((r) => [
811
+ ...(r.pending || []),
812
+ ...(r.currentId ? [r.currentId] : []),
813
+ ]),
814
+ );
815
+ saved = {
816
+ version: "agentpay.runtime-client-pending.v2",
817
+ binding,
818
+ pending: [...ids],
819
+ ...(ids.size === 1 ? { currentId: [...ids][0] } : {}),
820
+ };
821
+ await publishJson(path, saved);
822
+ }
823
+ }
824
+ if (
825
+ saved !== undefined &&
826
+ (!saved ||
827
+ saved.version !== "agentpay.runtime-client-pending.v2" ||
828
+ JSON.stringify(saved.binding) !== JSON.stringify(binding) ||
829
+ !Array.isArray(saved.pending) ||
830
+ (saved.currentId !== undefined &&
831
+ (typeof saved.currentId !== "string" || !ID.test(saved.currentId))) ||
832
+ saved.pending.some((id) => typeof id !== "string" || !ID.test(id)) ||
833
+ new Set(saved.pending).size !== saved.pending.length)
834
+ )
835
+ throw fail(
836
+ "REQUEST_STATE_INVALID",
837
+ "Preserve local request state for inspection; no new purchase was started.",
838
+ );
839
+ const state = saved || {
840
+ version: "agentpay.runtime-client-pending.v2",
841
+ binding,
842
+ pending: [],
843
+ };
844
+ return await work(state, async () =>
845
+ publishJson(path, state, { replace: saved !== undefined }),
846
+ );
847
+ } finally {
848
+ await rmdir(lock);
849
+ }
850
+ }
851
+ export async function selectInvocation(
852
+ connection,
853
+ service,
854
+ input,
855
+ { force = false, requestId } = {},
856
+ ) {
857
+ if (force && requestId !== undefined)
858
+ throw fail(
859
+ "INVALID_ARGUMENTS",
860
+ "Use --force without an explicit request identifier.",
861
+ );
862
+ return withPendingRequests(
863
+ connection,
864
+ service,
865
+ input,
866
+ async (state, save) => {
867
+ if (!force && requestId === undefined && state.pending.length > 1)
868
+ throw fail(
869
+ "REQUEST_AMBIGUOUS",
870
+ "Several unfinished requests match. Resume the intended request by its saved identifier; no new purchase was started.",
871
+ );
872
+ const id =
873
+ requestId || (!force ? state.currentId || state.pending[0] : undefined);
874
+ const checkpoint = id
875
+ ? await prepareInvocation(connection, service, input, id)
876
+ : await prepareInvocation(connection, service, input);
877
+ if (!state.pending.includes(checkpoint.requestId)) {
878
+ const cached = await cachedResult(connection, checkpoint);
879
+ if (!cached) state.pending.push(checkpoint.requestId);
880
+ const changed = !cached || state.currentId !== checkpoint.requestId;
881
+ state.currentId = checkpoint.requestId;
882
+ if (changed) await save();
883
+ }
884
+ return checkpoint;
885
+ },
886
+ );
887
+ }
888
+ export async function completeInvocation(connection, service, checkpoint) {
889
+ const saved = await resumeInvocation(
890
+ connection,
891
+ service,
892
+ checkpoint.requestId,
893
+ checkpoint.input,
894
+ );
895
+ if (!(await cachedResult(connection, saved)))
896
+ throw fail(
897
+ "REQUEST_STATE_INVALID",
898
+ "Keep this request pending until verified delivery has been saved.",
899
+ );
900
+ if (saved.fileDeclarations?.length) {
901
+ // Release only after both local verified-result persistence and independent
902
+ // Runtime verification. Failure leaves input protected; it never loses delivery.
903
+ try {
904
+ const token = await callerToken(connection);
905
+ await requestJson(
906
+ new URL("/files/input-release", connection.runtimeUrl),
907
+ {
908
+ method: "POST",
909
+ headers: {
910
+ "content-type": "application/json",
911
+ authorization: `Bearer ${token}`,
912
+ "idempotency-key": saved.requestId,
913
+ },
914
+ body: JSON.stringify({
915
+ version: "agentpay.runtime-call.v1",
916
+ service: saved.service,
917
+ ...effectiveInput(saved),
918
+ }),
919
+ },
920
+ 10000,
921
+ );
922
+ } catch {}
923
+ }
924
+ return withPendingRequests(
925
+ connection,
926
+ service,
927
+ checkpoint.input,
928
+ async (state, save) => {
929
+ if (state.pending.includes(checkpoint.requestId)) {
930
+ state.pending = state.pending.filter(
931
+ (id) => id !== checkpoint.requestId,
932
+ );
933
+ await save();
934
+ }
935
+ },
936
+ );
937
+ }
938
+
939
+ export async function readServiceOffer(
940
+ connection,
941
+ service,
942
+ { timeoutMs = 7000 } = {},
943
+ ) {
944
+ const token = await callerToken(connection),
945
+ expected = identity(service);
946
+ const { response, body } = await requestJson(
947
+ new URL("/services/offer", runtimeBase(connection.runtimeUrl)),
948
+ {
949
+ method: "POST",
950
+ headers: {
951
+ "content-type": "application/json",
952
+ authorization: "Bearer " + token,
953
+ },
954
+ body: JSON.stringify({ service: expected }),
955
+ },
956
+ timeoutMs,
957
+ );
958
+ if (!response.ok || body?.ok !== true)
959
+ throw fail(
960
+ "OFFER_UNAVAILABLE",
961
+ body?.error?.message || "Current offer unavailable",
962
+ );
963
+ return body.result;
964
+ }
965
+
966
+ // Explicit file operations are independent of service execution and payment.
967
+ // The Skill chooses which URL to save. No links in output are fetched implicitly.
968
+ export async function cacheResultFile(
969
+ connection,
970
+ { orderId, fileKey, url, filename, sha256 },
971
+ ) {
972
+ return resultFileOperation(connection, "cache", {
973
+ orderId,
974
+ fileKey,
975
+ url,
976
+ ...(filename === undefined ? {} : { filename }),
977
+ ...(sha256 === undefined ? {} : { sha256 }),
978
+ });
979
+ }
980
+ export async function listResultFiles(
981
+ connection,
982
+ orderId,
983
+ { offset = 0, limit = 100 } = {},
984
+ ) {
985
+ return resultFileOperation(connection, "list", { orderId, offset, limit });
986
+ }
987
+ export async function removeResultFile(
988
+ connection,
989
+ { orderId, fileKey, confirmed },
990
+ ) {
991
+ return resultFileOperation(connection, "remove", {
992
+ orderId,
993
+ fileKey,
994
+ confirmed,
995
+ });
996
+ }
997
+ async function resultFileOperation(connection, action, input) {
998
+ const token = await callerToken(connection);
999
+ const { response, body } = await requestJson(
1000
+ new URL("/files/" + action, runtimeBase(connection.runtimeUrl)),
1001
+ {
1002
+ method: "POST",
1003
+ headers: {
1004
+ "content-type": "application/json",
1005
+ authorization: `Bearer ${token}`,
1006
+ },
1007
+ body: JSON.stringify(input),
1008
+ },
1009
+ action === "cache" ? 86410000 : 10000,
1010
+ );
1011
+ if (!response.ok || body?.ok !== true)
1012
+ throw fail(
1013
+ body?.error?.code || "FILE_OPERATION_FAILED",
1014
+ body?.error?.message ||
1015
+ "Local file operation failed. No new purchase was made.",
1016
+ );
1017
+ return body.result;
1018
+ }
1019
+
1020
+ export async function cacheManagement(connection, input) {
1021
+ const token = await callerToken(connection);
1022
+ const { response, body } = await requestJson(
1023
+ new URL("/cache/manage", runtimeBase(connection.runtimeUrl)),
1024
+ {
1025
+ method: "POST",
1026
+ headers: {
1027
+ "content-type": "application/json",
1028
+ authorization: `Bearer ${token}`,
1029
+ },
1030
+ body: JSON.stringify(input),
1031
+ },
1032
+ 120000,
1033
+ );
1034
+ if (!response.ok || body?.ok !== true)
1035
+ throw fail(
1036
+ body?.error?.code || "FILE_OPERATION_FAILED",
1037
+ body?.error?.message ||
1038
+ "Cache management is unavailable. Update Runtime or use --offline after stopping it.",
1039
+ );
1040
+ return body.result;
1041
+ }
1042
+
1043
+ function validateFileDeclarations(declarations) {
1044
+ if (!Array.isArray(declarations) || declarations.length > 20)
1045
+ throw fail("INPUT_FILES_INVALID", "Declare at most 20 input files.");
1046
+ const keys = new Set();
1047
+ for (const f of declarations) {
1048
+ if (
1049
+ !f ||
1050
+ typeof f !== "object" ||
1051
+ Array.isArray(f) ||
1052
+ Object.keys(f).some(
1053
+ (k) =>
1054
+ !["fileKey", "sourcePath", "url", "filename", "sha256"].includes(k),
1055
+ ) ||
1056
+ typeof f.fileKey !== "string" ||
1057
+ !/^[A-Za-z0-9._-]{1,80}$/.test(f.fileKey) ||
1058
+ keys.has(f.fileKey) ||
1059
+ (typeof f.sourcePath === "string") === (typeof f.url === "string") ||
1060
+ (typeof f.sourcePath === "string" &&
1061
+ (!isAbsolute(f.sourcePath) || f.sourcePath.length > 8192)) ||
1062
+ (typeof f.url === "string" && f.url.length > 8192) ||
1063
+ (f.filename !== undefined &&
1064
+ (typeof f.filename !== "string" || f.filename.length > 200)) ||
1065
+ (f.sha256 !== undefined && !/^[a-f0-9]{64}$/.test(f.sha256))
1066
+ )
1067
+ throw fail(
1068
+ "INPUT_FILES_INVALID",
1069
+ "Declare unique file keys, one source per file and bounded metadata.",
1070
+ );
1071
+ keys.add(f.fileKey);
1072
+ }
1073
+ }
1074
+
1075
+ async function prepareInputSnapshot(
1076
+ connection,
1077
+ saved,
1078
+ { inputFiles, prepareRequest },
1079
+ ) {
1080
+ if (saved.fileDeclarations === undefined) {
1081
+ // A historical or possibly submitted request must never reinterpret its input.
1082
+ if (saved.submissionState !== "prepared" || !inputFiles) return saved;
1083
+ const declarations = await inputFiles(structuredClone(saved.input.request));
1084
+ validateFileDeclarations(declarations);
1085
+ if (declarations.length && !prepareRequest)
1086
+ throw fail(
1087
+ "INPUT_ADAPTER_REQUIRED",
1088
+ "This Skill must prepare its business request from the saved file copies.",
1089
+ );
1090
+ saved = { ...saved, fileDeclarations: declarations };
1091
+ await publishJson(checkpointPath(connection, saved.requestId), saved, {
1092
+ replace: true,
1093
+ });
1094
+ }
1095
+ if (!saved.fileDeclarations.length) return saved;
1096
+ const rows = await inputFileOperation(connection, {
1097
+ requestId: saved.requestId,
1098
+ files: saved.fileDeclarations,
1099
+ });
1100
+ if (saved.preparedRequest === undefined) {
1101
+ if (saved.submissionState !== "prepared" || !prepareRequest)
1102
+ throw fail(
1103
+ "INPUT_ADAPTER_REQUIRED",
1104
+ "Restore this Skill’s input adapter to continue the original request.",
1105
+ );
1106
+ const request = serviceRequest(
1107
+ await prepareRequest({
1108
+ request: structuredClone(saved.input.request),
1109
+ files: rows,
1110
+ requestId: saved.requestId,
1111
+ }),
1112
+ );
1113
+ await inputFileOperation(connection, {
1114
+ requestId: saved.requestId,
1115
+ files: saved.fileDeclarations,
1116
+ });
1117
+ saved = { ...saved, preparedRequest: request };
1118
+ await publishJson(checkpointPath(connection, saved.requestId), saved, {
1119
+ replace: true,
1120
+ });
1121
+ }
1122
+ return saved;
1123
+ }
1124
+ async function inputFileOperation(connection, input) {
1125
+ const token = await callerToken(connection);
1126
+ const { response, body } = await requestJson(
1127
+ new URL("/files/input", runtimeBase(connection.runtimeUrl)),
1128
+ {
1129
+ method: "POST",
1130
+ headers: {
1131
+ "content-type": "application/json",
1132
+ authorization: `Bearer ${token}`,
1133
+ },
1134
+ body: JSON.stringify(input),
1135
+ },
1136
+ 86410000,
1137
+ );
1138
+ if (!response.ok || body?.ok !== true)
1139
+ throw fail(
1140
+ body?.error?.code || "INPUT_SNAPSHOT_FAILED",
1141
+ body?.error?.message ||
1142
+ "Input snapshot is incomplete. Preserve the original request.",
1143
+ );
1144
+ return body.result;
1145
+ }