@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,114 @@
|
|
|
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
|
+
// Only public authorization fields are persisted; never Wallet unlock input.
|
|
5
|
+
export function authorizationFields({
|
|
6
|
+
context,
|
|
7
|
+
checkpoint,
|
|
8
|
+
ask,
|
|
9
|
+
save,
|
|
10
|
+
write,
|
|
11
|
+
details,
|
|
12
|
+
}) {
|
|
13
|
+
const allowed = ["safe", "policyChoice", "budget", "perPayment", "days"];
|
|
14
|
+
if (
|
|
15
|
+
checkpoint &&
|
|
16
|
+
(checkpoint.version !== 2 ||
|
|
17
|
+
checkpoint.context !== context ||
|
|
18
|
+
!checkpoint.fields ||
|
|
19
|
+
typeof checkpoint.fields !== "object" ||
|
|
20
|
+
Array.isArray(checkpoint.fields) ||
|
|
21
|
+
Object.entries(checkpoint.fields).some(
|
|
22
|
+
([key, value]) =>
|
|
23
|
+
!allowed.includes(key) ||
|
|
24
|
+
typeof value !== "string" ||
|
|
25
|
+
value.length > 256,
|
|
26
|
+
))
|
|
27
|
+
) {
|
|
28
|
+
throw Error("AUTHORIZATION_INPUT_CHECKPOINT_MISMATCH");
|
|
29
|
+
}
|
|
30
|
+
let state = checkpoint || { version: 2, context, fields: {} };
|
|
31
|
+
if (Object.keys(state.fields).length)
|
|
32
|
+
write("Continuing saved Safe and spending limits.");
|
|
33
|
+
const visited = [];
|
|
34
|
+
return async (key, prompt, validate, providedValue) => {
|
|
35
|
+
const previous = visited.at(-1);
|
|
36
|
+
if (providedValue === undefined) visited.push(key);
|
|
37
|
+
if (!allowed.includes(key)) throw Error("AUTHORIZATION_FIELD_INVALID");
|
|
38
|
+
if (Object.hasOwn(state.fields, key)) {
|
|
39
|
+
try {
|
|
40
|
+
validate(state.fields[key]);
|
|
41
|
+
} catch {
|
|
42
|
+
throw Error(`SAVED_AUTHORIZATION_FIELD_INVALID: ${key}`);
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
providedValue !== undefined &&
|
|
46
|
+
validate(state.fields[key]) !== validate(providedValue)
|
|
47
|
+
)
|
|
48
|
+
throw Error(
|
|
49
|
+
`SAVED_AUTHORIZATION_FIELD_MISMATCH: ${key}. Saved settings and authorization inputs differ; review before continuing.`,
|
|
50
|
+
);
|
|
51
|
+
return state.fields[key];
|
|
52
|
+
}
|
|
53
|
+
if (providedValue !== undefined) {
|
|
54
|
+
const normalized = validate(providedValue);
|
|
55
|
+
const next = {
|
|
56
|
+
...state,
|
|
57
|
+
fields: { ...state.fields, [key]: String(normalized) },
|
|
58
|
+
};
|
|
59
|
+
await save(next);
|
|
60
|
+
state = next;
|
|
61
|
+
return state.fields[key];
|
|
62
|
+
}
|
|
63
|
+
while (true) {
|
|
64
|
+
const value = (
|
|
65
|
+
await ask(
|
|
66
|
+
`${prompt}\n('back' for previous, 'details' for information, 'q' to return)\n> `,
|
|
67
|
+
)
|
|
68
|
+
).trim();
|
|
69
|
+
if (value.toLowerCase() === "back") {
|
|
70
|
+
if (!previous) {
|
|
71
|
+
write("This is the first editable item.");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const start = allowed.indexOf(previous);
|
|
75
|
+
const next = {
|
|
76
|
+
...state,
|
|
77
|
+
fields: Object.fromEntries(
|
|
78
|
+
Object.entries(state.fields).filter(
|
|
79
|
+
([name]) => allowed.indexOf(name) < start,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
await save(next);
|
|
84
|
+
throw Object.assign(Error("Return to previous item"), {
|
|
85
|
+
code: "AUTHORIZATION_BACK",
|
|
86
|
+
checkpoint: next,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (value.toLowerCase() === "q")
|
|
90
|
+
throw Object.assign(Error("Setup paused; saved inputs are retained."), {
|
|
91
|
+
code: "CANCELLED",
|
|
92
|
+
});
|
|
93
|
+
if (value.toLowerCase() === "details") {
|
|
94
|
+
write(JSON.stringify(details, null, 2));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let normalized;
|
|
98
|
+
try {
|
|
99
|
+
if (value.length > 256) throw Error();
|
|
100
|
+
normalized = validate(value) ?? value;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
write(error.message || "Check this value and try again.");
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const next = {
|
|
106
|
+
...state,
|
|
107
|
+
fields: { ...state.fields, [key]: String(normalized) },
|
|
108
|
+
};
|
|
109
|
+
await save(next);
|
|
110
|
+
state = next;
|
|
111
|
+
return state.fields[key];
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { isAbsolute, join } from "node:path";
|
|
5
|
+
function checked(value) {
|
|
6
|
+
if (
|
|
7
|
+
typeof value !== "string" ||
|
|
8
|
+
!isAbsolute(value) ||
|
|
9
|
+
/[\x00-\x1f\x7f]/.test(value)
|
|
10
|
+
)
|
|
11
|
+
throw Error("Autostart paths must be absolute without control characters.");
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
const unitArg = (value) =>
|
|
15
|
+
'"' +
|
|
16
|
+
checked(value)
|
|
17
|
+
.replaceAll("\\", "\\\\")
|
|
18
|
+
.replaceAll('"', '\\"')
|
|
19
|
+
.replaceAll("%", "%%")
|
|
20
|
+
.replaceAll("$", () => "$$") +
|
|
21
|
+
'"';
|
|
22
|
+
export const runtimeUnits = [
|
|
23
|
+
"agentpay-local-wallet.service",
|
|
24
|
+
"agentpay-local-runtime.service",
|
|
25
|
+
];
|
|
26
|
+
export const reminderUnit = "agentpay-wallet-reminder.service";
|
|
27
|
+
export const units = [...runtimeUnits, reminderUnit];
|
|
28
|
+
export function autostartFiles({ node, userDir, dataDir }) {
|
|
29
|
+
const entries = {};
|
|
30
|
+
for (const role of ["wallet", "runtime"])
|
|
31
|
+
entries[`agentpay-local-${role}.service`] =
|
|
32
|
+
`# Managed by AgentPay User\n[Unit]\nDescription=AgentPay local ${role}\n${role === "runtime" ? "Wants=agentpay-local-wallet.service\nAfter=agentpay-local-wallet.service\n" : ""}StartLimitIntervalSec=60\nStartLimitBurst=3\n\n[Service]\nType=simple\nExecStart=${[node, join(userDir, "scripts/local-service.mjs")].map(unitArg).join(" ")} ${role} ${unitArg(dataDir)}\nRestart=on-failure\nRestartSec=10\nUMask=0077\nKillMode=control-group\nTimeoutStopSec=30\n\n[Install]\nWantedBy=default.target\n`;
|
|
33
|
+
entries[reminderUnit] =
|
|
34
|
+
`# Managed by AgentPay User\n[Unit]\nDescription=AgentPay Wallet login reminder\nPartOf=graphical-session.target\nAfter=graphical-session-pre.target agentpay-local-wallet.service\n\n[Service]\nType=oneshot\nExecStart=${[node, join(userDir, "scripts/login-notify.mjs"), dataDir].map(unitArg).join(" ")}\nRemainAfterExit=yes\nTimeoutStartSec=90\nUMask=0077\n\n[Install]\nWantedBy=graphical-session.target\n`;
|
|
35
|
+
return entries;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Accept only an exact previously generated unit for this same private directory.
|
|
39
|
+
// This lets repair relocate program/Node paths without accepting custom unit bodies.
|
|
40
|
+
export function isManagedUnit(body, name, dataDir) {
|
|
41
|
+
try {
|
|
42
|
+
const line = body.split('\n').find(line => line.startsWith('ExecStart='));
|
|
43
|
+
const quoted = [...line.matchAll(/"(?:\\.|[^"\\])*"/g)].map(([text]) => JSON.parse(text).replaceAll('%%','%').replaceAll('$$','$'));
|
|
44
|
+
const [node, script] = quoted;
|
|
45
|
+
const suffix = name === reminderUnit ? '/scripts/login-notify.mjs' : '/scripts/local-service.mjs';
|
|
46
|
+
if (!script.endsWith(suffix)) return false;
|
|
47
|
+
const userDir = script.slice(0, -suffix.length);
|
|
48
|
+
return autostartFiles({node, userDir, dataDir})[name] === body;
|
|
49
|
+
} catch { return false; }
|
|
50
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
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 { constants } from "node:fs";
|
|
5
|
+
import { open, unlink, realpath } from "node:fs/promises";
|
|
6
|
+
import { join, resolve, dirname, sep, extname } from "node:path";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { resolveUserDataDir } from "./local-data-directory.mjs";
|
|
10
|
+
import { loadRuntimeConnection, cacheManagement } from "./runtime-client.mjs";
|
|
11
|
+
import { createFileCache } from "./file-cache.mjs";
|
|
12
|
+
import { readCacheLimits } from "./file-cache-settings.mjs";
|
|
13
|
+
import { manageCache } from "./cache-management.mjs";
|
|
14
|
+
import { withResultCacheView } from "./result-cache-view.mjs";
|
|
15
|
+
const safe = (v) => String(v ?? "").replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
16
|
+
const size = (n) =>
|
|
17
|
+
n < 1024
|
|
18
|
+
? `${n} B`
|
|
19
|
+
: n < 1048576
|
|
20
|
+
? `${(n / 1024).toFixed(1)} KiB`
|
|
21
|
+
: `${(n / 1048576).toFixed(1)} MiB`;
|
|
22
|
+
const contentSize = (row) => {
|
|
23
|
+
if (row.kind === "result")
|
|
24
|
+
return `Text (JSON) · content ${Number.isSafeInteger(row.contentBytes) ? size(row.contentBytes) : "unknown"} · cached ${size(row.storageBytes ?? row.bytes)}`;
|
|
25
|
+
const extension = extname(row.filename || "")
|
|
26
|
+
.slice(1)
|
|
27
|
+
.toUpperCase();
|
|
28
|
+
const type = /^[A-Z0-9]{1,12}$/.test(extension)
|
|
29
|
+
? `File (.${extension.toLowerCase()})`
|
|
30
|
+
: "File";
|
|
31
|
+
return `${type} · ${row.purpose === "input" ? "input" : "output"} · cached ${size(row.bytes)}`;
|
|
32
|
+
};
|
|
33
|
+
const prompt = async (message) => {
|
|
34
|
+
const r = createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
try {
|
|
36
|
+
return await r.question(message + "\n> ");
|
|
37
|
+
} finally {
|
|
38
|
+
r.close();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export async function exportCacheFile(row, target) {
|
|
42
|
+
const destination = resolve(target),
|
|
43
|
+
parent = await realpath(dirname(destination)),
|
|
44
|
+
cacheDir = await realpath(dirname(row.localPath));
|
|
45
|
+
if (parent === cacheDir || parent.startsWith(cacheDir + sep))
|
|
46
|
+
throw Error("Choose an export location outside the managed cache.");
|
|
47
|
+
let input,
|
|
48
|
+
output,
|
|
49
|
+
created = false;
|
|
50
|
+
try {
|
|
51
|
+
input = await open(
|
|
52
|
+
row.localPath,
|
|
53
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW || 0),
|
|
54
|
+
);
|
|
55
|
+
const st = await input.stat();
|
|
56
|
+
if (
|
|
57
|
+
!st.isFile() ||
|
|
58
|
+
st.nlink !== 1 ||
|
|
59
|
+
st.size !== row.bytes ||
|
|
60
|
+
(typeof process.getuid === "function" &&
|
|
61
|
+
(st.uid !== process.getuid() || st.mode & 0o077))
|
|
62
|
+
)
|
|
63
|
+
throw Error("Saved file is unavailable or changed.");
|
|
64
|
+
output = await open(destination, "wx", 0o600);
|
|
65
|
+
created = true;
|
|
66
|
+
const hash = createHash("sha256");
|
|
67
|
+
let bytes = 0;
|
|
68
|
+
for await (const chunk of input.createReadStream({ autoClose: false })) {
|
|
69
|
+
bytes += chunk.length;
|
|
70
|
+
if (bytes > row.bytes) throw Error("Saved file changed during export.");
|
|
71
|
+
hash.update(chunk);
|
|
72
|
+
await output.writeFile(chunk);
|
|
73
|
+
}
|
|
74
|
+
if (bytes !== row.bytes || hash.digest("hex") !== row.sha256)
|
|
75
|
+
throw Error("Saved file failed verification.");
|
|
76
|
+
await output.sync();
|
|
77
|
+
await output.close();
|
|
78
|
+
output = null;
|
|
79
|
+
const h = await open(parent, "r");
|
|
80
|
+
try {
|
|
81
|
+
await h.sync();
|
|
82
|
+
} finally {
|
|
83
|
+
await h.close();
|
|
84
|
+
}
|
|
85
|
+
return destination;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
await output?.close();
|
|
88
|
+
output = null;
|
|
89
|
+
if (created) await unlink(destination).catch(() => {});
|
|
90
|
+
if (e.code === "EEXIST")
|
|
91
|
+
throw Error("Export destination already exists. Choose a new filename.");
|
|
92
|
+
throw e;
|
|
93
|
+
} finally {
|
|
94
|
+
await input?.close();
|
|
95
|
+
await output?.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function runCacheManagement(
|
|
99
|
+
args,
|
|
100
|
+
{ write = console.log, ask = prompt, operate, dataDir } = {},
|
|
101
|
+
) {
|
|
102
|
+
const values = [...args],
|
|
103
|
+
offline = values.includes("--offline");
|
|
104
|
+
if (offline) values.splice(values.indexOf("--offline"), 1);
|
|
105
|
+
const [action = "summary", ...rest] = values;
|
|
106
|
+
const filters = {};
|
|
107
|
+
let invalidFilter = false;
|
|
108
|
+
if (action === "list")
|
|
109
|
+
for (let i = 0; i < rest.length; ) {
|
|
110
|
+
const fields = {
|
|
111
|
+
"--state": "state",
|
|
112
|
+
"--request": "requestId",
|
|
113
|
+
"--order": "orderId",
|
|
114
|
+
};
|
|
115
|
+
if (Object.hasOwn(fields, rest[i])) {
|
|
116
|
+
const key = fields[rest[i]];
|
|
117
|
+
if (
|
|
118
|
+
filters[key] !== undefined ||
|
|
119
|
+
!rest[i + 1] ||
|
|
120
|
+
rest[i + 1].startsWith("--")
|
|
121
|
+
) {
|
|
122
|
+
invalidFilter = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
filters[key] = rest[i + 1];
|
|
126
|
+
rest.splice(i, 2);
|
|
127
|
+
} else i++;
|
|
128
|
+
}
|
|
129
|
+
const usage =
|
|
130
|
+
"Usage: agentpay cache [info | list [PAGE] [input|output] [--state STATE] [--request ID | --order ID] | details ID | export ID PATH | clean | settings [KEY VALUE]]\nAdd --offline only when managing stopped Runtime storage. Wallet unlock is not required.";
|
|
131
|
+
if (
|
|
132
|
+
invalidFilter ||
|
|
133
|
+
!["summary", "list", "details", "export", "clean", "settings"].includes(
|
|
134
|
+
action,
|
|
135
|
+
) ||
|
|
136
|
+
(["summary", "clean"].includes(action) && rest.length) ||
|
|
137
|
+
(action === "details" && rest.length !== 1) ||
|
|
138
|
+
(action === "export" && rest.length !== 2) ||
|
|
139
|
+
(action === "settings" && ![0, 2].includes(rest.length)) ||
|
|
140
|
+
(action === "list" &&
|
|
141
|
+
(rest.length > 2 ||
|
|
142
|
+
(rest[0] !== undefined && !/^[1-9][0-9]{0,5}$/.test(rest[0])) ||
|
|
143
|
+
(rest[1] !== undefined && !["input", "output"].includes(rest[1]))))
|
|
144
|
+
) {
|
|
145
|
+
write(usage);
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
if (!operate) {
|
|
150
|
+
dataDir ??= await resolveUserDataDir();
|
|
151
|
+
if (offline) {
|
|
152
|
+
const state = join(dataDir, "runtime/invocations.json"),
|
|
153
|
+
cache = createFileCache(state, {
|
|
154
|
+
limits: await readCacheLimits(state),
|
|
155
|
+
});
|
|
156
|
+
operate = (body) => manageCache(cache, body);
|
|
157
|
+
} else {
|
|
158
|
+
const connection = await loadRuntimeConnection({ dataDir });
|
|
159
|
+
operate = (body) => cacheManagement(connection, body);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (dataDir)
|
|
163
|
+
operate = withResultCacheView(
|
|
164
|
+
operate,
|
|
165
|
+
dataDir,
|
|
166
|
+
process.env.AGENTPAY_USER_STATE_FILE
|
|
167
|
+
? { statePath: resolve(process.env.AGENTPAY_USER_STATE_FILE) }
|
|
168
|
+
: {},
|
|
169
|
+
);
|
|
170
|
+
write(
|
|
171
|
+
"\n------------------------------------------------------------\n> Local cache\n------------------------------------------------------------",
|
|
172
|
+
);
|
|
173
|
+
if (action === "summary") {
|
|
174
|
+
const r = await operate({ action });
|
|
175
|
+
write(
|
|
176
|
+
`\n> Saved locally\n Text ${r.resultRecords || 0} · ${size(r.resultBytes || 0)}\n Files ${r.records} · ${size(r.bytes)}\n\n> Attachment storage\n ${size(r.bytes)} / ${size(r.limits.totalBytes)}${r.overLimit ? " · LIMIT EXCEEDED" : ""}`,
|
|
177
|
+
);
|
|
178
|
+
if (r.reservedBytes)
|
|
179
|
+
write(` Transfers in progress: ${size(r.reservedBytes)}`);
|
|
180
|
+
if (r.unknownBytes)
|
|
181
|
+
write(
|
|
182
|
+
`[!] ${size(r.unknownBytes)} needs inspection; cleanup will keep it.`,
|
|
183
|
+
);
|
|
184
|
+
write("\n> Next\n agentpay cache list");
|
|
185
|
+
} else if (action === "list") {
|
|
186
|
+
const page = Number(rest[0] || 1),
|
|
187
|
+
r = await operate({
|
|
188
|
+
action: "list",
|
|
189
|
+
offset: (page - 1) * 20,
|
|
190
|
+
limit: 20,
|
|
191
|
+
...filters,
|
|
192
|
+
...(rest[1] ? { purpose: rest[1] } : {}),
|
|
193
|
+
});
|
|
194
|
+
write(
|
|
195
|
+
`\n${r.total} entries · page ${page}\nDetails: agentpay cache details <Cache ID>`,
|
|
196
|
+
);
|
|
197
|
+
if (!r.items.length)
|
|
198
|
+
write("No saved results or attachments match this view.");
|
|
199
|
+
for (const [index, row] of r.items.entries()) {
|
|
200
|
+
const label = safe(
|
|
201
|
+
row.skillName ||
|
|
202
|
+
row.providerId ||
|
|
203
|
+
(row.skillId ? "Service " + row.skillId : "Source not recorded"),
|
|
204
|
+
);
|
|
205
|
+
const identity = row.orderId || row.requestId || "";
|
|
206
|
+
write(
|
|
207
|
+
`\n────────────────────────────────────────────────────────────\n${(page - 1) * 20 + index + 1}. ${label}\n ${contentSize(row)}\n ${new Date(row.updatedAtMs).toLocaleString()}${row.kind === "result" ? "" : " · " + safe(row.state)}${row.kind === "result" ? "" : "\n " + safe(row.filename)}\n ${row.orderId ? "Order" : "Request"}: ${safe(identity)}\n Cache ID: ${row.id}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (page * 20 < r.total)
|
|
211
|
+
write(
|
|
212
|
+
`\nNext page: agentpay cache list ${page + 1}${rest[1] ? " " + rest[1] : ""}${Object.entries(
|
|
213
|
+
filters,
|
|
214
|
+
)
|
|
215
|
+
.map(
|
|
216
|
+
([k, v]) =>
|
|
217
|
+
" --" +
|
|
218
|
+
{ state: "state", requestId: "request", orderId: "order" }[k] +
|
|
219
|
+
" " +
|
|
220
|
+
v,
|
|
221
|
+
)
|
|
222
|
+
.join("")}`,
|
|
223
|
+
);
|
|
224
|
+
} else if (action === "details") {
|
|
225
|
+
const r = await operate({ action, id: rest[0] });
|
|
226
|
+
write(
|
|
227
|
+
`${safe(r.filename)}\n ${contentSize(r)} · ${r.state}\n ${r.requestId ? "Request" : "Order"}: ${safe(r.requestId || r.orderId)}\n File key: ${safe(r.fileKey)}\n SHA-256: ${r.sha256 || "Not available"}\n Updated: ${new Date(r.updatedAtMs).toLocaleString()}`,
|
|
228
|
+
);
|
|
229
|
+
if (r.protected)
|
|
230
|
+
write(
|
|
231
|
+
r.kind === "result"
|
|
232
|
+
? "Saved result retained for recovery. Cleanup will not remove it."
|
|
233
|
+
: "Kept for original-request recovery. Cleanup will not remove this input.",
|
|
234
|
+
);
|
|
235
|
+
if (r.error) write("Issue: " + safe(r.error));
|
|
236
|
+
if (r.state === "available")
|
|
237
|
+
write(
|
|
238
|
+
`Export a copy: agentpay cache export ${r.id} /your/destination/filename`,
|
|
239
|
+
);
|
|
240
|
+
} else if (action === "export") {
|
|
241
|
+
const row = await operate({ action: "verified", id: rest[0] });
|
|
242
|
+
write("Exported: " + safe(await exportCacheFile(row, rest[1])));
|
|
243
|
+
} else if (action === "settings") {
|
|
244
|
+
if (rest.length && !/^[0-9]+$/.test(rest[1]))
|
|
245
|
+
throw Error("Use a positive integer value. Byte limits are in bytes.");
|
|
246
|
+
const r = await operate({
|
|
247
|
+
action,
|
|
248
|
+
...(rest.length ? { overrides: { [rest[0]]: Number(rest[1]) } } : {}),
|
|
249
|
+
});
|
|
250
|
+
for (const [k, v] of Object.entries(r))
|
|
251
|
+
write(
|
|
252
|
+
` ${k}: ${k.endsWith("Bytes") ? size(v) + " (" + v + " bytes)" : v}`,
|
|
253
|
+
);
|
|
254
|
+
write(
|
|
255
|
+
rest.length
|
|
256
|
+
? "Limits saved and applied. Existing copies are kept."
|
|
257
|
+
: "Change a limit: agentpay cache settings totalBytes 1073741824\nfilesPerResult applies per input request or output Order.",
|
|
258
|
+
);
|
|
259
|
+
} else if (action === "clean") {
|
|
260
|
+
const items = await operate({ action: "preview" });
|
|
261
|
+
if (!items.length) {
|
|
262
|
+
write(
|
|
263
|
+
"No eligible file copies to clean. Protected inputs and history are kept.",
|
|
264
|
+
);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
write(
|
|
268
|
+
`${items.length} file copies · ${size(items.reduce((n, r) => n + r.bytes, 0))}\n` +
|
|
269
|
+
items
|
|
270
|
+
.map((r) => " " + safe(r.filename) + " · " + size(r.bytes))
|
|
271
|
+
.join("\n"),
|
|
272
|
+
);
|
|
273
|
+
if (
|
|
274
|
+
(
|
|
275
|
+
await ask(
|
|
276
|
+
"These local copies will be deleted. Original download links may no longer work.\nPayments, Order history and protected inputs are unchanged.\nType confirm to clean, or q to return.",
|
|
277
|
+
)
|
|
278
|
+
).trim() !== "confirm"
|
|
279
|
+
) {
|
|
280
|
+
write("Cleanup cancelled.");
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
const r = await operate({
|
|
284
|
+
action: "clean",
|
|
285
|
+
items: items.map(({ id, generation }) => ({ id, generation })),
|
|
286
|
+
confirmed: true,
|
|
287
|
+
});
|
|
288
|
+
write(
|
|
289
|
+
`${r.removed} local copies removed. History kept. Run clean again to review any remaining copies.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
write("Cache operation stopped: " + safe(error.message));
|
|
295
|
+
write(
|
|
296
|
+
offline
|
|
297
|
+
? "If another Runtime owns this cache, use the command without --offline. Do not delete its lock file."
|
|
298
|
+
: "Check agentpay status. When Runtime is stopped, add --offline. No automatic offline fallback or payment was made.",
|
|
299
|
+
);
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { fileError } from "./file-download.mjs";
|
|
5
|
+
// Shared by the authenticated local route and the explicit offline CLI. No
|
|
6
|
+
// import/download/financial operation is exposed through this management surface.
|
|
7
|
+
export async function manageCache(cache, body) {
|
|
8
|
+
const fields = {
|
|
9
|
+
summary: [],
|
|
10
|
+
list: ["purpose", "state", "orderId", "requestId", "offset", "limit"],
|
|
11
|
+
details: ["id"],
|
|
12
|
+
verified: ["id"],
|
|
13
|
+
preview: [],
|
|
14
|
+
clean: ["items", "confirmed"],
|
|
15
|
+
settings: ["overrides"],
|
|
16
|
+
};
|
|
17
|
+
if (
|
|
18
|
+
!body ||
|
|
19
|
+
typeof body !== "object" ||
|
|
20
|
+
Array.isArray(body) ||
|
|
21
|
+
!Object.hasOwn(fields, body.action) ||
|
|
22
|
+
Object.keys(body).some(
|
|
23
|
+
(k) => k !== "action" && !fields[body.action].includes(k),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
throw fileError(
|
|
27
|
+
"FILE_INPUT_INVALID",
|
|
28
|
+
"Choose a supported cache operation.",
|
|
29
|
+
);
|
|
30
|
+
const { action, ...args } = body;
|
|
31
|
+
switch (action) {
|
|
32
|
+
case "summary":
|
|
33
|
+
return cache.summary();
|
|
34
|
+
case "list":
|
|
35
|
+
return cache.listAll(args);
|
|
36
|
+
case "details":
|
|
37
|
+
return cache.details(args.id);
|
|
38
|
+
case "verified":
|
|
39
|
+
return cache.verified(args.id);
|
|
40
|
+
case "preview":
|
|
41
|
+
return cache.cleanPreview();
|
|
42
|
+
case "clean":
|
|
43
|
+
return cache.clean(args);
|
|
44
|
+
case "settings":
|
|
45
|
+
return cache.settings(args.overrides);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { constants } from "node:fs";
|
|
5
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
|
+
import { chmod, mkdir, open } from "node:fs/promises";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
|
|
9
|
+
const TOKEN_PATTERN = /^[0-9a-f]{64}$/;
|
|
10
|
+
|
|
11
|
+
export async function readCallerToken(path) {
|
|
12
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
13
|
+
let token;
|
|
14
|
+
try {
|
|
15
|
+
const info = await handle.stat();
|
|
16
|
+
if (
|
|
17
|
+
!info.isFile() ||
|
|
18
|
+
info.uid !== process.getuid() ||
|
|
19
|
+
info.mode & 0o077 ||
|
|
20
|
+
info.size > 1024
|
|
21
|
+
)
|
|
22
|
+
throw Error(
|
|
23
|
+
"User Runtime caller credential must be a private regular file",
|
|
24
|
+
);
|
|
25
|
+
token = (await handle.readFile("utf8")).trim();
|
|
26
|
+
} finally {
|
|
27
|
+
await handle.close();
|
|
28
|
+
}
|
|
29
|
+
if (!TOKEN_PATTERN.test(token))
|
|
30
|
+
throw new Error("User Runtime caller credential file is invalid");
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function authorizeCaller(header, expectedToken) {
|
|
35
|
+
const prefix = "Bearer ";
|
|
36
|
+
const value =
|
|
37
|
+
typeof header === "string" && header.startsWith(prefix)
|
|
38
|
+
? header.slice(prefix.length)
|
|
39
|
+
: "";
|
|
40
|
+
const actual = Buffer.from(value, "utf8");
|
|
41
|
+
const expected = Buffer.from(expectedToken, "utf8");
|
|
42
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function initializeCallerCredential(path) {
|
|
46
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
47
|
+
let handle;
|
|
48
|
+
try {
|
|
49
|
+
handle = await open(path, "wx", 0o600);
|
|
50
|
+
await handle.writeFile(`${randomBytes(32).toString("hex")}\n`, "utf8");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error?.code !== "EEXIST") throw error;
|
|
53
|
+
} finally {
|
|
54
|
+
await handle?.close();
|
|
55
|
+
}
|
|
56
|
+
await chmod(path, 0o600);
|
|
57
|
+
return readCallerToken(path);
|
|
58
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { open } from "node:fs/promises";
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
const queues = new Map();
|
|
7
|
+
// Same portable OS lock primitive as the file store, but scoped to a client call.
|
|
8
|
+
// No PID files or stale-lock deletion: process exit releases the descriptor lock.
|
|
9
|
+
export async function withClientRequestLock(path, operation) {
|
|
10
|
+
const previous = queues.get(path) || Promise.resolve();
|
|
11
|
+
let release;
|
|
12
|
+
const current = new Promise((r) => (release = r));
|
|
13
|
+
queues.set(path, current);
|
|
14
|
+
await previous;
|
|
15
|
+
let handle;
|
|
16
|
+
try {
|
|
17
|
+
const { tryLock } = await import("fs-native-extensions");
|
|
18
|
+
handle = await open(
|
|
19
|
+
path,
|
|
20
|
+
constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW,
|
|
21
|
+
0o600,
|
|
22
|
+
);
|
|
23
|
+
const s = await handle.stat();
|
|
24
|
+
if (
|
|
25
|
+
!s.isFile() ||
|
|
26
|
+
s.nlink !== 1 ||
|
|
27
|
+
s.uid !== process.getuid() ||
|
|
28
|
+
s.mode & 0o077
|
|
29
|
+
)
|
|
30
|
+
throw Error("Unsafe request lock");
|
|
31
|
+
if (!tryLock(handle.fd))
|
|
32
|
+
throw Object.assign(
|
|
33
|
+
Error(
|
|
34
|
+
"This original request is already running. Retry the same request shortly.",
|
|
35
|
+
),
|
|
36
|
+
{ code: "REQUEST_BUSY" },
|
|
37
|
+
);
|
|
38
|
+
return await operation();
|
|
39
|
+
} finally {
|
|
40
|
+
await handle?.close();
|
|
41
|
+
release();
|
|
42
|
+
if (queues.get(path) === current) queues.delete(path);
|
|
43
|
+
}
|
|
44
|
+
}
|