@c4roa0/agentpay-user 0.3.3 → 0.3.5
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 +14 -1
- package/doc/architecture.md +17 -0
- package/doc/records/local-npm-update-2026-11-08.md +4 -0
- package/doc/runbooks/market-connection.md +37 -0
- package/package.json +1 -1
- package/progress.md +48 -0
- package/src/connect-market.mjs +55 -11
- package/src/order-cancellation.mjs +2 -1
- package/src/platform-endpoints.mjs +138 -0
- package/src/settlement-address-history.mjs +28 -0
- package/src/status-panel.mjs +20 -2
- package/update-pair.json +1 -1
package/doc/TODO.md
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
# User TODO
|
|
2
2
|
|
|
3
|
+
## Existing Market address update
|
|
4
|
+
|
|
5
|
+
- [x] Allow source-to-npm reconnect without an onboarding draft; verify Policy contracts and preserve missing files.
|
|
6
|
+
- [ ] Publish 0.3.5 and retry the participant address update.
|
|
7
|
+
|
|
8
|
+
- [x] Reauthenticate and verify original account, Wallet tuple and Policy connection before replacing an address.
|
|
9
|
+
- [x] Verify unchanged deployment; preserve RPC, key, Policy, history/cache and original cancellation IDs.
|
|
10
|
+
- [x] Apply under installer lock, restart managed Runtime, retain retry marker and show incomplete update in status.
|
|
11
|
+
- [x] Full regression (387 checks), followed by targeted reconnect-boundary/transaction checks.
|
|
12
|
+
- [x] Publish User 0.3.4 through v0.3.4; workflow and independent registry integrity verification passed.
|
|
13
|
+
- [ ] Verify the existing participant installation and explicit address update against HTTPS.
|
|
14
|
+
|
|
3
15
|
## Directory npm update, then HTTPS
|
|
4
16
|
|
|
5
17
|
- [x] Support directory-local npm update without replacing source/link installs.
|
|
6
18
|
- [x] Verify real npm directory upgrade and isolated preflight; nine regression checks passed.
|
|
7
|
-
- [
|
|
19
|
+
- [x] Publish 0.3.3 via v0.3.3 and independently verify registry archive/dependency pins.
|
|
20
|
+
- [ ] Verify participant directory install/update.
|
|
8
21
|
|
|
9
22
|
## Tag-triggered publication
|
|
10
23
|
|
package/doc/architecture.md
CHANGED
|
@@ -98,3 +98,20 @@ required for the local test suite.
|
|
|
98
98
|
Tests use public interfaces and controlled remote responses. The explicit fault
|
|
99
99
|
loader only instruments a spawned test Runtime; production startup contains no
|
|
100
100
|
crash-test switch. See [test guide](https://github.com/c4rO-0/agentpay-user/blob/main/test/README.md) for evidence limits.
|
|
101
|
+
|
|
102
|
+
## Explicit platform address changes
|
|
103
|
+
|
|
104
|
+
`connect-market.mjs` reauthenticates at the user-selected origin and checks the
|
|
105
|
+
original account, Wallet tuple and exact Policy connection ID. Public configuration
|
|
106
|
+
is not a Market identity proof. `platform-endpoints.mjs` compares saved deployment
|
|
107
|
+
and bound Policy with the new configuration, probes Settlement's existing public
|
|
108
|
+
readiness projection, then changes only transport URLs after consent. No new API
|
|
109
|
+
or platform identity key is introduced.
|
|
110
|
+
|
|
111
|
+
The shared installer lock serializes the local apply step. Only the managed Runtime
|
|
112
|
+
unit is stopped; its listener must be gone before writes. Private atomic files and
|
|
113
|
+
a pending marker allow retry after interruption. Wallet and Order files are never
|
|
114
|
+
rewritten. `settlement-address-history.mjs` preserves exact directed, Policy-scoped
|
|
115
|
+
address mappings so cancellation retries retain their immutable original IDs.
|
|
116
|
+
Runtime recovery observations remain tagged by source and are rechecked instead of
|
|
117
|
+
being relabelled. The pending marker is surfaced by status.
|
|
@@ -26,3 +26,7 @@ User 0.3.3 release preparation (2026-11-09): all 383 behavior checks and standal
|
|
|
26
26
|
archive installation with isolated Wallet/Runtime process checks passed. Wallet
|
|
27
27
|
0.3.2, Protocol 0.14.3 and official package pin are unchanged. Publication and
|
|
28
28
|
participant acceptance remain separate gates.
|
|
29
|
+
|
|
30
|
+
Publication verified on 2026-11-09: tag v0.3.3 at c5f3be1, GitHub run
|
|
31
|
+
42567636609. Public npm archive matches the final local release archive byte for
|
|
32
|
+
byte; provenance metadata is present. Participant acceptance remains pending.
|
|
@@ -16,6 +16,43 @@ Review the account and nickname, then type `confirm`. Use `details` to inspect t
|
|
|
16
16
|
Safe, network and Policy. Open Market → Account to view your Wallets and Orders.
|
|
17
17
|
A separate browser login may be needed; terminal sign-in does not create browser cookies.
|
|
18
18
|
|
|
19
|
+
## Change the address of the same Market
|
|
20
|
+
|
|
21
|
+
In a version containing the address-update feature, run:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
agentpay market connect --market https://YOUR_NEW_MARKET_ADDRESS
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For a folder installation, use `./node_modules/.bin/agentpay` instead of `agentpay`.
|
|
28
|
+
Sign in again at the address you trust. User checks that the account ID and the
|
|
29
|
+
original Wallet/Policy connection are still present. The existing nickname is
|
|
30
|
+
reused. It also checks the network, contracts and Settlement deployment before
|
|
31
|
+
showing the new platform addresses for confirmation.
|
|
32
|
+
|
|
33
|
+
Confirm to update the saved Market address and, if changed, the Settlement and
|
|
34
|
+
package download addresses. Runtime pauses briefly and restarts; Wallet remains
|
|
35
|
+
unlocked. Keys, Policy, Orders, input/output caches, startup preference and the
|
|
36
|
+
participant's RPC address stay unchanged. No paid test runs automatically.
|
|
37
|
+
|
|
38
|
+
A different account, missing original connection or changed contracts stops the
|
|
39
|
+
address update. This is a deliberate address change, not automatic discovery or
|
|
40
|
+
cryptographic proof of server identity. Check the website address yourself; the
|
|
41
|
+
terminal never forwards a previously saved login token to the new address.
|
|
42
|
+
|
|
43
|
+
If interrupted, repeat the same command with the same address. `status` reports
|
|
44
|
+
an incomplete update until it finishes. If a killed process leaves an installation
|
|
45
|
+
lock, confirm no installer is running before following the lock-review guidance;
|
|
46
|
+
User does not break an unknown live lock automatically. Existing cancellation requests keep their
|
|
47
|
+
original IDs; recovery checks refresh observations from the new Settlement.
|
|
48
|
+
Keep any old SSH tunnels until the update succeeds and `status` confirms operation.
|
|
49
|
+
This feature requires User 0.3.5 or newer. Updating software does not change
|
|
50
|
+
your saved addresses; run the command above to review the change.
|
|
51
|
+
|
|
52
|
+
Older source installations may leave onboarding files outside the npm package.
|
|
53
|
+
Address updates use the saved Wallet Policy and Runtime settings; they do not
|
|
54
|
+
require or recreate an old onboarding draft.
|
|
55
|
+
|
|
19
56
|
## Wallet identity and Policy access
|
|
20
57
|
|
|
21
58
|
A Wallet is identified by network + Module + Safe + execution address. Its nickname
|
package/package.json
CHANGED
package/progress.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# AgentPay User progress
|
|
2
2
|
|
|
3
|
+
## Missing onboarding draft during npm address update — 2026-11-09
|
|
4
|
+
|
|
5
|
+
Participant 0.3.4 reconnect stopped before address writes because an old onboarding
|
|
6
|
+
file was absent. Address updates now validate the saved Policy and service settings
|
|
7
|
+
without requiring that draft; existing draft/public files are updated only if
|
|
8
|
+
present. No RPC, Policy, test configuration or private history is synthesized.
|
|
9
|
+
389 full regressions passed, followed by targeted error-guidance checks. Missing
|
|
10
|
+
drafts still reject contract drift; preflight errors explain the stage, preserved
|
|
11
|
+
state and retry action, while interrupted writes warn about partial updates.
|
|
12
|
+
User 0.3.5 publication is pending;
|
|
13
|
+
participant migration remains unverified.
|
|
14
|
+
|
|
15
|
+
## User 0.3.4 published — 2026-11-09
|
|
16
|
+
|
|
17
|
+
Release the explicit Market address update with unchanged Wallet 0.3.2 and
|
|
18
|
+
Protocol 0.14.3 pins. No participant data or running services are changed by
|
|
19
|
+
publication. Independent npm archive installation and real isolated local-service
|
|
20
|
+
startup passed; all 388 release regressions passed. Tag v0.3.4 identifies c63ff80.
|
|
21
|
+
GitHub run 42574859894 completed OIDC publication and registry verification.
|
|
22
|
+
Independent registry integrity comparison matched the reviewed local archive.
|
|
23
|
+
Participant address migration remains pending.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Explicit Market address update — 2026-11-09
|
|
27
|
+
|
|
28
|
+
Approved reduced scope: reuse login, Wallet list and public deployment interfaces;
|
|
29
|
+
no Market identity key or new backend endpoint. User-selected address changes must
|
|
30
|
+
match the original account/Wallet/Policy connection and deployment. Consent updates
|
|
31
|
+
transport URLs, restarts only managed Runtime and preserves key/Policy/Orders/cache.
|
|
32
|
+
Policy-scoped aliases retain cancellation IDs; interrupted apply has a retry marker
|
|
33
|
+
and status guidance. Controlled address/connection/cancellation/status checks pass;
|
|
34
|
+
387 full regression checks passed; subsequent reconnect-boundary/transaction
|
|
35
|
+
changes passed targeted checks. npm publication and participant acceptance remain
|
|
36
|
+
pending. Only explicit origin changes or interrupted updates enter this path;
|
|
37
|
+
ordinary reconnect/renewal does not acquire new configuration dependencies.
|
|
38
|
+
No participant settings have been modified.
|
|
39
|
+
|
|
40
|
+
## User 0.3.3 published — 2026-11-09
|
|
41
|
+
|
|
42
|
+
Tag v0.3.3 identifies c5f3be1. GitHub run 42567636609 passed behavior, standalone
|
|
43
|
+
archive and OIDC publication steps. Independent public-registry verification
|
|
44
|
+
confirmed exact Wallet 0.3.2 / Protocol 0.14.3 dependencies, provenance metadata
|
|
45
|
+
and byte-identical archive (SHA-256
|
|
46
|
+
aa231297a4690117850f5d50414d89ad0da86b29c29d03947383c23bfa62a833).
|
|
47
|
+
Participant directory-update acceptance remains pending. Existing directory installs
|
|
48
|
+
older than 0.3.3 need one npm install of 0.3.3 and repair before using local update.
|
|
49
|
+
HTTPS endpoint migration is not included.
|
|
50
|
+
|
|
3
51
|
## User 0.3.3 release candidate — 2026-11-09
|
|
4
52
|
|
|
5
53
|
Release directory-local npm updates, keeping exact Wallet 0.3.2 and Protocol
|
package/src/connect-market.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { checkServiceDirectory } from "./local-services.mjs";
|
|
|
13
13
|
import { localWalletAdmin } from "./local-wallet-ipc.mjs";
|
|
14
14
|
import { repairMarketWallet } from "./repair-market-wallet.mjs";
|
|
15
15
|
import { controlWallet } from "./wallet-control.mjs";
|
|
16
|
+
import { preparePlatformEndpoints } from "./platform-endpoints.mjs";
|
|
16
17
|
import { fetchJsonWithTimeout } from "./transport.mjs";
|
|
17
18
|
export function marketOrigin(value) {
|
|
18
19
|
const u = new URL(value);
|
|
@@ -129,6 +130,7 @@ export async function connectMarket(
|
|
|
129
130
|
save = saveConnection,
|
|
130
131
|
repair = repairMarketWallet,
|
|
131
132
|
unlock = controlWallet,
|
|
133
|
+
prepareEndpoints = preparePlatformEndpoints,
|
|
132
134
|
} = {},
|
|
133
135
|
) {
|
|
134
136
|
let token, origin, completion;
|
|
@@ -194,6 +196,8 @@ export async function connectMarket(
|
|
|
194
196
|
return 0;
|
|
195
197
|
}
|
|
196
198
|
origin = marketOrigin(answer.trim() || previous?.marketOrigin);
|
|
199
|
+
write("Checking Market and platform addresses…");
|
|
200
|
+
const changingOrigin = !!previous?.marketOrigin && marketOrigin(previous.marketOrigin) !== origin;
|
|
197
201
|
const call = async (path, body, auth = true) => {
|
|
198
202
|
const { response, result } = await fetchJson(
|
|
199
203
|
origin + "/market" + path,
|
|
@@ -244,6 +248,8 @@ export async function connectMarket(
|
|
|
244
248
|
token = signedIn.sessionToken;
|
|
245
249
|
if (typeof token !== "string" || !token || !signedIn.developer?.id)
|
|
246
250
|
throw Error("Market returned an invalid sign-in response.");
|
|
251
|
+
if (changingOrigin && previous.accountId !== signedIn.developer.id)
|
|
252
|
+
throw Error("This is not your saved Market account. The previous connection and addresses were kept.");
|
|
247
253
|
write(
|
|
248
254
|
"\n------------------------------------------------------------\n2/3 Name your Wallet\n------------------------------------------------------------",
|
|
249
255
|
);
|
|
@@ -257,6 +263,33 @@ export async function connectMarket(
|
|
|
257
263
|
String(w[key]).toLowerCase() === scope[key].toLowerCase(),
|
|
258
264
|
),
|
|
259
265
|
);
|
|
266
|
+
// These authenticated record checks detect accidental cross-Market moves.
|
|
267
|
+
// They are not a cryptographic server identity; the user chooses the URL.
|
|
268
|
+
const originalConnectionMatches = !!previous?.connection?.id && !!existing &&
|
|
269
|
+
["network", "module", "safe", "delegate"].every(key =>
|
|
270
|
+
typeof previous.connection[key] === "string" &&
|
|
271
|
+
previous.connection[key].toLowerCase() === String(existing[key]).toLowerCase()) &&
|
|
272
|
+
existing.policies?.some(p => p.id === previous.connection.id &&
|
|
273
|
+
String(p.policyId).toLowerCase() === String(previous.connection.policyId).toLowerCase());
|
|
274
|
+
if (changingOrigin && !originalConnectionMatches)
|
|
275
|
+
throw Error("Your original Wallet connection was not found at this address. The previous connection and addresses were kept.");
|
|
276
|
+
let endpoints;
|
|
277
|
+
try {
|
|
278
|
+
endpoints = previous ? await prepareEndpoints({dataDir,origin,scope,fetchJson,changingOrigin}) : {changed:false,apply:async()=>{}};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
const networkFailure = ["AbortError", "TimeoutError"].includes(error.name) ||
|
|
281
|
+
["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "ETIMEDOUT"].includes(error.code || error.cause?.code) || error.message === "fetch failed";
|
|
282
|
+
const reason = networkFailure
|
|
283
|
+
? "Could not reach the platform to check its settings. Check your network and the Market address."
|
|
284
|
+
: error.message;
|
|
285
|
+
write("\n[!] Platform address check stopped");
|
|
286
|
+
write(reason);
|
|
287
|
+
write("No local settings were changed by this check. Your Wallet and Order records are kept.");
|
|
288
|
+
write("Next: resolve the issue above, then retry this command.");
|
|
289
|
+
return 1;
|
|
290
|
+
}
|
|
291
|
+
if (endpoints.changed && (previous?.accountId !== signedIn.developer.id || !originalConnectionMatches))
|
|
292
|
+
throw Error("Platform addresses changed, but your original account and Wallet connection could not be confirmed. Current addresses were kept.");
|
|
260
293
|
if (existing) write(`Using your Wallet name: ${existing.label}`);
|
|
261
294
|
else
|
|
262
295
|
write(
|
|
@@ -307,6 +340,10 @@ export async function connectMarket(
|
|
|
307
340
|
write(
|
|
308
341
|
"View this Wallet’s Orders and request help on Market. No spending permission is added.",
|
|
309
342
|
);
|
|
343
|
+
if (endpoints.changed) write(`Also update platform addresses:
|
|
344
|
+
Settlement: ${endpoints.urls.settlementUrl}
|
|
345
|
+
Downloads: ${endpoints.urls.marketBundleMetadataUrl}
|
|
346
|
+
Runtime will restart; your spending permission stays the same.`);
|
|
310
347
|
while (true) {
|
|
311
348
|
const choice = (
|
|
312
349
|
await ask(
|
|
@@ -345,17 +382,24 @@ export async function connectMarket(
|
|
|
345
382
|
throw Error("Connection execution address differs from this Wallet.");
|
|
346
383
|
if (result.connection.label !== label)
|
|
347
384
|
throw Error("Connection nickname differs. Check Market Settings.");
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
385
|
+
if (changingOrigin && previous.connection.policyId === scope.policyId &&
|
|
386
|
+
result.connection.id !== previous.connection.id)
|
|
387
|
+
throw Error("Market returned a different connection record. The previous local connection was kept.");
|
|
388
|
+
const saveConnected = () => save(dataDir, {
|
|
389
|
+
version: 1,
|
|
390
|
+
marketOrigin: origin,
|
|
391
|
+
accountId: signedIn.developer.id,
|
|
392
|
+
connection: result.connection,
|
|
393
|
+
});
|
|
394
|
+
if (endpoints.changed) {
|
|
395
|
+
await endpoints.apply({write,saveConnection:saveConnected});
|
|
396
|
+
} else {
|
|
397
|
+
try { await saveConnected(); }
|
|
398
|
+
catch {
|
|
399
|
+
if (changingOrigin)
|
|
400
|
+
throw Error("The updated Market connection could not be saved. Retry the same command.");
|
|
401
|
+
write("Market connected this Wallet, but the local shortcut could not be saved. Use --market URL next time.");
|
|
402
|
+
}
|
|
359
403
|
}
|
|
360
404
|
completion = `\nWallet connected: ${label}\nView this Wallet’s Orders in Market → Account → My Orders.\nManage or disconnect it in Market Settings. This guide has finished.`;
|
|
361
405
|
return 0;
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { sameSettlementAddress } from "./settlement-address-history.mjs";
|
|
4
5
|
import { isUnpaidExpiry } from "./order-closure.mjs";
|
|
5
6
|
import { randomBytes, createHash } from "node:crypto";
|
|
6
7
|
import { constants } from "node:fs";
|
|
@@ -109,7 +110,7 @@ async function readIntent(ctx) {
|
|
|
109
110
|
!B32.test(intent.cancellationId || "") ||
|
|
110
111
|
intent.orderId !== ctx.orderId ||
|
|
111
112
|
intent.policyId !== ctx.policyId ||
|
|
112
|
-
intent.settlementUrl
|
|
113
|
+
!(await sameSettlementAddress(ctx.dataDir,intent.settlementUrl,ctx.settlementUrl,ctx.policyId)) ||
|
|
113
114
|
!Number.isSafeInteger(intent.createdAtMs) ||
|
|
114
115
|
intent.createdAtMs < 0
|
|
115
116
|
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
// Deliberate transport changes only. Never replace a Policy, request, or result.
|
|
3
|
+
import {open,mkdir,lstat,rename,unlink,readFile} from 'node:fs/promises';
|
|
4
|
+
import {constants} from 'node:fs';
|
|
5
|
+
import {join,dirname} from 'node:path';
|
|
6
|
+
import {homedir} from 'node:os';
|
|
7
|
+
import {randomUUID} from 'node:crypto';
|
|
8
|
+
import {execFile} from 'node:child_process';
|
|
9
|
+
import {promisify} from 'node:util';
|
|
10
|
+
import {installedUserDir} from './local-data-directory.mjs';
|
|
11
|
+
import {deploymentFilePath,installationDirectory} from './installation-paths.mjs';
|
|
12
|
+
import {readPrivateJson,deploymentChanges} from './deployment-status.mjs';
|
|
13
|
+
import {validatePublicDeployment} from './prepare-onboarding.mjs';
|
|
14
|
+
import {readServiceSettings,checkServiceDirectory,listenerPresent} from './local-services.mjs';
|
|
15
|
+
import {isManagedUnit} from './autostart-files.mjs';
|
|
16
|
+
import {fetchJsonWithTimeout} from './transport.mjs';
|
|
17
|
+
import {settlementProfileBlocker} from './readiness-profile.mjs';
|
|
18
|
+
import {readRuntimeStatus} from './runtime-status.mjs';
|
|
19
|
+
import {readSettlementAddresses} from './settlement-address-history.mjs';
|
|
20
|
+
|
|
21
|
+
const execute=promisify(execFile);
|
|
22
|
+
const normalize=value=>new URL(value).href.replace(/\/+$/,'');
|
|
23
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
24
|
+
const pendingPath=dir=>join(dir,'runtime/endpoint-update.pending.json');
|
|
25
|
+
const aliasesPath=dir=>join(dir,'runtime/settlement-addresses.json');
|
|
26
|
+
|
|
27
|
+
async function durableJson(path,value){
|
|
28
|
+
const temp=path+'.'+randomUUID()+'.tmp';let file;
|
|
29
|
+
try{
|
|
30
|
+
file=await open(temp,'wx',0o600);await file.writeFile(JSON.stringify(value)+'\n');await file.sync();await file.close();file=null;
|
|
31
|
+
await rename(temp,path);
|
|
32
|
+
const directory=await open(dirname(path),constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW);
|
|
33
|
+
try{await directory.sync();}finally{await directory.close();}
|
|
34
|
+
}finally{await file?.close();await unlink(temp).catch(e=>{if(e.code!=='ENOENT')throw e;});}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async function runtimeControl(action,{dataDir,settings}){
|
|
39
|
+
if(process.platform!=='linux')throw Error('Automatic address updates currently require managed Linux services. Keep your saved settings.');
|
|
40
|
+
const unit='agentpay-local-runtime.service',path=join(homedir(),'.config/systemd/user',unit);
|
|
41
|
+
const stat=await lstat(path);
|
|
42
|
+
if(!stat.isFile()||stat.isSymbolicLink()||stat.uid!==process.getuid()||(stat.mode&0o022)||!isManagedUnit(await readFile(path,'utf8'),unit,dataDir))
|
|
43
|
+
throw Error('Runtime is not managed by this User installation. Run agentpay repair before updating addresses.');
|
|
44
|
+
const ctl=async args=>(await execute('systemctl',['--user',...args],{timeout:45000,maxBuffer:16384})).stdout.trim();
|
|
45
|
+
if(await ctl(['show',unit,'--property=FragmentPath','--value'])!==path||await ctl(['show',unit,'--property=DropInPaths','--value']))
|
|
46
|
+
throw Error('Runtime service settings need review before updating addresses.');
|
|
47
|
+
await ctl([action,unit]);
|
|
48
|
+
if(action==='stop'){
|
|
49
|
+
const url=new URL(settings.runtimeUrl);
|
|
50
|
+
if(await listenerPresent({host:url.hostname,port:Number(url.port)}))throw Error('Runtime is still running. No connection settings were changed.');
|
|
51
|
+
}else{
|
|
52
|
+
for(let i=0;i<30;i++){
|
|
53
|
+
try{await readRuntimeStatus(dataDir);return;}catch{await new Promise(r=>setTimeout(r,500));}
|
|
54
|
+
}
|
|
55
|
+
throw Error('Runtime did not become ready. Retry agentpay market connect with the same Market address.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function preparePlatformEndpoints({dataDir,origin,scope,userDir=installedUserDir,fetchJson=fetchJsonWithTimeout,changingOrigin=false}){
|
|
60
|
+
await checkServiceDirectory(dataDir);
|
|
61
|
+
const pending=await readPrivateJson(pendingPath(dataDir));
|
|
62
|
+
// Ordinary reconnect/renewal keeps its existing behavior. Only a deliberately
|
|
63
|
+
// changed origin or an interrupted update owns transport changes.
|
|
64
|
+
if(!changingOrigin&&!pending)return {changed:false,apply:async()=>{}};
|
|
65
|
+
const setup=await readPrivateJson(await deploymentFilePath(dataDir,userDir,'anvil-onboarding.json'));
|
|
66
|
+
// A source-to-npm installation may have no copied onboarding draft. The
|
|
67
|
+
// bound Policy and live service settings remain the authoritative local state.
|
|
68
|
+
// Never synthesize a draft or replace its RPC/test configuration from Market.
|
|
69
|
+
const policy=await readPrivateJson(join(dataDir,'wallet/policy.json'));
|
|
70
|
+
if(!policy)throw Error('Saved spending permission is missing (wallet/policy.json). Check your data folder with agentpay wallet info and restore its backup before retrying. Do not create a replacement Policy to fix this.');
|
|
71
|
+
if(!policy||policy.policyId?.toLowerCase()!==scope.policyId?.toLowerCase()||policy.safe?.toLowerCase()!==scope.safe?.toLowerCase()||`eip155:${policy.chainId}`!==scope.network||policy.module?.toLowerCase()!==scope.module?.toLowerCase())
|
|
72
|
+
throw Error('Wallet authorization changed. Run agentpay market connect again.');
|
|
73
|
+
const connectionBefore=await readPrivateJson(join(dataDir,'market-connection.json'));
|
|
74
|
+
if(pending&&(pending.version!==1||pending.origin!==origin||pending.policyId!==policy.policyId))
|
|
75
|
+
throw Error('Finish the pending address update with the same Policy. Retry: agentpay market connect --market '+pending.origin);
|
|
76
|
+
const {response,result}=await fetchJson(origin+'/market/web/onboarding/user-config',{method:'GET'},15000);
|
|
77
|
+
if(!response.ok)throw Error('Market deployment settings are unavailable. Check the website address and your connection, then retry.');
|
|
78
|
+
const proposed=validatePublicDeployment(result);
|
|
79
|
+
let settings;
|
|
80
|
+
try{settings=await readServiceSettings(dataDir);}catch(e){
|
|
81
|
+
if(e.code==='ENOENT')throw Error('Runtime settings are missing (runtime/local-services.json). Run agentpay repair, then retry this command.');
|
|
82
|
+
throw Error('Runtime settings could not be read safely. Check the selected data folder with agentpay wallet info before retrying.');
|
|
83
|
+
}
|
|
84
|
+
const urls={settlementUrl:proposed.settlementUrl,marketBundleMetadataUrl:proposed.marketBundleMetadataUrl};
|
|
85
|
+
const changed=!!setup&&Object.keys(urls).some(key=>normalize(setup[key])!==normalize(urls[key]))||normalize(settings.settlementUrl)!==normalize(urls.settlementUrl)||!!pending;
|
|
86
|
+
// RPC is participant-owned: a different local tunnel must never be overwritten.
|
|
87
|
+
// A Market connection alone is not authority to rotate contracts or test packages.
|
|
88
|
+
const publicPath=await deploymentFilePath(dataDir,userDir,'user-deployment.json');
|
|
89
|
+
const previousPublic=await readPrivateJson(publicPath);
|
|
90
|
+
if(!changed&&!changingOrigin)return {changed:false,apply:async()=>{}};
|
|
91
|
+
if(deploymentChanges(policy,proposed).length||(setup&&(deploymentChanges(setup,proposed).length||setup.mode!==proposed.mode||setup.tokenDecimals!==proposed.tokenDecimals||setup.tokenSymbol!==proposed.tokenSymbol)))
|
|
92
|
+
throw Error('This Market uses different network or contract settings. Address update stopped; your current Policy and Orders are kept.');
|
|
93
|
+
|
|
94
|
+
// Query an unknown service to inspect deployment identity without depending on
|
|
95
|
+
// a particular Provider being online, or granting it an Order.
|
|
96
|
+
const query=new URL(normalize(urls.settlementUrl)+'/agentpay/purchase-readiness');
|
|
97
|
+
query.searchParams.set('skillId','0x'+'0'.repeat(64));query.searchParams.set('profileHash','0x'+'0'.repeat(64));
|
|
98
|
+
const readiness=await fetchJson(query.href,{method:'GET'},15000);
|
|
99
|
+
if(!readiness.response.ok||!readiness.result?.ok||settlementProfileBlocker(policy,readiness.result.result?.settlementProfile))
|
|
100
|
+
throw Error('The new Settlement address does not match this Wallet deployment. Current addresses are kept.');
|
|
101
|
+
const metadata=await fetchJson(urls.marketBundleMetadataUrl,{method:'GET'},15000);
|
|
102
|
+
if(!metadata.response.ok||!metadata.result?.ok||!/^[a-f0-9]{64}$/.test(metadata.result.result?.sha256||''))
|
|
103
|
+
throw Error('The new package address could not be checked. Current addresses are kept.');
|
|
104
|
+
if(!changed)return {changed:false,apply:async()=>{}};
|
|
105
|
+
return {changed:true,urls,async apply({write=console.log,manage=runtimeControl,stateDir=installationDirectory(),saveConnection=async()=>{}}={}){
|
|
106
|
+
await mkdir(stateDir,{recursive:true,mode:0o700});
|
|
107
|
+
const stat=await lstat(stateDir);
|
|
108
|
+
if(!stat.isDirectory()||stat.isSymbolicLink()||stat.uid!==process.getuid()||(stat.mode&0o077))throw Error('Unsafe installation state directory.');
|
|
109
|
+
const lockPath=join(stateDir,'install.lock');let lock;
|
|
110
|
+
try{lock=await open(lockPath,'wx',0o600);}catch(e){if(e.code==='EEXIST')throw Error('Another setup or update is running. Retry market connect when it finishes.');throw e;}
|
|
111
|
+
let updating=false;
|
|
112
|
+
try{
|
|
113
|
+
if(!same(await readPrivateJson(join(dataDir,'market-connection.json')),connectionBefore)||!same(await readPrivateJson(join(dataDir,'wallet/policy.json')),policy)||!same(await readServiceSettings(dataDir),settings)||!same(await readPrivateJson(publicPath),previousPublic)||!same(await readPrivateJson(await deploymentFilePath(dataDir,userDir,'anvil-onboarding.json')),setup))
|
|
114
|
+
throw Error('Local settings changed during connection. Retry market connect; no addresses were changed.');
|
|
115
|
+
const aliases=await readSettlementAddresses(dataDir);
|
|
116
|
+
const entry={from:normalize(settings.settlementUrl),to:normalize(urls.settlementUrl),policyId:policy.policyId};
|
|
117
|
+
if(entry.from!==entry.to&&!aliases.entries.some(e=>same(e,entry))){
|
|
118
|
+
if(aliases.entries.length>=256)throw Error('Settlement address history needs review.');
|
|
119
|
+
aliases.entries.push(entry);
|
|
120
|
+
}
|
|
121
|
+
write('Updating platform addresses. Runtime will pause briefly; Wallet stays unlocked.');
|
|
122
|
+
// A durable marker makes interruptions retryable even after settings are written.
|
|
123
|
+
// Write the alias before the new active address; never mutate cancellation IDs.
|
|
124
|
+
await durableJson(pendingPath(dataDir),{version:1,origin,policyId:policy.policyId});
|
|
125
|
+
updating=true;
|
|
126
|
+
await saveConnection();
|
|
127
|
+
await manage('stop',{dataDir,settings});
|
|
128
|
+
await durableJson(aliasesPath(dataDir),aliases);
|
|
129
|
+
if(previousPublic)await durableJson(join(dataDir,'user-deployment.json'),{...previousPublic,...urls});
|
|
130
|
+
if(setup)await durableJson(join(dataDir,'anvil-onboarding.json'),{...setup,...urls});
|
|
131
|
+
await durableJson(join(dataDir,'runtime/local-services.json'),{...settings,settlementUrl:normalize(urls.settlementUrl)});
|
|
132
|
+
await manage('start',{dataDir,settings:{...settings,settlementUrl:normalize(urls.settlementUrl)}});
|
|
133
|
+
await unlink(pendingPath(dataDir));
|
|
134
|
+
write('Platform addresses updated. Your Wallet, permission and Order records were kept.');
|
|
135
|
+
}catch(e){throw Error('Address update did not finish.\n'+e.message+'\n'+(updating?'Some connection settings may already be saved. Wallet keys, Policies and Orders were kept.':'No connection addresses were changed.')+'\nNext: agentpay market connect --market '+origin);}
|
|
136
|
+
finally{await lock.close();await unlink(lockPath);}
|
|
137
|
+
}};
|
|
138
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
// Local aliases acknowledge an explicitly confirmed address change, not extra
|
|
3
|
+
// authority. Cancellation IDs and Policy scopes remain unchanged.
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {readPrivateFile} from './runtime-client.mjs';
|
|
6
|
+
const normalize=value=>{
|
|
7
|
+
const u=new URL(value);
|
|
8
|
+
if(u.username||u.password||u.search||u.hash||!(u.protocol==='https:'||u.protocol==='http:'&&['localhost','127.0.0.1','[::1]'].includes(u.hostname)))throw Error('Invalid saved Settlement address.');
|
|
9
|
+
return u.href.replace(/\/+$/,'');
|
|
10
|
+
};
|
|
11
|
+
export async function readSettlementAddresses(dataDir){
|
|
12
|
+
let saved;
|
|
13
|
+
try{saved=JSON.parse(await readPrivateFile(join(dataDir,'runtime/settlement-addresses.json'),65536));}
|
|
14
|
+
catch(e){if(e.code==='ENOENT')return {version:1,entries:[]};throw e;}
|
|
15
|
+
if(saved?.version!==1||!Array.isArray(saved.entries)||saved.entries.length>256)throw Error('Invalid saved Settlement address history.');
|
|
16
|
+
for(const e of saved.entries){
|
|
17
|
+
if(!e||Object.keys(e).sort().join(',')!=='from,policyId,to'||!/^0x[a-fA-F0-9]{64}$/.test(e.policyId)||normalize(e.from)!==e.from||normalize(e.to)!==e.to)throw Error('Invalid saved Settlement address history.');
|
|
18
|
+
}
|
|
19
|
+
return saved;
|
|
20
|
+
}
|
|
21
|
+
export async function sameSettlementAddress(dataDir,from,to,policyId){
|
|
22
|
+
if(normalize(from)===normalize(to))return true;
|
|
23
|
+
const saved=await readSettlementAddresses(dataDir),seen=new Set([normalize(from)]);
|
|
24
|
+
for(let i=0;i<saved.entries.length;i++)for(const entry of saved.entries){
|
|
25
|
+
if(entry.policyId.toLowerCase()===policyId.toLowerCase()&&seen.has(entry.from))seen.add(entry.to);
|
|
26
|
+
}
|
|
27
|
+
return seen.has(normalize(to));
|
|
28
|
+
}
|
package/src/status-panel.mjs
CHANGED
|
@@ -118,6 +118,16 @@ export async function readMarketStatus(dataDir) {
|
|
|
118
118
|
await h?.close();
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
const endpointUpdate = await saved("runtime/endpoint-update.pending.json");
|
|
122
|
+
if (endpointUpdate) {
|
|
123
|
+
let origin;
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(endpointUpdate.origin);
|
|
126
|
+
if (url.origin === endpointUpdate.origin &&
|
|
127
|
+
(url.protocol === "https:" || url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))) origin = url.origin;
|
|
128
|
+
} catch {}
|
|
129
|
+
return {state:"address_pending", ...(origin ? {origin} : {})};
|
|
130
|
+
}
|
|
121
131
|
const receipt = await saved("market-connection.json");
|
|
122
132
|
if (!receipt) return { state: "none" };
|
|
123
133
|
const policy = await saved("wallet/policy.json"),
|
|
@@ -409,7 +419,7 @@ export function renderStatusPanel(
|
|
|
409
419
|
next =
|
|
410
420
|
"Keep services running. Check progress in the original Agent conversation. If continuing a supported request, keep its original identifier; never create a replacement purchase.";
|
|
411
421
|
}
|
|
412
|
-
const marketPending = ["pending", "unknown"].includes(model.market?.state);
|
|
422
|
+
const marketPending = ["pending", "unknown", "address_pending"].includes(model.market?.state);
|
|
413
423
|
if (marketPending && level === "ok") {
|
|
414
424
|
title =
|
|
415
425
|
model.market.state === "pending"
|
|
@@ -420,6 +430,12 @@ export function renderStatusPanel(
|
|
|
420
430
|
"Website access to this Policy is not confirmed. Local payments are unaffected.";
|
|
421
431
|
next = "agentpay market connect";
|
|
422
432
|
}
|
|
433
|
+
if (model.market?.state === "address_pending") {
|
|
434
|
+
title = "Platform address update incomplete";
|
|
435
|
+
level = "wait";
|
|
436
|
+
detail = "Retry your Market connection to finish updating addresses. Original Order records are kept.";
|
|
437
|
+
next = "agentpay market connect" + (model.market.origin ? " --market " + model.market.origin : "");
|
|
438
|
+
}
|
|
423
439
|
const divider = "-".repeat(Math.min(60, width));
|
|
424
440
|
const lines = [
|
|
425
441
|
"",
|
|
@@ -524,7 +540,9 @@ export function renderStatusPanel(
|
|
|
524
540
|
lines.push("", "> Market");
|
|
525
541
|
const state = model.market.state;
|
|
526
542
|
lines.push(
|
|
527
|
-
state === "
|
|
543
|
+
state === "address_pending"
|
|
544
|
+
? ` ${badge("wait")} Platform address update incomplete`
|
|
545
|
+
: state === "saved"
|
|
528
546
|
? ` ${badge("ok")} Current Policy connection saved locally`
|
|
529
547
|
: state === "none"
|
|
530
548
|
? " Not connected from this installation"
|