@c4roa0/agentpay-user 0.3.6 → 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 +12 -0
- package/doc/TODO.md +48 -1
- 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/records/npm-update-participant-2026-11-11.md +25 -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 +136 -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/cache-management-cli.mjs +32 -2
- 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 +68 -20
- 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/progress.md
CHANGED
|
@@ -1,20 +1,150 @@
|
|
|
1
1
|
# AgentPay User progress
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Real two-installation switch passed — 2026-11-12
|
|
4
|
+
|
|
5
|
+
Actual user-systemd switching A → B → A passed using source and folder-local npm
|
|
6
|
+
candidate installations with separate data. Cancellation preserved A's process;
|
|
7
|
+
repeated B start preserved its process. B authenticated status passed. Original
|
|
8
|
+
A key/Policy hashes were unchanged, and its existing Orders/Market connection
|
|
9
|
+
remained visible. A is restored with both services running and startup enabled;
|
|
10
|
+
Wallet is locked. Source dependencies now match Wallet 0.3.3 / Protocol 0.14.3;
|
|
11
|
+
old dependencies and inactive B are retained privately. No payment or authorization.
|
|
12
|
+
The initial harness rejected an expected nonzero status for B's unconfigured Wallet;
|
|
13
|
+
corrected full execution passed. This does not establish fresh guided install,
|
|
14
|
+
desktop login/reboot or funded recovery. [Evidence](doc/records/dual-install-systemd-2026-11-12.md).
|
|
15
|
+
|
|
16
|
+
## Service command recovery — 2026-11-12
|
|
17
|
+
|
|
18
|
+
Service-manager failure recovery is now stage-aware: complete exact target unit
|
|
19
|
+
files are checkpointed before reload/start/stop, so command failure permits retry
|
|
20
|
+
or repair. Older incomplete journals recover only after all target units match;
|
|
21
|
+
partial/conflicting units remain blocked. Sixteen selection checks passed, including
|
|
22
|
+
actual autostart subprocesses with a controlled systemctl executable. No real
|
|
23
|
+
systemd service or transaction was changed (2026-11-12).
|
|
24
|
+
|
|
25
|
+
## Sequential review fixes — 2026-11-11
|
|
26
|
+
|
|
27
|
+
Shared-data installation switching now relocates the client module only after
|
|
28
|
+
switch confirmation and stopped-service/listener checks. It accepts the previous
|
|
29
|
+
managed installation path or the already-relocated current path; unrelated paths
|
|
30
|
+
remain blocked. Retrying `service start` completes interrupted preparation without
|
|
31
|
+
requiring repair first. Keys, authorization and request records are unchanged.
|
|
32
|
+
Twelve selection tests and both connection/preparation test files passed with
|
|
33
|
+
real temporary storage/native locks and simulated service management. Real systemd
|
|
34
|
+
acceptance is still pending.
|
|
35
|
+
|
|
36
|
+
Default availability now requires authenticated Wallet/Runtime readiness and
|
|
37
|
+
matching running versions; locked Wallets are accepted. Startup preferences and
|
|
38
|
+
preparation preserve existing availability, removal closes it, and readiness failure
|
|
39
|
+
leaves maintenance retryable. Login startup verifies both services under the selection
|
|
40
|
+
lock and checks the selected program/data identity. Transport fixtures and lifecycle
|
|
41
|
+
regressions passed; actual systemd acceptance remains pending.
|
|
42
|
+
|
|
43
|
+
Installation now stages downloaded settings in memory, confirms activation under
|
|
44
|
+
the shared operation lock, then writes settings and binds the installation. Cancel
|
|
45
|
+
preserves the previous deployment. A private per-user state locator preserves the
|
|
46
|
+
initial XDG location across shells, background services and candidate Skill loaders;
|
|
47
|
+
changing XDG does not create another ownership/maintenance lock namespace.
|
|
48
|
+
Isolated installation, transport, process-lock and update/uninstall regressions passed.
|
|
49
|
+
Next: overall review and real two-install systemd acceptance before release.
|
|
50
|
+
systemd is a Linux backend, not the definition of installation identity.
|
|
51
|
+
macOS background startup remains deferred until a test machine is available.
|
|
52
|
+
|
|
53
|
+
## Multiple installations, one active Runtime — 2026-11-11
|
|
54
|
+
|
|
55
|
+
Working-tree implementation now includes canonical installation/data bindings,
|
|
56
|
+
reuse/new/cancel selection, confirmed service switching, single Runtime lifetime
|
|
57
|
+
ownership, default Skill routing and protection for newly recorded request scopes.
|
|
58
|
+
Agent template 0.1.2 / example Skill 1.2.2 remain unpublished candidates; User still
|
|
59
|
+
pins official Skill 1.2.1. Historical request ownership is now indexed during
|
|
60
|
+
confirmed switches; original files and authorization remain unchanged.
|
|
61
|
+
[Design and remaining acceptance](doc/plans/local-instances-and-install-recovery.md).
|
|
62
|
+
|
|
63
|
+
Installation, update, repair, uninstall, service switching and address maintenance
|
|
64
|
+
now share one operation lock. Inactive update/repair preserves the selected services;
|
|
65
|
+
shared active program/data paths require explicit switching. Short-lived writers and
|
|
66
|
+
administration children inherit the lock, while daemons do not. Recognizable v2 locks
|
|
67
|
+
recover after writers exit; old unknown locks remain blocked for review. Interrupted
|
|
68
|
+
updates retain the confirmed target and finish repair even when npm already installed
|
|
69
|
+
that version. Uninstall rechecks the confirmed scope and supports partial-unit retry.
|
|
70
|
+
|
|
71
|
+
77 combined targeted checks passed, followed by the marker-publication recovery
|
|
72
|
+
check and its process-lock regressions. These cover process interruption/inheritance, temporary
|
|
73
|
+
storage and controlled npm/service interfaces. A separate offline npm install verified
|
|
74
|
+
Wallet 0.3.3, Protocol 0.14.3 and native-lock loading with hooks disabled. The working
|
|
75
|
+
checkout's older dependencies were not replaced. [Evidence and limits](doc/records/installation-maintenance-2026-11-11.md).
|
|
76
|
+
|
|
77
|
+
Historical routing now preserves known IDs and refuses missing/conflicting original
|
|
78
|
+
selection. Unclassified older history does not prevent startup, but blocks automatic
|
|
79
|
+
replacement calls from another installation; explicit force remains a new purchase.
|
|
80
|
+
35 focused checks passed with controlled HTTP/service fixtures.
|
|
81
|
+
|
|
82
|
+
Remaining: complete two-install public-entry and
|
|
83
|
+
real systemd switch/login acceptance; release coordination and participant rehearsal.
|
|
84
|
+
Unknown occupied ports still conservatively block uninstall. No live service, key,
|
|
85
|
+
Policy, Order, payment, migration, commit or publication changed in this loop.
|
|
86
|
+
|
|
87
|
+
## User 0.3.7 published — 2026-11-11
|
|
88
|
+
|
|
89
|
+
Patch release includes installed/target Wallet version preview and clearer Cache ID
|
|
90
|
+
help with unique short-ID lookup for read-only details. Wallet remains pinned to
|
|
91
|
+
0.3.3 and Protocol to 0.14.3. Manifest, lockfile and update-pair versions are aligned.
|
|
92
|
+
393 behavior tests passed from an isolated lockfile installation; standalone npm
|
|
93
|
+
archive installation and process lifecycle passed. Tag v0.3.7 points to source
|
|
94
|
+
3d31297dd3baedbd798f51e86c5fa8a1e474bb4a. GitHub run 42976422954 succeeded;
|
|
95
|
+
public registry SHA-512 matches the tested archive and latest is 0.3.7. No local
|
|
96
|
+
participant service or private data was changed by publication.
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Existing client update accepted — 2026-11-11
|
|
100
|
+
|
|
101
|
+
Participant directory-local npm update to User 0.3.6 / Wallet 0.3.3 completed.
|
|
102
|
+
Running versions, unlocked Wallet, two settled Orders, unchanged saved validity,
|
|
103
|
+
startup/Market connection and retained available result cache were observed.
|
|
104
|
+
Participant confirmed both original Orders remain visible on Market. The older
|
|
105
|
+
missing result predates this update. No new payment or Policy was created.
|
|
106
|
+
[Evidence and limits](doc/records/npm-update-participant-2026-11-11.md). Update-preview and Cache ID guidance fixes are
|
|
107
|
+
published in 0.3.7. Fresh installation and interrupted-update
|
|
108
|
+
acceptance remain next for discussion; neither has started in this loop.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## Cache details guidance — 2026-11-11
|
|
112
|
+
|
|
113
|
+
Missing details IDs now show a short example before accessing local storage.
|
|
114
|
+
List guidance explains that the namespace is part of the ID. Read-only details
|
|
115
|
+
accept a unique prefix with at least eight hexadecimal characters; ambiguous,
|
|
116
|
+
missing or incomplete bounded lookups require a full ID. Export and cleanup keep
|
|
117
|
+
their existing exact-identity rules. All 19 targeted cache/CLI regressions passed
|
|
118
|
+
with isolated process tests outside the restricted sandbox; no live cache changed.
|
|
119
|
+
Included in published 0.3.7. There is no input: ID namespace: inputs use the
|
|
120
|
+
attachment ID shown by list, while results can use result: or runtime-result:0x.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Update preview clarity — 2026-11-11
|
|
124
|
+
|
|
125
|
+
The confirmation screen now lists installed User and Wallet versions separately
|
|
126
|
+
from update targets. Wallet is read from the installed dependency, not inferred
|
|
127
|
+
from User's requested dependency. Unchanged versions are explicit; unreadable or
|
|
128
|
+
missing Wallet metadata shows “not confirmed” without blocking the normal repair
|
|
129
|
+
through npm. Controlled update tests cover all three cases and cancellation without
|
|
130
|
+
service/install mutation. Published in 0.3.7; running-process
|
|
131
|
+
versions remain the responsibility of status after update/restart.
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
## User 0.3.6 published — 2026-11-10
|
|
4
135
|
|
|
5
136
|
Pins published Wallet 0.3.3, retaining Protocol 0.14.3. Maintenance errors provide
|
|
6
137
|
same-request guidance without automatic write retries. Package manifest, lockfile
|
|
7
|
-
and update-pair metadata agree.
|
|
8
|
-
|
|
9
|
-
|
|
138
|
+
and update-pair metadata agree. Published through matching tag v0.3.6 after 391 behavior tests and independent
|
|
139
|
+
installation/lifecycle checks. Public npm metadata and SHA-512 match the verified
|
|
140
|
+
archive. No local data, Policy or service changes occur on publication.
|
|
10
141
|
|
|
11
142
|
|
|
12
143
|
## Maintenance guidance — 2026-11-10
|
|
13
144
|
|
|
14
145
|
Added explicit maintenance guidance without automatic write retries or replacement
|
|
15
146
|
purchases. Controlled HTTP/IPC tests verify that retry retains original request
|
|
16
|
-
identifiers. This is source verification, not live financial acceptance.
|
|
17
|
-
have not been published to npm.
|
|
147
|
+
identifiers. This is source verification, not live financial acceptance. These changes were subsequently published in 0.3.6; see the release record above.
|
|
18
148
|
|
|
19
149
|
|
|
20
150
|
## Acceptance bookkeeping — 2026-11-10
|
package/scripts/autostart.mjs
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MPL-2.0
|
|
3
3
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
4
4
|
// If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
|
|
5
|
+
import { withServiceSelection } from "../src/service-selection.mjs";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
5
7
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import {
|
|
8
|
+
import {runInstallationCommand,withInstallationOperation} from "../src/installation-operation.mjs";
|
|
7
9
|
import { lstat, mkdir, readFile, writeFile, unlink, rename } from "node:fs/promises";
|
|
8
10
|
import { join } from "node:path";
|
|
9
11
|
import { homedir } from "node:os";
|
|
@@ -22,15 +24,17 @@ import {
|
|
|
22
24
|
saveServiceSettings,
|
|
23
25
|
} from "../src/local-services.mjs";
|
|
24
26
|
import { ensureLocalServices } from "../src/prepare-local-services.mjs";
|
|
25
|
-
const ctl = (...args) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const ctl = async (...args) => {
|
|
28
|
+
const result = await runInstallationCommand("systemctl", ["--user", ...args], {timeout:30000});
|
|
29
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
30
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
30
33
|
try {
|
|
31
34
|
const [action, ...args] = process.argv.slice(2);
|
|
32
35
|
if (
|
|
33
36
|
![
|
|
37
|
+
"activate-setup",
|
|
34
38
|
"prepare",
|
|
35
39
|
"prepare-repair",
|
|
36
40
|
"enable",
|
|
@@ -53,13 +57,21 @@ try {
|
|
|
53
57
|
);
|
|
54
58
|
const dataDir = await resolveUserDataDir();
|
|
55
59
|
if (action === "configure") {
|
|
56
|
-
await saveServiceSettings({
|
|
60
|
+
await withInstallationOperation(() => saveServiceSettings({
|
|
57
61
|
dataDir,
|
|
58
62
|
runtimeUrl: args[0],
|
|
59
63
|
settlementUrl: args[1],
|
|
60
|
-
});
|
|
64
|
+
}));
|
|
61
65
|
console.log("Local startup settings saved. No service was started.");
|
|
62
66
|
} else {
|
|
67
|
+
const selectionResult = await withServiceSelection({
|
|
68
|
+
target:{node:process.execPath,userDir:installedUserDir,dataDir}, action,
|
|
69
|
+
ctl:(...args) => runInstallationCommand('systemctl',['--user',...args],{timeout:30000}),
|
|
70
|
+
ask:process.stdin.isTTY ? async message => {
|
|
71
|
+
const terminal = createInterface({input:process.stdin,output:process.stdout});
|
|
72
|
+
try { return await terminal.question(message + '\n> '); } finally { terminal.close(); }
|
|
73
|
+
} : undefined,
|
|
74
|
+
operation:async ({previous,switched,unitsPrepared}) => {
|
|
63
75
|
const files = autostartFiles({
|
|
64
76
|
node: process.execPath,
|
|
65
77
|
userDir: installedUserDir,
|
|
@@ -68,7 +80,7 @@ try {
|
|
|
68
80
|
const config = join(homedir(), ".config"),
|
|
69
81
|
unitDir = join(config, "systemd/user");
|
|
70
82
|
for (const directory of [config, join(config, "systemd"), unitDir]) {
|
|
71
|
-
if (["enable", "prepare", "prepare-repair"].includes(action))
|
|
83
|
+
if (["enable", "prepare", "prepare-repair", "start", "restart", "activate-setup"].includes(action))
|
|
72
84
|
await mkdir(directory, { mode: 0o700 }).catch((e) => {
|
|
73
85
|
if (e.code !== "EEXIST") throw e;
|
|
74
86
|
});
|
|
@@ -96,7 +108,7 @@ try {
|
|
|
96
108
|
info.isSymbolicLink() ||
|
|
97
109
|
info.uid !== process.getuid() ||
|
|
98
110
|
info.mode & 0o022 ||
|
|
99
|
-
!isManagedUnit(await readFile(path, "utf8"), name, dataDir)
|
|
111
|
+
!(isManagedUnit(await readFile(path, "utf8"), name, dataDir) || (switched && previous && autostartFiles(previous)[name] === await readFile(path,"utf8")))
|
|
100
112
|
)
|
|
101
113
|
throw Error(
|
|
102
114
|
"An existing startup file belongs to another configuration. Preserve it and review before switching.",
|
|
@@ -106,21 +118,27 @@ try {
|
|
|
106
118
|
}
|
|
107
119
|
}
|
|
108
120
|
if (action === "status") {
|
|
109
|
-
ctl("status", "--no-pager", ...runtimeUnits);
|
|
121
|
+
await ctl("status", "--no-pager", ...runtimeUnits);
|
|
110
122
|
console.log(
|
|
111
123
|
"The login reminder runs once per graphical session; it can be inactive outside a desktop login.",
|
|
112
124
|
);
|
|
113
125
|
} else if (["start", "restart", "stop"].includes(action)) {
|
|
114
|
-
if (action !==
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
if (action !== 'stop') {
|
|
127
|
+
await ensureLocalServices({dataDir});
|
|
128
|
+
for (const {path,body} of paths) {
|
|
129
|
+
const temporary = path + '.' + randomUUID();
|
|
130
|
+
try { await writeFile(temporary,body,{flag:'wx',mode:0o600}); await rename(temporary,path); }
|
|
131
|
+
finally { await unlink(temporary).catch(e=>{if(e.code!=='ENOENT')throw e;}); }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await unitsPrepared();
|
|
135
|
+
if (action !== 'stop') await ctl('daemon-reload');
|
|
136
|
+
await ctl(action, ...(action !== "stop" ? runtimeUnits : units));
|
|
137
|
+
if (action === 'stop') console.log(
|
|
138
|
+
'Stop requested through systemd. Login startup and saved Wallet data are unchanged.\nNext: agentpay status',
|
|
121
139
|
);
|
|
122
|
-
} else if (["enable", "prepare", "prepare-repair"].includes(action)) {
|
|
123
|
-
await ensureLocalServices({ dataDir, connectionOptions: action === "prepare-repair" ? { refreshClientModule: true, checkOnly: true } : {} });
|
|
140
|
+
} else if (["enable", "prepare", "prepare-repair", "activate-setup"].includes(action)) {
|
|
141
|
+
if (action !== "activate-setup") await ensureLocalServices({ dataDir, connectionOptions: action === "prepare-repair" ? { refreshClientModule: true, checkOnly: true } : {} });
|
|
124
142
|
// No --now: avoid replacing a Wallet currently owned by terminal setup/system services.
|
|
125
143
|
for (const { path, body } of paths)
|
|
126
144
|
try {
|
|
@@ -133,9 +151,9 @@ try {
|
|
|
133
151
|
await rename(temporary,path);
|
|
134
152
|
} finally { await unlink(temporary).catch(e => { if(e.code !== 'ENOENT') throw e; }); }
|
|
135
153
|
}
|
|
136
|
-
ctl("daemon-reload");
|
|
154
|
+
await ctl("daemon-reload");
|
|
137
155
|
if (action === "enable") {
|
|
138
|
-
ctl("enable", ...units);
|
|
156
|
+
await ctl("enable", ...units);
|
|
139
157
|
await saveServiceSettings({
|
|
140
158
|
dataDir,
|
|
141
159
|
...(await readServiceSettings(dataDir)),
|
|
@@ -150,7 +168,7 @@ try {
|
|
|
150
168
|
);
|
|
151
169
|
} else if (action === "disable") {
|
|
152
170
|
await readServiceSettings(dataDir);
|
|
153
|
-
ctl("disable", ...units);
|
|
171
|
+
await ctl("disable", ...units);
|
|
154
172
|
await saveServiceSettings({
|
|
155
173
|
dataDir,
|
|
156
174
|
...(await readServiceSettings(dataDir)),
|
|
@@ -165,16 +183,21 @@ try {
|
|
|
165
183
|
...(await readServiceSettings(dataDir)),
|
|
166
184
|
startupChoice: "disabled",
|
|
167
185
|
});
|
|
168
|
-
ctl("disable", "--now", ...units);
|
|
186
|
+
await ctl("disable", "--now", ...units);
|
|
169
187
|
for (const { path } of paths)
|
|
170
188
|
await unlink(path).catch((e) => {
|
|
171
189
|
if (e.code !== "ENOENT") throw e;
|
|
172
190
|
});
|
|
173
|
-
ctl("daemon-reload");
|
|
191
|
+
await ctl("daemon-reload");
|
|
174
192
|
console.log(
|
|
175
193
|
"Automatic startup and its reminder removed. Wallet data, Policy and request history are kept.",
|
|
176
194
|
);
|
|
177
195
|
}
|
|
196
|
+
}});
|
|
197
|
+
if (selectionResult?.cancelled) process.exitCode = 2;
|
|
198
|
+
else if (['start','restart'].includes(action)) console.log(
|
|
199
|
+
'Wallet and Runtime are ready. Default Skill access is available.\nNext: agentpay status',
|
|
200
|
+
);
|
|
178
201
|
}
|
|
179
202
|
} catch (error) {
|
|
180
203
|
console.error(
|
|
@@ -182,5 +205,8 @@ try {
|
|
|
182
205
|
? error.message
|
|
183
206
|
: "The systemd command failed; some service changes may already have applied. Wallet keys and request history were kept. Run agentpay status before retrying.",
|
|
184
207
|
);
|
|
208
|
+
if (process.argv[2] === 'stop') console.error('Retry: agentpay service stop');
|
|
209
|
+
else if (['start','restart'].includes(process.argv[2]))
|
|
210
|
+
console.error('Check: agentpay status\nRetry: agentpay service start');
|
|
185
211
|
process.exitCode = 1;
|
|
186
212
|
}
|
package/scripts/install-user.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
4
4
|
// If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { selectInstallationData } from "../src/install-selection.mjs";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { installAndSetup } from "../src/install-and-setup.mjs";
|
|
8
9
|
try {
|
|
9
10
|
const args = process.argv.slice(2);
|
|
@@ -13,10 +14,16 @@ try {
|
|
|
13
14
|
throw Error(
|
|
14
15
|
"Run Install in an interactive terminal so setup can continue.",
|
|
15
16
|
);
|
|
16
|
-
|
|
17
|
+
const userDir = fileURLToPath(new URL("..", import.meta.url));
|
|
18
|
+
const terminal = createInterface({input:process.stdin,output:process.stdout});
|
|
19
|
+
let selection;
|
|
20
|
+
try { selection = await selectInstallationData({userDir,ask:message=>terminal.question(message+'\n> ')}); }
|
|
21
|
+
finally { terminal.close(); }
|
|
22
|
+
if (!selection) console.log('Installation cancelled. Existing services were kept.');
|
|
23
|
+
else process.exitCode = await installAndSetup({
|
|
17
24
|
userDir: fileURLToPath(new URL("..", import.meta.url)),
|
|
18
25
|
market: args[1],
|
|
19
|
-
dataDir:
|
|
26
|
+
dataDir: selection.dataDir,
|
|
20
27
|
});
|
|
21
28
|
} catch (error) {
|
|
22
29
|
if (!error.messageDisplayed) console.error(error.message);
|
|
@@ -31,7 +31,7 @@ try {
|
|
|
31
31
|
? walletCliPath()
|
|
32
32
|
: join(userDir, "src/index.mjs");
|
|
33
33
|
const env = Object.fromEntries(
|
|
34
|
-
["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR"]
|
|
34
|
+
["HOME", "PATH", "LANG", "LC_ALL", "TZ", "TMPDIR", "XDG_STATE_HOME"]
|
|
35
35
|
.filter((k) => process.env[k])
|
|
36
36
|
.map((k) => [k, process.env[k]]),
|
|
37
37
|
);
|
package/scripts/user-setup.mjs
CHANGED
|
@@ -28,6 +28,10 @@ try {
|
|
|
28
28
|
"../src/local-data-directory.mjs"
|
|
29
29
|
);
|
|
30
30
|
const { runGuidedEntry } = await import("../src/guided-setup.mjs");
|
|
31
|
+
const dataDir = await resolveUserDataDir({argument:args[1]});
|
|
32
|
+
const { activateSetup } = await import('../src/setup-service-selection.mjs');
|
|
33
|
+
const activation = await activateSetup({dataDir});
|
|
34
|
+
if (activation) process.exit(activation);
|
|
31
35
|
process.exit(
|
|
32
36
|
await runGuidedEntry({
|
|
33
37
|
userDir: fileURLToPath(new URL("..", import.meta.url)),
|
|
@@ -128,6 +128,11 @@ export async function runCacheManagement(
|
|
|
128
128
|
}
|
|
129
129
|
const usage =
|
|
130
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(action === "details" && rest.length === 0){
|
|
132
|
+
write("Choose a Cache ID from agentpay cache list. Copy the whole ID, including any prefix such as result:.");
|
|
133
|
+
write("Example: agentpay cache details result:5aa18b50\nA unique short ID needs at least 8 hexadecimal characters.");
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
131
136
|
if (
|
|
132
137
|
invalidFilter ||
|
|
133
138
|
!["summary", "list", "details", "export", "clean", "settings"].includes(
|
|
@@ -192,7 +197,7 @@ export async function runCacheManagement(
|
|
|
192
197
|
...(rest[1] ? { purpose: rest[1] } : {}),
|
|
193
198
|
});
|
|
194
199
|
write(
|
|
195
|
-
`\n${r.total} entries · page ${page}\nDetails: agentpay cache details <Cache ID
|
|
200
|
+
`\n${r.total} entries · page ${page}\nDetails: agentpay cache details <Cache ID>\nCopy the full ID including its prefix, or use a unique short ID (8+ hex characters).`,
|
|
196
201
|
);
|
|
197
202
|
if (!r.items.length)
|
|
198
203
|
write("No saved results or attachments match this view.");
|
|
@@ -222,7 +227,32 @@ export async function runCacheManagement(
|
|
|
222
227
|
.join("")}`,
|
|
223
228
|
);
|
|
224
229
|
} else if (action === "details") {
|
|
225
|
-
|
|
230
|
+
let id=rest[0];
|
|
231
|
+
if(!/^(?:result:|runtime-result:0x)?[0-9a-f]{64}$/.test(id)){
|
|
232
|
+
if(!/^(?:result:|runtime-result:0x)?[0-9a-f]{8,63}$/.test(id)){
|
|
233
|
+
write('Use the Cache ID from agentpay cache list, or its first 8 or more hexadecimal characters.');
|
|
234
|
+
return 2;
|
|
235
|
+
}
|
|
236
|
+
// Resolve only read-only details. Bound enumeration; never guess from an
|
|
237
|
+
// incomplete list or turn a short display identifier into a cleanup key.
|
|
238
|
+
const matches=new Set();let offset=0,total;
|
|
239
|
+
do {
|
|
240
|
+
const page=await operate({action:'list',offset,limit:100});
|
|
241
|
+
if(page.total>1000 || total!==undefined&&page.total!==total || !page.items.length&&offset<page.total){
|
|
242
|
+
write('Use the full Cache ID; the cache is too large or changed during lookup.');return 2;
|
|
243
|
+
}
|
|
244
|
+
total=page.total;
|
|
245
|
+
for(const row of page.items){
|
|
246
|
+
const key=id.includes(':')?row.id:row.id.replace(/^(?:result:|runtime-result:0x)/,'');
|
|
247
|
+
if(key.startsWith(id))matches.add(row.id);
|
|
248
|
+
}
|
|
249
|
+
if(matches.size>1){write('More than one cache entry matches. Copy a longer or full Cache ID from agentpay cache list.');return 2;}
|
|
250
|
+
offset+=page.items.length;
|
|
251
|
+
}while(offset<total);
|
|
252
|
+
if(!matches.size){write('No cache entry matches. Run agentpay cache list and copy its Cache ID.');return 2;}
|
|
253
|
+
id=[...matches][0];
|
|
254
|
+
}
|
|
255
|
+
const r = await operate({ action, id });
|
|
226
256
|
write(
|
|
227
257
|
`${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
258
|
);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
// A local routing index, not payment authority. Preserve old records in place;
|
|
3
|
+
// only service/input hashes and originating data folders enter this index.
|
|
4
|
+
import {constants} from 'node:fs';
|
|
5
|
+
import {mkdir,open,readdir,lstat,realpath} from 'node:fs/promises';
|
|
6
|
+
import {join,resolve,isAbsolute} from 'node:path';
|
|
7
|
+
import {createHash} from 'node:crypto';
|
|
8
|
+
import {hashPrivateRequest} from '@c4roa0/agentpay-protocol';
|
|
9
|
+
import {installationDirectory} from './installation-paths.mjs';
|
|
10
|
+
import {atomicWritePrivate} from './private-journal-files.mjs';
|
|
11
|
+
import {withInstallationOperation} from './installation-operation.mjs';
|
|
12
|
+
import {hashInvocationKey} from './invocation-store.mjs';
|
|
13
|
+
import {parseServiceReference} from './service-reference.mjs';
|
|
14
|
+
|
|
15
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
16
|
+
const scopeKey = (service,requestHash) => digest(JSON.stringify([service.skillId,service.profileHash,requestHash]));
|
|
17
|
+
const codeError = (code,message) => Object.assign(Error(message),{code});
|
|
18
|
+
const requestIdPattern = /^[A-Za-z0-9._:-]{16,128}$/;
|
|
19
|
+
const review = () => codeError('REQUEST_HISTORY_REVIEW_REQUIRED','Original request history could not be identified safely. Keep the old data folder and inspect agentpay orders list; no replacement purchase was started.');
|
|
20
|
+
const canonical = path => realpath(path).catch(error=>{if(error.code==='ENOENT')return resolve(path);throw error;});
|
|
21
|
+
async function privateDirectory(path) {
|
|
22
|
+
try {
|
|
23
|
+
const info=await lstat(path);
|
|
24
|
+
if(!info.isDirectory() || info.isSymbolicLink() || info.uid!==process.getuid() || info.mode & 0o077)throw review();
|
|
25
|
+
return true;
|
|
26
|
+
}catch(error){if(error.code==='ENOENT')return false;throw error;}
|
|
27
|
+
}
|
|
28
|
+
async function json(path,max=2*1048576) {
|
|
29
|
+
let file;
|
|
30
|
+
try {
|
|
31
|
+
file=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
32
|
+
const info=await file.stat();
|
|
33
|
+
if(!info.isFile() || info.uid!==process.getuid() || info.mode & 0o077 || info.size>max)throw review();
|
|
34
|
+
const value=JSON.parse(await file.readFile('utf8'));
|
|
35
|
+
if(value===null)throw review();
|
|
36
|
+
return value;
|
|
37
|
+
}catch(error){if(error.code==='ENOENT')return null;throw review();}
|
|
38
|
+
finally{await file?.close();}
|
|
39
|
+
}
|
|
40
|
+
async function indexDirectory() {
|
|
41
|
+
const directory=join(installationDirectory(),'request-owners');
|
|
42
|
+
await mkdir(directory,{recursive:true,mode:0o700});await privateDirectory(directory);
|
|
43
|
+
return directory;
|
|
44
|
+
}
|
|
45
|
+
function validOwner(saved) {
|
|
46
|
+
if(saved && (saved.version!==1 || typeof saved.dataDir!=='string' || !isAbsolute(saved.dataDir) ||
|
|
47
|
+
(saved.requestIds!==undefined && (!Array.isArray(saved.requestIds) || saved.requestIds.some(id=>!requestIdPattern.test(id)))) ||
|
|
48
|
+
(saved.conflicts!==undefined && (!Array.isArray(saved.conflicts) || saved.conflicts.some(path=>typeof path!=='string' || !isAbsolute(path))))))throw review();
|
|
49
|
+
}
|
|
50
|
+
async function recordHistory(directory,key,dataDir,requestIds=[]) {
|
|
51
|
+
const path=join(directory,key+'.json'),saved=await json(path,32768);validOwner(saved);
|
|
52
|
+
// Explicit force is a deliberate later purchase, not a migration decision.
|
|
53
|
+
if(saved?.deliberate && saved.dataDir!==dataDir)return;
|
|
54
|
+
if(saved?.dataDir===dataDir) {
|
|
55
|
+
const ids=[...new Set([...(saved.requestIds || []),...requestIds])];
|
|
56
|
+
if(ids.length)await atomicWritePrivate(path,JSON.stringify({...saved,requestIds:ids})+'\n');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const conflicts=saved ? [...new Set([...(saved.conflicts || []),saved.dataDir,dataDir])] : undefined;
|
|
60
|
+
await atomicWritePrivate(path,JSON.stringify(saved ? {...saved,conflicts} : {version:1,dataDir,...(requestIds.length?{requestIds}: {})})+'\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Called with services stopped during a confirmed switch. Scan only these known
|
|
64
|
+
// private stores, once per switch, never arbitrary folders or remote services.
|
|
65
|
+
export async function importHistoricalRequests(dataDir) {
|
|
66
|
+
return withInstallationOperation(async()=>{
|
|
67
|
+
dataDir=await canonical(dataDir);
|
|
68
|
+
try { return await scanHistoricalRequests(dataDir); }
|
|
69
|
+
catch {
|
|
70
|
+
// Incomplete old history must not prevent Runtime startup. Retain a
|
|
71
|
+
// conservative routing barrier; never turn unreadable records into new calls.
|
|
72
|
+
const directory=await indexDirectory();
|
|
73
|
+
await recordHistory(directory,'unclassified',dataDir);
|
|
74
|
+
return {indexed:0,unclassified:true,reviewRequired:true};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async function scanHistoricalRequests(dataDir) {
|
|
79
|
+
if(!await privateDirectory(dataDir))return;
|
|
80
|
+
const runtime=join(dataDir,'runtime');if(!await privateDirectory(runtime))return;
|
|
81
|
+
const directory=await indexDirectory(),keys=new Map(),clientRequests=new Map();
|
|
82
|
+
let bytes=0,count=0;
|
|
83
|
+
for(const kind of ['client-requests','client-pending']) {
|
|
84
|
+
const parent=join(runtime,kind);if(!await privateDirectory(parent))continue;
|
|
85
|
+
const names=await readdir(parent);
|
|
86
|
+
if(names.length>20000 || (kind==='client-pending' && names.some(name=>name.endsWith('.lock'))))throw review();
|
|
87
|
+
for(const name of names) {
|
|
88
|
+
if(!/^[a-f0-9]{64}\.json$/.test(name))continue;
|
|
89
|
+
const path=join(parent,name),info=await lstat(path);bytes+=info.size;
|
|
90
|
+
if(++count>20000 || bytes>64*1048576)throw review();
|
|
91
|
+
const saved=await json(path);if(!saved)throw review();
|
|
92
|
+
let service,input,ids;
|
|
93
|
+
if(kind==='client-requests') {
|
|
94
|
+
if(saved.version!=='agentpay.runtime-client-request.v1' || !requestIdPattern.test(saved.requestId || ''))throw review();
|
|
95
|
+
service=parseServiceReference(saved.service);input=saved.input?.request;ids=[saved.requestId];
|
|
96
|
+
clientRequests.set(hashInvocationKey(saved.requestId),{requestHash:hashPrivateRequest(saved.preparedRequest===undefined ? input : saved.preparedRequest),serviceKey:scopeKey(service,hashPrivateRequest(saved.preparedRequest===undefined ? input : saved.preparedRequest))});
|
|
97
|
+
}else {
|
|
98
|
+
if(!['agentpay.runtime-client-pending.v1','agentpay.runtime-client-pending.v2'].includes(saved.version) || !saved.binding?.service)throw review();
|
|
99
|
+
// Pending records intentionally omit the business adapter. The full
|
|
100
|
+
// checkpoint remains the authority for resume; only scope IDs are used here.
|
|
101
|
+
service=saved.binding.service;input=saved.binding.input;
|
|
102
|
+
if(!Array.isArray(saved.pending))throw review();
|
|
103
|
+
ids=[...saved.pending,...(saved.currentId ? [saved.currentId]:[])];
|
|
104
|
+
if(ids.some(id=>!requestIdPattern.test(id)))throw review();
|
|
105
|
+
if(!/^0x[a-fA-F0-9]{64}$/.test(service.skillId || '') || !/^0x[a-fA-F0-9]{64}$/.test(service.profileHash || ''))throw review();
|
|
106
|
+
}
|
|
107
|
+
const key=scopeKey(service,hashPrivateRequest(input));
|
|
108
|
+
keys.set(key,[...new Set([...(keys.get(key) || []),...ids])]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const history=await json(join(runtime,'invocations.json'),32*1048576);
|
|
112
|
+
let unclassified=false;
|
|
113
|
+
if(history) {
|
|
114
|
+
if(history.version!=='agentpay.user-invocations.v1' || !history.invocations || typeof history.invocations!=='object' || Array.isArray(history.invocations))throw review();
|
|
115
|
+
for(const [id,entry] of Object.entries(history.invocations)) {
|
|
116
|
+
if(!/^0x[a-f0-9]{64}$/.test(entry?.requestHash || '') || entry.invocationId!==id)throw review();
|
|
117
|
+
// Runtime-only records cannot prove the caller's original input (a Skill
|
|
118
|
+
// may prepare/upload files first). Do not guess a cross-installation match.
|
|
119
|
+
const client=clientRequests.get(id);
|
|
120
|
+
let linked=false;
|
|
121
|
+
if(client && client.requestHash===entry.requestHash) {
|
|
122
|
+
try {
|
|
123
|
+
const binding=JSON.parse(entry.serviceBinding);
|
|
124
|
+
linked=binding.version==='agentpay.runtime-service.v1' &&
|
|
125
|
+
scopeKey(parseServiceReference(binding.reference),entry.requestHash)===client.serviceKey;
|
|
126
|
+
}catch { /* Old or incomplete service identity stays unclassified. */ }
|
|
127
|
+
}
|
|
128
|
+
if(!linked)unclassified=true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for(const [key,ids] of keys)await recordHistory(directory,key,dataDir,ids);
|
|
132
|
+
if(unclassified)await recordHistory(directory,'unclassified',dataDir);
|
|
133
|
+
return {indexed:keys.size,unclassified};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Keep ownership selection and local request creation in one maintenance section.
|
|
137
|
+
// Network/payment calls run after it, always against the pinned original connection.
|
|
138
|
+
export async function withDefaultRequest({service,input,dataDir,force=false},operation) {
|
|
139
|
+
return withInstallationOperation(async()=>{
|
|
140
|
+
dataDir=await canonical(dataDir);
|
|
141
|
+
const directory=await indexDirectory();
|
|
142
|
+
const path=join(directory,scopeKey(service,hashPrivateRequest(input.request))+'.json');
|
|
143
|
+
const saved=await json(path,32768);validOwner(saved);
|
|
144
|
+
const unknown=await json(join(directory,'unclassified.json'),32768);validOwner(unknown);
|
|
145
|
+
if(!force && !(saved?.deliberate && saved.dataDir===dataDir) && (unknown?.conflicts || (unknown && unknown.dataDir!==dataDir)))
|
|
146
|
+
throw codeError('REQUEST_HISTORY_REVIEW_REQUIRED','Another installation has older requests whose original input cannot be identified. Review them in the original installation. Use --force only if you deliberately want a new purchase.');
|
|
147
|
+
if(!force && saved?.conflicts)
|
|
148
|
+
throw codeError('ORIGINAL_REQUEST_AMBIGUOUS','Several local installations contain this request. Return to the intended installation and use --resume with its saved request ID. No new purchase was started.');
|
|
149
|
+
if(!force && saved && saved.dataDir!==dataDir)
|
|
150
|
+
throw codeError('ORIGINAL_INSTALLATION_REQUIRED','This input was used by another local Wallet. Return to that installation to recover it. Use --force only for a deliberate new purchase.');
|
|
151
|
+
const next = !saved || force ? {version:1,dataDir,...(force?{deliberate:true}:{})} : saved;
|
|
152
|
+
if(!saved || force)await atomicWritePrivate(path,JSON.stringify(next)+'\n');
|
|
153
|
+
const result = await operation(!force ? next.requestIds : undefined);
|
|
154
|
+
if(result?.requestId) {
|
|
155
|
+
if(!requestIdPattern.test(result.requestId))throw review();
|
|
156
|
+
await atomicWritePrivate(path,JSON.stringify({...next,requestIds:[...new Set([...(next.requestIds || []),result.requestId])]})+'\n');
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export async function claimDefaultRequest(options) {
|
|
162
|
+
return withDefaultRequest(options,async()=>{});
|
|
163
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { publishRuntimeReady } from "./runtime-selection-ready.mjs";
|
|
2
|
+
import { acquireRuntimeOwner } from "./runtime-owner.mjs";
|
|
1
3
|
import { runtimeSoftware } from "./software-versions.mjs";
|
|
2
4
|
// Authenticated local invocation engine. Preserve durable request/payment checkpoints before
|
|
3
5
|
// network calls; startup reconciliation observes history without creating replacement purchases.
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
import { serviceUrl } from "./service-url.mjs";
|
|
16
18
|
import { createRuntimeRecovery } from "./runtime-recovery.mjs";
|
|
17
19
|
import { constants } from "node:fs";
|
|
20
|
+
import { dirname } from "node:path";
|
|
18
21
|
import { open } from "node:fs/promises";
|
|
19
22
|
import { assertOrderCanContinue } from "./order-closure.mjs";
|
|
20
23
|
import { invocationResults } from "./invocation-result.mjs";
|
|
@@ -37,6 +40,9 @@ import { fetchJsonWithTimeout } from "./transport.mjs";
|
|
|
37
40
|
import { readFinalizationDisclosure } from "./finalization-disclosure.mjs";
|
|
38
41
|
import { walletIpcError } from "./wallet-ipc-error.mjs";
|
|
39
42
|
|
|
43
|
+
// Retain the FileHandle for the entire server lifetime; process exit releases it.
|
|
44
|
+
const runtimeOwner = await acquireRuntimeOwner();
|
|
45
|
+
process.once('exit', () => { void runtimeOwner; });
|
|
40
46
|
const host = String(process.env.AGENTPAY_USER_HOST || "127.0.0.1").trim();
|
|
41
47
|
if (!["127.0.0.1", "localhost", "::1"].includes(host))
|
|
42
48
|
throw Error("User Runtime must listen on loopback");
|
|
@@ -785,6 +791,9 @@ const server = createServer(async (req, res) => {
|
|
|
785
791
|
});
|
|
786
792
|
|
|
787
793
|
server.listen(port, host, () => {
|
|
794
|
+
void publishRuntimeReady(dirname(dirname(callerCredentialFile))).catch(error=>{
|
|
795
|
+
console.error('Default Runtime connection could not be refreshed:',error.message);
|
|
796
|
+
});
|
|
788
797
|
process.stdout.write(
|
|
789
798
|
`${JSON.stringify({
|
|
790
799
|
ready: true,
|
|
@@ -5,6 +5,7 @@ import { rememberUserDataDir } from "./local-data-directory.mjs";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { runInstall } from "./install-user.mjs";
|
|
8
|
+
import { activateSetup } from './setup-service-selection.mjs';
|
|
8
9
|
|
|
9
10
|
export async function installAndSetup({
|
|
10
11
|
userDir,
|
|
@@ -13,9 +14,13 @@ export async function installAndSetup({
|
|
|
13
14
|
install = runInstall,
|
|
14
15
|
start = startGuide,
|
|
15
16
|
remember = rememberUserDataDir,
|
|
17
|
+
activate = activateSetup,
|
|
16
18
|
write = console.log,
|
|
17
19
|
}) {
|
|
18
|
-
const result = await install({ userDir, market, dataDir, write
|
|
20
|
+
const result = await install({ userDir, market, dataDir, write,
|
|
21
|
+
beforeApplySettings: () => activate({dataDir,write}),
|
|
22
|
+
});
|
|
23
|
+
if (result?.cancelled) return result.exitCode;
|
|
19
24
|
if (!result?.completed)
|
|
20
25
|
throw Error(
|
|
21
26
|
"Installation did not complete. Run the same Install command to resume.",
|