@c4roa0/agentpay-user 0.3.7 → 0.3.8
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/README.md +3 -1
- package/doc/README.md +10 -0
- package/doc/TODO.md +38 -3
- package/doc/plans/local-instances-and-install-recovery.md +250 -0
- package/doc/records/client-isolated-acceptance-2026-11-11.md +37 -0
- package/doc/records/dual-install-systemd-2026-11-12.md +40 -0
- package/doc/records/historical-request-routing-2026-11-11.md +47 -0
- package/doc/records/installation-maintenance-2026-11-11.md +46 -0
- package/doc/reference/local-update.md +2 -1
- package/doc/runbooks/commands.md +28 -0
- package/doc/runbooks/local-update.md +11 -5
- package/package.json +1 -1
- package/progress.md +93 -6
- package/scripts/autostart.mjs +51 -25
- package/scripts/install-user.mjs +10 -3
- package/scripts/local-service.mjs +1 -1
- package/scripts/user-setup.mjs +4 -0
- package/src/default-request-owner.mjs +163 -0
- package/src/index.mjs +9 -0
- package/src/install-and-setup.mjs +6 -1
- package/src/install-selection.mjs +41 -0
- package/src/install-user.mjs +22 -22
- package/src/installation-binding.mjs +62 -0
- package/src/installation-operation.mjs +114 -0
- package/src/installation-paths.mjs +50 -1
- package/src/invocation-store.mjs +1 -1
- package/src/local-data-directory.mjs +17 -17
- package/src/local-update.mjs +41 -10
- package/src/local-wallet-ipc.mjs +2 -2
- package/src/npm-update.mjs +55 -19
- package/src/platform-endpoints.mjs +5 -8
- package/src/runtime-client.mjs +21 -4
- package/src/runtime-connection-settings.mjs +2 -1
- package/src/runtime-owner.mjs +22 -0
- package/src/runtime-selection-ready.mjs +29 -0
- package/src/service-readiness.mjs +29 -0
- package/src/service-selection.mjs +189 -0
- package/src/setup-autostart.mjs +12 -2
- package/src/setup-service-selection.mjs +25 -0
- package/src/skill-client.mjs +8 -3
- package/src/uninstall-local.mjs +69 -15
- package/src/user-cli.mjs +1 -2
- package/update-pair.json +1 -1
package/src/skill-client.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {withDefaultRequest} from "./default-request-owner.mjs";
|
|
1
2
|
// Generic Skill CLI glue. Business input/output validation is supplied by the Skill;
|
|
2
3
|
// payment authority and request recovery remain in the installed client/Runtime.
|
|
3
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -7,6 +8,7 @@ export const skillClientCapabilities = Object.freeze([
|
|
|
7
8
|
"input-snapshots-v1",
|
|
8
9
|
"output-files-v1",
|
|
9
10
|
"original-request-v1",
|
|
11
|
+
"default-selection-v1",
|
|
10
12
|
]);
|
|
11
13
|
import {
|
|
12
14
|
readServiceOffer,
|
|
@@ -115,7 +117,7 @@ export async function runSkill({
|
|
|
115
117
|
"Check the business input against this Skill’s instructions.",
|
|
116
118
|
);
|
|
117
119
|
}
|
|
118
|
-
|
|
120
|
+
const select = (knownRequestIds) => selectInvocation(
|
|
119
121
|
connection,
|
|
120
122
|
reference,
|
|
121
123
|
{
|
|
@@ -123,8 +125,11 @@ export async function runSkill({
|
|
|
123
125
|
requestedMaxUsageUnits: units,
|
|
124
126
|
...(acceptedTerms === undefined ? {} : { acceptedTerms }),
|
|
125
127
|
},
|
|
126
|
-
{ force, ...(requestId === undefined ? {} : { requestId }) },
|
|
128
|
+
{ force, knownRequestIds, ...(requestId === undefined ? {} : { requestId }) },
|
|
127
129
|
);
|
|
130
|
+
checkpoint = env.AGENTPAY_DEFAULT_SELECTION === '1'
|
|
131
|
+
? await withDefaultRequest({service:reference,input:{request},dataDir:connection.dataDir,force},select)
|
|
132
|
+
: await select();
|
|
128
133
|
} else
|
|
129
134
|
throw new RuntimeClientError(
|
|
130
135
|
"INVALID_ARGUMENTS",
|
|
@@ -181,7 +186,7 @@ export async function runSkill({
|
|
|
181
186
|
} catch (error) {
|
|
182
187
|
const known =
|
|
183
188
|
error instanceof RuntimeClientError ||
|
|
184
|
-
["SERVICE_REFERENCE_INVALID"].includes(error.code);
|
|
189
|
+
["SERVICE_REFERENCE_INVALID", "ORIGINAL_INSTALLATION_REQUIRED", "ORIGINAL_REQUEST_AMBIGUOUS", "REQUEST_HISTORY_REVIEW_REQUIRED"].includes(error.code);
|
|
185
190
|
write({
|
|
186
191
|
ok: false,
|
|
187
192
|
...(requestId ? { requestId } : {}),
|
package/src/uninstall-local.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { installedServiceSelection, withServiceLifecycleLock } from "./service-selection.mjs";
|
|
2
|
+
import { installationBindingPath } from "./installation-binding.mjs";
|
|
1
3
|
import { installationDirectory, legacyInstallationDirectory } from "./installation-paths.mjs";
|
|
2
4
|
import { walletPackage } from "./wallet-package.mjs";
|
|
3
5
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -6,8 +8,7 @@ import { walletPackage } from "./wallet-package.mjs";
|
|
|
6
8
|
import { lstat, realpath, readFile, rm, unlink } from "node:fs/promises";
|
|
7
9
|
import { dirname, join, relative, resolve, parse, isAbsolute } from "node:path";
|
|
8
10
|
import { homedir } from "node:os";
|
|
9
|
-
import {
|
|
10
|
-
import { promisify } from "node:util";
|
|
11
|
+
import {runInstallationCommand, installationLockMarker} from "./installation-operation.mjs";
|
|
11
12
|
import { createInterface } from "node:readline/promises";
|
|
12
13
|
import { autostartFiles, isManagedUnit } from "./autostart-files.mjs";
|
|
13
14
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
installedUserDir,
|
|
16
17
|
} from "./local-data-directory.mjs";
|
|
17
18
|
import { readServiceSettings, listenerPresent } from "./local-services.mjs";
|
|
18
|
-
const execute =
|
|
19
|
+
const execute = runInstallationCommand;
|
|
19
20
|
const inside = (parent, path) => {
|
|
20
21
|
const r = relative(parent, path);
|
|
21
22
|
return r === "" || (!r.startsWith("..") && !parse(r).root);
|
|
@@ -43,9 +44,13 @@ export async function uninstallPlan({
|
|
|
43
44
|
home = homedir(),
|
|
44
45
|
node = process.execPath,
|
|
45
46
|
removeData = false,
|
|
47
|
+
underLock = false,
|
|
46
48
|
}) {
|
|
47
49
|
dataDir = resolve(dataDir);
|
|
48
|
-
userDir = resolve(userDir)
|
|
50
|
+
userDir = await realpath(resolve(userDir)).catch(error => {
|
|
51
|
+
if (error.code === "ENOENT") return resolve(userDir);
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
49
54
|
home = resolve(home);
|
|
50
55
|
let walletDir;
|
|
51
56
|
try { walletDir = walletPackage(userDir).directory; } catch { walletDir = userDir; }
|
|
@@ -84,10 +89,15 @@ export async function uninstallPlan({
|
|
|
84
89
|
if (e.code === "ENOENT") return null;
|
|
85
90
|
throw e;
|
|
86
91
|
});
|
|
87
|
-
if (lock)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
92
|
+
if (lock && !underLock) {
|
|
93
|
+
// A v2 marker can remain after interruption; the operation lock, acquired
|
|
94
|
+
// after confirmation, decides whether its writers have actually exited.
|
|
95
|
+
const path = join(installDir, "install.lock");
|
|
96
|
+
await target(path, {privateMode:true});
|
|
97
|
+
if (lock.size !== Buffer.byteLength(installationLockMarker) ||
|
|
98
|
+
await readFile(path, "utf8") !== installationLockMarker)
|
|
99
|
+
throw Error("An older installer lock exists. Keep it for review before uninstalling.");
|
|
100
|
+
}
|
|
91
101
|
const install = await target(installDir, {
|
|
92
102
|
directory: true,
|
|
93
103
|
privateMode: true,
|
|
@@ -101,16 +111,24 @@ export async function uninstallPlan({
|
|
|
101
111
|
if (
|
|
102
112
|
saved.version !== 1 ||
|
|
103
113
|
typeof saved.dataDir !== "string" ||
|
|
104
|
-
!isAbsolute(saved.dataDir)
|
|
105
|
-
resolve(saved.dataDir) !== dataDir
|
|
114
|
+
!isAbsolute(saved.dataDir)
|
|
106
115
|
)
|
|
107
116
|
throw Error(
|
|
108
117
|
"Installation record points to another data directory; no files were removed.",
|
|
109
118
|
);
|
|
110
|
-
targets.push({ ...
|
|
119
|
+
if (resolve(saved.dataDir) === dataDir) targets.push({ ...state, checkpoint: true });
|
|
111
120
|
|
|
112
121
|
}
|
|
113
122
|
}
|
|
123
|
+
if (removeData) {
|
|
124
|
+
const path = await installationBindingPath(userDir).catch(e => {if(e.code==='ENOENT')return null;throw e;});
|
|
125
|
+
const binding = path && await target(path, {privateMode:true});
|
|
126
|
+
if (binding) {
|
|
127
|
+
const saved = JSON.parse(await readFile(binding.path,'utf8'));
|
|
128
|
+
if (saved.dataDir !== dataDir) throw Error('Binding changed; no data was removed.');
|
|
129
|
+
targets.push({...binding,checkpoint:true});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
114
132
|
// Retire only the old source checkpoint that names this exact data directory.
|
|
115
133
|
// Otherwise a later setup could rediscover and reimport purged configuration.
|
|
116
134
|
if (removeData) {
|
|
@@ -135,7 +153,14 @@ export async function uninstallPlan({
|
|
|
135
153
|
const unitDir = join(home, ".config/systemd/user"),
|
|
136
154
|
generated = autostartFiles({ node, userDir, dataDir }),
|
|
137
155
|
units = [];
|
|
138
|
-
|
|
156
|
+
// A valid pair owned by a different installation is not an uninstall target.
|
|
157
|
+
const owner = await installedServiceSelection(unitDir, undefined, {allowPartial:true}).catch(error => {
|
|
158
|
+
throw Error('Service files are not a consistent managed file set: ' + error.message);
|
|
159
|
+
});
|
|
160
|
+
const foreign = owner && owner.userDir !== userDir;
|
|
161
|
+
if (foreign && owner.dataDir === dataDir && removeData)
|
|
162
|
+
throw Error('Another installation uses this data folder. Its data cannot be deleted here.');
|
|
163
|
+
for (const [name, body] of Object.entries(foreign ? {} : generated)) {
|
|
139
164
|
const file = await target(join(unitDir, name));
|
|
140
165
|
if (!file) continue;
|
|
141
166
|
for (const path of [
|
|
@@ -158,14 +183,14 @@ export async function uninstallPlan({
|
|
|
158
183
|
);
|
|
159
184
|
units.push({ ...file, name });
|
|
160
185
|
}
|
|
161
|
-
return { dataDir, targets, units, settings, removeData };
|
|
186
|
+
return { dataDir, targets, units, settings, removeData, scope: {dataDir,userDir,home,node,removeData} };
|
|
162
187
|
}
|
|
163
188
|
async function unchanged(item) {
|
|
164
189
|
const fresh = await target(item.path, item);
|
|
165
190
|
if (!fresh || fresh.dev !== item.dev || fresh.ino !== item.ino)
|
|
166
191
|
throw Error("An uninstall target changed. No further files were removed.");
|
|
167
192
|
}
|
|
168
|
-
|
|
193
|
+
async function removeConfirmedInstallation(
|
|
169
194
|
plan,
|
|
170
195
|
{
|
|
171
196
|
manage = async (args) =>
|
|
@@ -232,6 +257,34 @@ export async function removeInstallation(
|
|
|
232
257
|
await rm(item.path, { recursive: item.directory, force: false });
|
|
233
258
|
}
|
|
234
259
|
}
|
|
260
|
+
// The preview is not authority: a service switch can happen while the user reads
|
|
261
|
+
// the confirmation. Rebuild it under the same lock used by start/stop/switch.
|
|
262
|
+
export async function removeInstallation(plan, options = {}) {
|
|
263
|
+
return withServiceLifecycleLock(async () => {
|
|
264
|
+
const current = await uninstallPlan({...plan.scope, underLock:true});
|
|
265
|
+
const signature = value => JSON.stringify({
|
|
266
|
+
targets: value.targets, units: value.units, settings: value.settings,
|
|
267
|
+
});
|
|
268
|
+
if (signature(current) !== signature(plan))
|
|
269
|
+
throw Error('Installation or services changed since confirmation. Run uninstall again to review the current scope.');
|
|
270
|
+
const selectionPath = join(installationDirectory(), 'active-services.json');
|
|
271
|
+
let selection;
|
|
272
|
+
try {
|
|
273
|
+
await target(selectionPath, {privateMode:true});
|
|
274
|
+
selection = JSON.parse(await readFile(selectionPath, 'utf8'));
|
|
275
|
+
if (['preparing','stopping'].includes(selection.phase))
|
|
276
|
+
throw Error('A service switch is unfinished. Complete it before uninstalling.');
|
|
277
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
278
|
+
await removeConfirmedInstallation(current, options);
|
|
279
|
+
if (selection?.target?.userDir === plan.scope.userDir &&
|
|
280
|
+
selection?.target?.dataDir === plan.dataDir) {
|
|
281
|
+
// Keep an unavailable selection, rather than falling back to another Wallet.
|
|
282
|
+
const {atomicWritePrivate} = await import('./private-journal-files.mjs');
|
|
283
|
+
await atomicWritePrivate(selectionPath, JSON.stringify({...selection, phase:'stopped'}) + '\n');
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
235
288
|
export async function uninstallLocal({
|
|
236
289
|
ask,
|
|
237
290
|
write = console.log,
|
|
@@ -256,7 +309,7 @@ export async function uninstallLocal({
|
|
|
256
309
|
write(
|
|
257
310
|
"It does not revoke Safe permissions or cancel on-chain Orders. Resolve pending Orders and revoke the Safe permission first if you intend to retire it.",
|
|
258
311
|
);
|
|
259
|
-
write("
|
|
312
|
+
write(selected.units.length ? "Selected services will stop. Removal is limited to:" : "No managed services will be stopped. Removal is limited to:");
|
|
260
313
|
for (const item of [...selected.units, ...selected.targets])
|
|
261
314
|
write(` ${item.path}`);
|
|
262
315
|
const confirmation = removeData ? "DELETE LOCAL AGENTPAY DATA" : "REMOVE AGENTPAY SERVICES";
|
|
@@ -293,6 +346,7 @@ export async function uninstallLocal({
|
|
|
293
346
|
write(
|
|
294
347
|
`Selected services and data removal complete. On-chain permissions remain unchanged.
|
|
295
348
|
To remove a global program installation: npm uninstall --global @c4roa0/agentpay-user
|
|
349
|
+
For a folder-local npm installation, run npm uninstall @c4roa0/agentpay-user from its installation folder.
|
|
296
350
|
For a source checkout, you may now delete the User program folder.`,
|
|
297
351
|
);
|
|
298
352
|
return 0;
|
package/src/user-cli.mjs
CHANGED
|
@@ -289,8 +289,7 @@ export async function runUserCli(
|
|
|
289
289
|
}
|
|
290
290
|
if (command === "service") {
|
|
291
291
|
if (["start", "restart"].includes(action) && !values.length) {
|
|
292
|
-
|
|
293
|
-
return prepared || run([script("autostart"), action]);
|
|
292
|
+
return run([script("autostart"), action]);
|
|
294
293
|
}
|
|
295
294
|
if (action === "stop" && !values.length)
|
|
296
295
|
return run([script("autostart"), "stop"]);
|