@c4roa0/agentpay-user 0.3.9 → 0.3.11
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 +9 -1
- package/doc/usage.md +12 -0
- package/package.json +1 -1
- package/progress.md +33 -1
- package/src/npm-update.mjs +21 -3
- package/src/service-selection.mjs +46 -7
- package/update-pair.json +1 -1
package/doc/TODO.md
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# User TODO
|
|
2
2
|
|
|
3
|
+
- [x] Allow update of an exact confirmed stopped switch target, then resume using new code; preserve retry checkpoint and reject other targets/live services (controlled verification).
|
|
4
|
+
- [ ] Publish and accept the update recovery path; document one-time bootstrap for older updaters already blocked before download.
|
|
5
|
+
|
|
6
|
+
- [x] Fix switching back from setup cancelled before Wallet creation; retain stop/listener and established-data checks.
|
|
7
|
+
- [x] Publish User 0.3.10 switch-back fix; matching tag and npm integrity verified.
|
|
8
|
+
- [ ] Verify participant switch-back to the original installation after installing 0.3.10.
|
|
9
|
+
- [ ] Improve unconfigured installation status/info: direct to setup, not service start or missing-key recovery.
|
|
10
|
+
|
|
3
11
|
- [x] Verify local official Skill files before downloading and stream update repair progress (2026-11-12; targeted checks).
|
|
4
|
-
- [
|
|
12
|
+
- [x] Publish local official-Skill verification and streaming update progress in User 0.3.9; tag and npm integrity verified (2026-11-12).
|
|
5
13
|
|
|
6
14
|
- [x] Publish User 0.3.8 with matching v0.3.8 tag; full CI, standalone package and public registry integrity passed (2026-11-12).
|
|
7
15
|
|
package/doc/usage.md
CHANGED
|
@@ -60,3 +60,15 @@ then compare its installed files. Matching local files need no download. Missing
|
|
|
60
60
|
archives trigger a download; changed or incomplete extracted files are kept and
|
|
61
61
|
reported for review. These checks do not start a payment or select a newer Skill
|
|
62
62
|
from Market. Software updates display repair progress while it runs.
|
|
63
|
+
|
|
64
|
+
### Updating after an interrupted installation switch
|
|
65
|
+
|
|
66
|
+
From the installation you previously selected, run `agentpay update`. If its
|
|
67
|
+
background services have stopped, the updater can install the new version and
|
|
68
|
+
continue that same switch. It preserves Wallet data and the confirmed target. If
|
|
69
|
+
recovery fails, run the same command again; do not delete the switch records.
|
|
70
|
+
|
|
71
|
+
User 0.3.9 and 0.3.10 stop before downloading when unit replacement is unfinished.
|
|
72
|
+
For those versions only, install the intended fixed User version with npm in the
|
|
73
|
+
same program folder, then run `agentpay service start` there. Do not uninstall or
|
|
74
|
+
create a replacement Wallet to recover an interrupted switch.
|
package/package.json
CHANGED
package/progress.md
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
# AgentPay User progress
|
|
2
2
|
|
|
3
|
+
## Update recovery during an unfinished switch — 2026-11-12
|
|
4
|
+
|
|
5
|
+
User 0.3.11 candidate: the updater may replace the exact previously confirmed switch target
|
|
6
|
+
only after managed services and known listeners are stopped. It retains the switch
|
|
7
|
+
journal, runs the new service command after package installation, then repairs the
|
|
8
|
+
installation. A failed resume keeps the update checkpoint for retry; another target
|
|
9
|
+
cannot adopt the transition. An existing confirmed switch no longer needs duplicate
|
|
10
|
+
consent during resume. 35 update/selection checks passed with temporary storage,
|
|
11
|
+
real local sockets and controlled npm/systemctl; participant acceptance remains open.
|
|
12
|
+
Already-installed 0.3.9/0.3.10 updaters still reject this state before fetching code,
|
|
13
|
+
so the current stranded installation needs a one-time npm bootstrap. This fix cannot
|
|
14
|
+
retroactively change an installed updater.
|
|
15
|
+
|
|
16
|
+
## Participant incomplete-setup switch — 2026-11-12
|
|
17
|
+
|
|
18
|
+
Published as `v0.3.10` at `6e89266`; 20 targeted selection checks, full CI behavior
|
|
19
|
+
and standalone package checks passed. npm latest and archive integrity independently
|
|
20
|
+
verified. Wallet stays 0.3.3; participant switch-back remains to be verified.
|
|
21
|
+
[Release workflow](https://github.com/c4rO-0/agentpay-user/actions/runs/43173745431).
|
|
22
|
+
|
|
23
|
+
Participant A-to-B setup cancellation exposed a switch-back failure: B had no
|
|
24
|
+
Wallet directory, but selection required complete service settings. User 0.3.10 fix
|
|
25
|
+
allows demonstrably unconfigured B to release selection after confirmed systemd
|
|
26
|
+
stop and socket checks. Existing key/Policy/connection or invalid directories still
|
|
27
|
+
block missing-configuration bypass. Original participant A remains stopped until
|
|
28
|
+
the corrected switch is run. Full two-running-installation acceptance is pending.
|
|
29
|
+
|
|
3
30
|
## Official Skill local verification — 2026-11-12
|
|
4
31
|
|
|
5
|
-
|
|
32
|
+
Published as `v0.3.9` at `8fbae4e`. Full GitHub behavior and standalone package
|
|
33
|
+
checks passed; npm latest and archive integrity independently verified. Wallet stays
|
|
34
|
+
0.3.3, Protocol 0.14.3 and official test Skill 1.2.1.
|
|
35
|
+
[Release workflow](https://github.com/c4rO-0/agentpay-user/actions/runs/43162494802).
|
|
36
|
+
|
|
37
|
+
User 0.3.9: update/repair verifies the release-pinned local ZIP and its extracted
|
|
6
38
|
files before using the network. Matching files skip download; edited or incomplete
|
|
7
39
|
extracted files are preserved with guidance. Software-update repair progress streams
|
|
8
40
|
as it happens. Targeted update/package regressions passed; the actual pinned 1.2.1
|
package/src/npm-update.mjs
CHANGED
|
@@ -106,7 +106,13 @@ async function updateUnderLock({
|
|
|
106
106
|
} catch(error) { if(error.code !== 'ENOENT') throw error; }
|
|
107
107
|
dataDir ??= await resolveUserDataDir({userDir});
|
|
108
108
|
if (pending && pending.dataDir !== dataDir) throw Error('The interrupted update belongs to another data folder. Keep its recovery record for review.');
|
|
109
|
-
const scope = await serviceScope({userDir,dataDir});
|
|
109
|
+
const scope = await serviceScope({userDir,dataDir,resumeUpdate:true});
|
|
110
|
+
const resumeSwitch = async options => {
|
|
111
|
+
if (!scope.resumeSwitch) return;
|
|
112
|
+
write('Continuing your previously confirmed installation switch…');
|
|
113
|
+
const output = await service('start',options);
|
|
114
|
+
if(output)write(output);
|
|
115
|
+
};
|
|
110
116
|
if (pending && current.version === pending.targetVersion) {
|
|
111
117
|
installing = true;
|
|
112
118
|
if (current.dependencies?.['@c4roa0/agentpay-wallet'] !== pending.walletVersion ||
|
|
@@ -115,6 +121,7 @@ async function updateUnderLock({
|
|
|
115
121
|
write('Continuing the interrupted update. Checking installed packages and service settings.');
|
|
116
122
|
const configured = await lstat(join(dataDir,'runtime/local-services.json')).then(s=>s.isFile(),e=>{if(e.code==='ENOENT')return false;throw e;});
|
|
117
123
|
if (configured) {
|
|
124
|
+
await resumeSwitch({env:{...process.env,AGENTPAY_USER_DATA_DIR:dataDir},cwd:target.prefix,timeout:600000});
|
|
118
125
|
const output = await repair({env:{...process.env,AGENTPAY_USER_DATA_DIR:dataDir},cwd:target.prefix,timeout:600000});
|
|
119
126
|
if(output)write(output);
|
|
120
127
|
} else await officialTest({userDir,dataDir,write});
|
|
@@ -132,6 +139,15 @@ async function updateUnderLock({
|
|
|
132
139
|
if (pending && (metadata.version !== pending.targetVersion || metadata.dependencies["@c4roa0/agentpay-wallet"] !== pending.walletVersion || metadata.dependencies["@c4roa0/agentpay-protocol"] !== pending.protocolVersion))
|
|
133
140
|
throw Error("The published package differs from the interrupted update. No program files were changed.");
|
|
134
141
|
if (!newer(metadata.version,current.version)) {
|
|
142
|
+
if (scope.resumeSwitch) {
|
|
143
|
+
if (!force && (await ask("Continue the previously confirmed installation switch? Type 'confirm', or 'q' to cancel")).trim().toLowerCase() !== 'confirm') {
|
|
144
|
+
write('Update cancelled. The saved switch was kept.'); return 0;
|
|
145
|
+
}
|
|
146
|
+
const options={env:{...process.env,AGENTPAY_USER_DATA_DIR:dataDir},cwd:target.prefix,timeout:600000};
|
|
147
|
+
await resumeSwitch(options);
|
|
148
|
+
const output=await repair(options); if(output)write(output);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
135
151
|
write(`User ${current.version} is current. Checking the official test Skill.`);
|
|
136
152
|
await officialTest({userDir,dataDir,write});
|
|
137
153
|
return 0;
|
|
@@ -149,7 +165,8 @@ async function updateUnderLock({
|
|
|
149
165
|
write(`User ${current.version} → ${metadata.version}`);
|
|
150
166
|
write(`Wallet ${walletVersion === targetWallet ? `${walletVersion} (unchanged)` : `${walletVersion || 'not confirmed'} → ${targetWallet}`}`);
|
|
151
167
|
write(`Installation folder: ${target.prefix}`);
|
|
152
|
-
|
|
168
|
+
if (scope.resumeSwitch) write('After updating, AgentPay will finish your previously confirmed installation switch.');
|
|
169
|
+
write(scope.active ? 'Background services will pause. Keys, permissions, Orders and cache are kept.' : scope.resumeSwitch ? 'Services are stopped. Keys, permissions, Orders and cache are kept.' : 'This installation is inactive. The running Wallet and Runtime will not change.');
|
|
153
170
|
if (!force && (await ask("Type 'confirm' to update, or 'q' to cancel")).trim().toLowerCase() !== 'confirm') {
|
|
154
171
|
write('Update cancelled. Nothing was changed.'); return 0;
|
|
155
172
|
}
|
|
@@ -202,7 +219,8 @@ async function updateUnderLock({
|
|
|
202
219
|
installed.dependencies?.['@c4roa0/agentpay-protocol'] !== metadata.dependencies['@c4roa0/agentpay-protocol'])
|
|
203
220
|
throw Error('The installed version does not match the confirmed update.');
|
|
204
221
|
if (configured) {
|
|
205
|
-
|
|
222
|
+
await resumeSwitch(options);
|
|
223
|
+
write(scope.active || scope.resumeSwitch ? '3/3 Restore services and verify the official test Skill.' : '3/3 Verify the inactive installation and official test Skill.');
|
|
206
224
|
// A fresh Node process loads the NEW repair/client/official digest, not old imports.
|
|
207
225
|
const output = await repair(options);
|
|
208
226
|
if(output) write(output);
|
|
@@ -6,7 +6,7 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { autostartFiles, units } from './autostart-files.mjs';
|
|
8
8
|
import { installationDirectory } from './installation-paths.mjs';
|
|
9
|
-
import { withInstallationOperation } from './installation-operation.mjs';
|
|
9
|
+
import { withInstallationOperation, runInstallationCommand } from './installation-operation.mjs';
|
|
10
10
|
import { readServiceSettings, listenerPresent } from './local-services.mjs';
|
|
11
11
|
import { atomicWritePrivate } from './private-journal-files.mjs';
|
|
12
12
|
import { ensureRuntimeConnection } from './runtime-connection-settings.mjs';
|
|
@@ -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}) {
|
|
@@ -84,7 +104,7 @@ export async function withServiceSelection({target, action, ask, write = console
|
|
|
84
104
|
write('Current data: ' + previous.dataDir);
|
|
85
105
|
write('Selected data: ' + target.dataDir);
|
|
86
106
|
write('Wallet and Runtime will stop. Keys, permissions and Order records stay in their own folders.');
|
|
87
|
-
if (!ask || String(await ask("Type 'confirm' to switch, or 'q' to cancel")).trim().toLowerCase() !== 'confirm') {
|
|
107
|
+
if (!recovery && (!ask || String(await ask("Type 'confirm' to switch, or 'q' to cancel")).trim().toLowerCase() !== 'confirm')) {
|
|
88
108
|
write('Switch cancelled. Existing services were kept.');
|
|
89
109
|
return {cancelled:true};
|
|
90
110
|
}
|
|
@@ -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) {
|
|
@@ -168,7 +188,8 @@ export async function withServiceLifecycleLock(operation) {
|
|
|
168
188
|
|
|
169
189
|
// Called while holding the installation-operation lock. Maintenance of an inactive
|
|
170
190
|
// program tree must never claim the fixed service pair or rewrite active data.
|
|
171
|
-
export async function maintenanceServiceScope({userDir,dataDir,unitDir
|
|
191
|
+
export async function maintenanceServiceScope({userDir,dataDir,unitDir, resumeUpdate = false,
|
|
192
|
+
ctl = (...args) => runInstallationCommand('systemctl',['--user',...args],{timeout:30000})}) {
|
|
172
193
|
const canonical = async path => realpath(path).catch(error=>{if(error.code==='ENOENT')return resolve(path);throw error;});
|
|
173
194
|
userDir = await canonical(userDir); dataDir = await canonical(dataDir);
|
|
174
195
|
let saved;
|
|
@@ -177,8 +198,26 @@ export async function maintenanceServiceScope({userDir,dataDir,unitDir}) {
|
|
|
177
198
|
const {assertPrivateFile}=await import('./private-journal-files.mjs');
|
|
178
199
|
await assertPrivateFile(path); saved=JSON.parse(await readFile(path,'utf8'));
|
|
179
200
|
if(saved.version!==1)throw Error('Unrecognized service selection. Keep it for review.');
|
|
180
|
-
if(['stopping','preparing'].includes(saved.phase) && !await targetUnitsPrepared(unitDir,saved.target))
|
|
181
|
-
|
|
201
|
+
if(['stopping','preparing'].includes(saved.phase) && !await targetUnitsPrepared(unitDir,saved.target)) {
|
|
202
|
+
if (!resumeUpdate || !saved.target ||
|
|
203
|
+
await canonical(saved.target.userDir)!==userDir || await canonical(saved.target.dataDir)!==dataDir)
|
|
204
|
+
throw Error('A service switch is unfinished. Run agentpay update from its originally selected installation.');
|
|
205
|
+
// Read-only preflight: the prior confirmation remains bound to this exact
|
|
206
|
+
// target. Do not replace code while any managed process could still use it.
|
|
207
|
+
await installedServiceSelection(unitDir,saved);
|
|
208
|
+
for (const name of units) {
|
|
209
|
+
const result=await ctl('show',name,'--property=ActiveState','--value');
|
|
210
|
+
if (!['inactive','failed'].includes(String(result.stdout||'').trim()))
|
|
211
|
+
throw Error('The interrupted switch still has running services. Stop them before updating.');
|
|
212
|
+
}
|
|
213
|
+
for (const path of new Set([saved.previous?.dataDir,dataDir].filter(Boolean))) {
|
|
214
|
+
const settings=await previousServiceSettings(path);
|
|
215
|
+
if (await listenerPresent(join(path,'wallet/wallet.sock')) ||
|
|
216
|
+
(settings && await listenerPresent({host:'127.0.0.1',port:Number(new URL(settings.runtimeUrl).port)})))
|
|
217
|
+
throw Error('An installation listener is still running. Stop it before updating.');
|
|
218
|
+
}
|
|
219
|
+
return {active:false,resumeSwitch:true};
|
|
220
|
+
}
|
|
182
221
|
} catch(error) {if(error.code!=='ENOENT')throw error;}
|
|
183
222
|
const owner=await installedServiceSelection(unitDir) || saved?.target;
|
|
184
223
|
if(!owner)return {active:true}; // Existing single-install setup/repair behavior.
|