@c4roa0/agentpay-user 0.3.8 → 0.3.10
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/doc/TODO.md +11 -2
- package/doc/usage.md +8 -0
- package/package.json +1 -1
- package/progress.md +35 -0
- package/src/installation-operation.mjs +2 -2
- package/src/npm-update.mjs +11 -4
- package/src/official-test-update.mjs +13 -0
- package/src/service-selection.mjs +22 -2
- package/src/skill-bundle.mjs +38 -0
- package/update-pair.json +1 -1
package/doc/TODO.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# User TODO
|
|
2
2
|
|
|
3
|
+
- [x] Fix switching back from setup cancelled before Wallet creation; retain stop/listener and established-data checks.
|
|
4
|
+
- [ ] Deliver the fix and verify participant switch-back to the original installation.
|
|
5
|
+
- [ ] Improve unconfigured installation status/info: direct to setup, not service start or missing-key recovery.
|
|
6
|
+
|
|
7
|
+
- [x] Verify local official Skill files before downloading and stream update repair progress (2026-11-12; targeted checks).
|
|
8
|
+
- [x] Publish local official-Skill verification and streaming update progress in User 0.3.9; tag and npm integrity verified (2026-11-12).
|
|
9
|
+
|
|
10
|
+
- [x] Publish User 0.3.8 with matching v0.3.8 tag; full CI, standalone package and public registry integrity passed (2026-11-12).
|
|
11
|
+
|
|
3
12
|
- [x] Recover failed service start/stop without blocking repair; incomplete target unit replacement remains protected (2026-11-12, controlled service-manager verification).
|
|
4
13
|
|
|
5
14
|
- [x] Fix shared-data switch client relocation and retry without a repair deadlock; isolated selection/connection checks pass (2026-11-11).
|
|
@@ -18,13 +27,13 @@
|
|
|
18
27
|
- [x] Implement and check storage-choice module (reuse/new/cancel); public installer selection now connects through a service-switch gate (2026-11-11).
|
|
19
28
|
- [x] Connect installer selection and service-start switching with confirmation, stopped-state/listener checks and Runtime lifetime exclusion; focused checks passed (2026-11-11; systemd results simulated).
|
|
20
29
|
- [x] Correct shared-directory purge, mixed-unit retry, manual setup regression, first-start preparation and startup-preference inheritance; 37 targeted checks pass (2026-11-11).
|
|
21
|
-
- [ ]
|
|
30
|
+
- [ ] Complete remaining switch-crash recovery and desktop login acceptance; real A/B/A systemd switching passed; preserve historical request scope across switches. Inactive maintenance isolation is implemented with scoped verification.
|
|
22
31
|
|
|
23
32
|
|
|
24
33
|
|
|
25
34
|
- [x] Isolated public npm fresh install, registry failure/retry and disposable-data uninstall (2026-11-11; no real systemd or financial acceptance).
|
|
26
35
|
- [x] Replace existence-only locking for new operations: inherited native lock survives caller death while writers live, then permits retry. Old unknown locks require review; they are not automatically removed.
|
|
27
|
-
- [
|
|
36
|
+
- [x] Publish reviewed folder-local npm uninstall completion guidance in User 0.3.8.
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
- [x] Participant directory-local update to User 0.3.6 / Wallet 0.3.3: running versions, original Orders in CLI/Market and available result cache verified (2026-11-11).
|
package/doc/usage.md
CHANGED
|
@@ -52,3 +52,11 @@ to that business example. An interrupted test retains its original request.
|
|
|
52
52
|
[Background service guide](runbooks/local-autostart.md) explains Linux user services,
|
|
53
53
|
login reminders and manual startup. A local lock or process stop does not revoke
|
|
54
54
|
permission; follow [emergency stop](runbooks/emergency-payment-stop.md) when needed.
|
|
55
|
+
|
|
56
|
+
### Official test Skill checks during updates
|
|
57
|
+
|
|
58
|
+
Update and repair verify the official test ZIP against the version pinned by User,
|
|
59
|
+
then compare its installed files. Matching local files need no download. Missing
|
|
60
|
+
archives trigger a download; changed or incomplete extracted files are kept and
|
|
61
|
+
reported for review. These checks do not start a payment or select a newer Skill
|
|
62
|
+
from Market. Software updates display repair progress while it runs.
|
package/package.json
CHANGED
package/progress.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# AgentPay User progress
|
|
2
2
|
|
|
3
|
+
## Participant incomplete-setup switch — 2026-11-12
|
|
4
|
+
|
|
5
|
+
Participant A-to-B setup cancellation exposed a switch-back failure: B had no
|
|
6
|
+
Wallet directory, but selection required complete service settings. User 0.3.10 candidate
|
|
7
|
+
allows demonstrably unconfigured B to release selection after confirmed systemd
|
|
8
|
+
stop and socket checks. Existing key/Policy/connection or invalid directories still
|
|
9
|
+
block missing-configuration bypass. Original participant A remains stopped until
|
|
10
|
+
the corrected switch is run. Full two-running-installation acceptance is pending.
|
|
11
|
+
|
|
12
|
+
## Official Skill local verification — 2026-11-12
|
|
13
|
+
|
|
14
|
+
Published as `v0.3.9` at `8fbae4e`. Full GitHub behavior and standalone package
|
|
15
|
+
checks passed; npm latest and archive integrity independently verified. Wallet stays
|
|
16
|
+
0.3.3, Protocol 0.14.3 and official test Skill 1.2.1.
|
|
17
|
+
[Release workflow](https://github.com/c4rO-0/agentpay-user/actions/runs/43162494802).
|
|
18
|
+
|
|
19
|
+
User 0.3.9: update/repair verifies the release-pinned local ZIP and its extracted
|
|
20
|
+
files before using the network. Matching files skip download; edited or incomplete
|
|
21
|
+
extracted files are preserved with guidance. Software-update repair progress streams
|
|
22
|
+
as it happens. Targeted update/package regressions passed; the actual pinned 1.2.1
|
|
23
|
+
archive was installed in temporary storage and verified with network access forbidden.
|
|
24
|
+
Child output streaming was checked with a real process. No live services or payments changed.
|
|
25
|
+
|
|
26
|
+
## User 0.3.8 published — 2026-11-12
|
|
27
|
+
|
|
28
|
+
User `0.3.8` and tag `v0.3.8` identify release commit `727e547`.
|
|
29
|
+
[Publication workflow](https://github.com/c4rO-0/agentpay-user/actions/runs/43148236738)
|
|
30
|
+
passed the complete behavior suite, standalone package installation and publication.
|
|
31
|
+
The public npm `latest` version and archive integrity were independently verified.
|
|
32
|
+
Dependencies remain Wallet 0.3.3 and Protocol 0.14.3; official test Skill remains 1.2.1.
|
|
33
|
+
The release includes multiple installations with one active Runtime/Wallet pair.
|
|
34
|
+
Real A/B/A switching passed; desktop login/reboot, full guided installation and
|
|
35
|
+
complete interrupted-switch acceptance remain separate pending checks.
|
|
36
|
+
Agent's candidate template is not published by this release.
|
|
37
|
+
|
|
3
38
|
## Real two-installation switch passed — 2026-11-12
|
|
4
39
|
|
|
5
40
|
Actual user-systemd switching A → B → A passed using source and folder-local npm
|
|
@@ -91,7 +91,7 @@ export function installationChildOptions({env = process.env, stdio = ['ignore','
|
|
|
91
91
|
|
|
92
92
|
// Wait for close even on timeout/error: returning on a timer while npm still writes
|
|
93
93
|
// would release the caller's lock too early. Writer children retain the descriptor.
|
|
94
|
-
export async function runInstallationCommand(file, args, {env = process.env, timeout = 120000, maxBuffer = 2 * 1024 * 1024, cwd} = {}) {
|
|
94
|
+
export async function runInstallationCommand(file, args, {env = process.env, timeout = 120000, maxBuffer = 2 * 1024 * 1024, cwd, onStdout} = {}) {
|
|
95
95
|
const {spawn} = await import('node:child_process');
|
|
96
96
|
return new Promise((resolve,reject) => {
|
|
97
97
|
const child = spawn(file,args,{cwd,...installationChildOptions({env})});
|
|
@@ -102,7 +102,7 @@ export async function runInstallationCommand(file, args, {env = process.env, tim
|
|
|
102
102
|
killTimer ||= setTimeout(()=>child.kill('SIGKILL'),1000);
|
|
103
103
|
};
|
|
104
104
|
const timer = setTimeout(()=>stop(Error('Command timed out.')),timeout);
|
|
105
|
-
child.stdout.on('data',chunk=>{if(failure)return;stdout+=chunk;if(Buffer.byteLength(stdout)>maxBuffer)stop(Error('Command output exceeded its limit.'));});
|
|
105
|
+
child.stdout.on('data',chunk=>{if(failure)return;stdout+=chunk;if(onStdout)onStdout(chunk.toString());if(Buffer.byteLength(stdout)>maxBuffer)stop(Error('Command output exceeded its limit.'));});
|
|
106
106
|
child.stderr.on('data',chunk=>{if(failure)return;stderr+=chunk;if(Buffer.byteLength(stderr)>maxBuffer)stop(Error('Command output exceeded its limit.'));});
|
|
107
107
|
child.once('error',error=>{failure=error;});
|
|
108
108
|
child.once('close',(code,signal)=>{
|
package/src/npm-update.mjs
CHANGED
|
@@ -21,12 +21,12 @@ function newer(next, current) {
|
|
|
21
21
|
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
|
-
async function command(file, args, { logPath, timeout = 120000, env = process.env, cwd = process.cwd() } = {}) {
|
|
24
|
+
async function command(file, args, { logPath, timeout = 120000, env = process.env, cwd = process.cwd(), onStdout } = {}) {
|
|
25
25
|
try {
|
|
26
|
-
const result = await execute(file, args, { timeout, cwd, maxBuffer: 2 * 1024 * 1024,
|
|
26
|
+
const result = await execute(file, args, { timeout, cwd, onStdout, maxBuffer: 2 * 1024 * 1024,
|
|
27
27
|
env: { ...env, PATH: dirname(process.execPath) + ':' + (env.PATH || '') } });
|
|
28
28
|
if (logPath) await appendFile(logPath, result.stdout + result.stderr, {mode:0o600});
|
|
29
|
-
return result.stdout.trim();
|
|
29
|
+
return onStdout ? '' : result.stdout.trim();
|
|
30
30
|
} catch (error) {
|
|
31
31
|
if (logPath) await appendFile(logPath, String(error.stdout || '') + String(error.stderr || ''), {mode:0o600});
|
|
32
32
|
throw Error(logPath ? 'The command did not complete. Check the update log.' : 'Could not query npm. Check your network and npm configuration, then retry.');
|
|
@@ -75,7 +75,14 @@ async function updateUnderLock({
|
|
|
75
75
|
ask = askConfirmation,
|
|
76
76
|
npm = (args, options) => command('npm', args, options),
|
|
77
77
|
service = (action, options) => command(process.execPath, [join(userDir,'scripts/autostart.mjs'), action], options),
|
|
78
|
-
repair = options =>
|
|
78
|
+
repair = options => {
|
|
79
|
+
let pending = '';
|
|
80
|
+
return command(process.execPath, [join(userDir,'scripts/repair.mjs'), '-f'], {...options, onStdout(chunk) {
|
|
81
|
+
pending += chunk;
|
|
82
|
+
const lines = pending.split('\n'); pending = lines.pop();
|
|
83
|
+
for (const line of lines) write(line);
|
|
84
|
+
}}).finally(() => { if (pending) write(pending); });
|
|
85
|
+
},
|
|
79
86
|
officialTest = prepareOfficialTestUpdate,
|
|
80
87
|
serviceScope = maintenanceServiceScope,
|
|
81
88
|
} = {}) {
|
|
@@ -70,6 +70,19 @@ export async function prepareOfficialTestUpdate({
|
|
|
70
70
|
} = {}) {
|
|
71
71
|
write("\n> Official test Skill");
|
|
72
72
|
try {
|
|
73
|
+
// Only the release-pinned archive can establish the expected installed files.
|
|
74
|
+
// Never trust installation.json or a Market-provided digest for this shortcut.
|
|
75
|
+
const { inspectSavedSkillBundle } = await import("./skill-bundle.mjs");
|
|
76
|
+
const local = await inspectSavedSkillBundle(dataDir, OFFICIAL_TEST_PACKAGE);
|
|
77
|
+
if (local.status === "ready") {
|
|
78
|
+
write(`Version ${OFFICIAL_TEST_PACKAGE.version} verified locally. No download needed.`);
|
|
79
|
+
return local;
|
|
80
|
+
}
|
|
81
|
+
if (local.status === "local_edits") {
|
|
82
|
+
write("Local test Skill files are changed or incomplete. Existing files were kept; review them before testing.");
|
|
83
|
+
return local;
|
|
84
|
+
}
|
|
85
|
+
write("Downloading the official test Skill…");
|
|
73
86
|
let source;
|
|
74
87
|
for (const name of ["anvil-onboarding.json", "user-deployment.json"]) {
|
|
75
88
|
const saved = await readJson(await deploymentFilePath(dataDir, userDir, name));
|
|
@@ -45,6 +45,26 @@ async function targetUnitsPrepared(unitDir, target) {
|
|
|
45
45
|
} catch { return false; }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// A cancelled first setup can own generated units before it has a Wallet or
|
|
49
|
+
// runtime address. Only that demonstrably unconfigured state may omit settings;
|
|
50
|
+
// missing settings in an established installation still require review.
|
|
51
|
+
async function previousServiceSettings(dataDir) {
|
|
52
|
+
try { return await readServiceSettings(dataDir); }
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error.code !== 'ENOENT') throw error;
|
|
55
|
+
for (const directory of [dataDir, join(dataDir,'wallet'), join(dataDir,'runtime')]) {
|
|
56
|
+
const info = await lstat(directory).catch(e => { if(e.code==='ENOENT' && directory!==dataDir)return null;throw e; });
|
|
57
|
+
if (info && (!info.isDirectory() || info.isSymbolicLink() || info.uid!==process.getuid() || info.mode & 0o077))
|
|
58
|
+
throw Error('Previous installation directories need ownership review before switching.');
|
|
59
|
+
}
|
|
60
|
+
for (const name of ['wallet/session.keystore.json','wallet/policy.json','runtime/local-services.json','runtime/connection.json']) {
|
|
61
|
+
const exists = await lstat(join(dataDir,name)).then(()=>true,e=>{if(e.code==='ENOENT')return false;throw e;});
|
|
62
|
+
if (exists) throw Error('Previous installation has saved Wallet or Runtime settings, but its service configuration is incomplete. Keep its files and repair it before switching.');
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
// The caller holds this lock through unit replacement and start/stop. No prompts
|
|
49
69
|
// can grant approval to a different target while the current transition writes.
|
|
50
70
|
export async function withServiceSelection({target, action, ask, write = console.log, ctl, unitDir, operation, verifyReady = waitForServiceReadiness}) {
|
|
@@ -104,9 +124,9 @@ export async function withServiceSelection({target, action, ask, write = console
|
|
|
104
124
|
}
|
|
105
125
|
}
|
|
106
126
|
if (different) {
|
|
107
|
-
const settings = await
|
|
127
|
+
const settings = await previousServiceSettings(previous.dataDir);
|
|
108
128
|
if (await listenerPresent(join(previous.dataDir,'wallet/wallet.sock')) ||
|
|
109
|
-
await listenerPresent({host:'127.0.0.1',port:Number(new URL(settings.runtimeUrl).port)}))
|
|
129
|
+
(settings && await listenerPresent({host:'127.0.0.1',port:Number(new URL(settings.runtimeUrl).port)})))
|
|
110
130
|
throw Error('An original Wallet or Runtime listener is still running. Stop it before switching; no new service was started.');
|
|
111
131
|
}
|
|
112
132
|
if (different) {
|
package/src/skill-bundle.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MPL-2.0
|
|
2
2
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
3
3
|
// If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { constants } from "node:fs";
|
|
4
5
|
import { currentTimeMs } from "./local-time.mjs";
|
|
5
6
|
import { readSkillZip } from "./skill-zip.mjs";
|
|
6
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
8
|
import {
|
|
9
|
+
open,
|
|
8
10
|
mkdir,
|
|
9
11
|
lstat,
|
|
10
12
|
readFile,
|
|
@@ -140,3 +142,39 @@ export async function installSkillBundle({
|
|
|
140
142
|
await rm(staging, { recursive: true, force: true });
|
|
141
143
|
}
|
|
142
144
|
}
|
|
145
|
+
|
|
146
|
+
// Read-only fast path for a release-pinned package. A matching ZIP alone is not
|
|
147
|
+
// enough: compare every installed file with its authenticated archive content.
|
|
148
|
+
export async function inspectSavedSkillBundle(dataDir, pin) {
|
|
149
|
+
const parent = join(dataDir, "runtime", "skill-bundles");
|
|
150
|
+
const directory = join(parent, `${pin.name}-${pin.version}-${pin.sha256}`);
|
|
151
|
+
async function readBounded(file, max) {
|
|
152
|
+
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
153
|
+
try {
|
|
154
|
+
const stat = await handle.stat();
|
|
155
|
+
if (!stat.isFile() || stat.size > max) throw Error("Invalid saved Skill file");
|
|
156
|
+
return await handle.readFile();
|
|
157
|
+
} finally { await handle.close(); }
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
for (const path of [dataDir, join(dataDir, "runtime"), parent, directory]) await privateDir(path);
|
|
161
|
+
const bytes = await readBounded(join(directory, "publisher.zip"), skillArchiveLimits.compressed);
|
|
162
|
+
if (bundleDigest(bytes) !== pin.sha256) return {status: "download_required"};
|
|
163
|
+
const parsed = parseSkillBundle(bytes);
|
|
164
|
+
if (parsed.manifest.packageName !== pin.name || parsed.manifest.packageVersion !== pin.version)
|
|
165
|
+
return {status: "download_required"};
|
|
166
|
+
try {
|
|
167
|
+
for (const [name, expected] of Object.entries(parsed.files)) {
|
|
168
|
+
// Refuse symlinked intermediate directories as well as symlinked files.
|
|
169
|
+
let ancestor = dirname(join(directory, name));
|
|
170
|
+
while (ancestor !== directory) {
|
|
171
|
+
await privateDir(ancestor);
|
|
172
|
+
ancestor = dirname(ancestor);
|
|
173
|
+
}
|
|
174
|
+
const actual = await readBounded(join(directory, name), skillArchiveLimits.file);
|
|
175
|
+
if (!actual.equals(expected)) return {status: "local_edits", directory};
|
|
176
|
+
}
|
|
177
|
+
} catch { return {status: "local_edits", directory}; }
|
|
178
|
+
return {status: "ready", directory};
|
|
179
|
+
} catch { return {status: "download_required"}; }
|
|
180
|
+
}
|