@envarai/envarpay 0.2.0-alpha.8 → 2026.1025.1
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 +18 -56
- package/dist/commerce/agent-setup.d.ts +19 -0
- package/dist/commerce/agent-setup.js +210 -0
- package/dist/commerce/alerts.d.ts +5 -15
- package/dist/commerce/alerts.js +21 -77
- package/dist/commerce/card.js +1 -1
- package/dist/commerce/cli.js +74 -137
- package/dist/commerce/config.d.ts +1 -4
- package/dist/commerce/config.js +5 -81
- package/dist/commerce/configure.d.ts +2 -0
- package/dist/commerce/configure.js +78 -0
- package/dist/commerce/discovery.d.ts +20 -0
- package/dist/commerce/discovery.js +162 -0
- package/dist/commerce/envar-balance.d.ts +25 -0
- package/dist/commerce/envar-balance.js +65 -0
- package/dist/commerce/envar-runtime.d.ts +1 -0
- package/dist/commerce/envar-runtime.js +7 -0
- package/dist/commerce/envar.d.ts +13 -31
- package/dist/commerce/envar.js +35 -83
- package/dist/commerce/external-agent.d.ts +26 -0
- package/dist/commerce/external-agent.js +129 -0
- package/dist/commerce/external-events.d.ts +76 -0
- package/dist/commerce/external-events.js +273 -0
- package/dist/commerce/external-mcp.d.ts +10 -0
- package/dist/commerce/external-mcp.js +199 -0
- package/dist/commerce/external-oauth.d.ts +12 -0
- package/dist/commerce/external-oauth.js +230 -0
- package/dist/commerce/external-runtime.d.ts +25 -0
- package/dist/commerce/external-runtime.js +244 -0
- package/dist/commerce/managed.d.ts +3 -0
- package/dist/commerce/managed.js +143 -0
- package/dist/commerce/native-inventory.d.ts +9 -0
- package/dist/commerce/native-inventory.js +75 -0
- package/dist/commerce/native-skills.d.ts +8 -0
- package/dist/commerce/native-skills.js +11 -8
- package/dist/commerce/openclaw-setup.d.ts +28 -0
- package/dist/commerce/openclaw-setup.js +43 -0
- package/dist/commerce/operations.d.ts +1 -2
- package/dist/commerce/operations.js +17 -20
- package/dist/commerce/pricing.d.ts +0 -1
- package/dist/commerce/pricing.js +1 -3
- package/dist/commerce/runtime.d.ts +2 -2
- package/dist/commerce/runtime.js +2 -2
- package/dist/commerce/server.d.ts +0 -1
- package/dist/commerce/server.js +2 -3
- package/dist/commerce/setup.d.ts +25 -0
- package/dist/commerce/setup.js +379 -0
- package/dist/commerce/store.d.ts +11 -1
- package/dist/commerce/store.js +36 -2
- package/dist/commerce/types.d.ts +1 -56
- package/examples/envar-integration-policy.json +1 -9
- package/examples/seller.json +143 -26
- package/package.json +11 -20
- package/runtime/hermes-setup-probe.py +109 -0
- package/runtime/hermes-skill-inventory.py +108 -0
- package/schemas/envarpay.config.schema.json +7 -73
- package/dist/commerce/buyer-store.d.ts +0 -101
- package/dist/commerce/buyer-store.js +0 -168
- package/dist/commerce/buyer.d.ts +0 -138
- package/dist/commerce/buyer.js +0 -775
- package/dist/commerce/chain.d.ts +0 -24
- package/dist/commerce/chain.js +0 -146
- package/dist/commerce/client-runtime.d.ts +0 -8
- package/dist/commerce/client-runtime.js +0 -7
- package/dist/commerce/management.d.ts +0 -14
- package/dist/commerce/management.js +0 -140
- package/dist/commerce/mpp-client.d.ts +0 -70
- package/dist/commerce/mpp-client.js +0 -29
- package/dist/commerce/mpp-runtime.d.ts +0 -4
- package/dist/commerce/mpp-runtime.js +0 -4
- package/dist/commerce/mpp.d.ts +0 -53
- package/dist/commerce/mpp.js +0 -163
- package/dist/commerce/peer-proxy.d.ts +0 -44
- package/dist/commerce/peer-proxy.js +0 -673
- package/dist/commerce/stripe-issuer.d.ts +0 -41
- package/dist/commerce/stripe-issuer.js +0 -351
- package/dist/commerce/stripe-provider.d.ts +0 -11
- package/dist/commerce/stripe-provider.js +0 -132
- package/dist/commerce/x402.d.ts +0 -37
- package/dist/commerce/x402.js +0 -174
- package/dist/index.d.ts +0 -95
- package/dist/index.js +0 -163
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
- /package/{examples → runtime}/native-skill-runner.py +0 -0
package/README.md
CHANGED
|
@@ -1,69 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@envarai/envarpay`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
A2A 1.0; payments use native x402 v2 exact USDC or MPP Stripe charge. Prices and
|
|
5
|
-
scope are versioned configuration, enforced before dispatch, not instructions in
|
|
6
|
-
a prompt. Envar is an optional directory and management UI.
|
|
3
|
+
This package runs an A2A seller gate for free services and Envar account-balance purchases. Buyers use their Envar Wallet; this package does not sign buyer payments.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
this checkout while the release is undergoing acceptance; a source version is
|
|
10
|
-
not evidence that its npm/OCI artifact is published.
|
|
5
|
+
## Recommended setup
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Node.js 22.14+ (Node 24 recommended). SQLite is single-instance. Contracts can be
|
|
15
|
-
read without loading the seller or buyer runtime; Bun validation covers the
|
|
16
|
-
contracts entry, not the SQLite server.
|
|
17
|
-
|
|
18
|
-
| Entry | Purpose |
|
|
19
|
-
|---|---|
|
|
20
|
-
| `@envarai/envarpay` | Config types/validation, integer pricing, quotes and A2A Cards |
|
|
21
|
-
| `/server` | Durable seller A2A handler, exact x402 gate and native upstream executor |
|
|
22
|
-
| `/client` | Buyer policy, atomic budgets, original-operation recovery and private management |
|
|
23
|
-
| `/mpp` | Opt-in native MPP seller and read-only Stripe reconciliation |
|
|
24
|
-
| `/envar` | Optional bounded config pull/ack and durable observation reports |
|
|
7
|
+
Use the Agent-specific setup file downloaded from Envar:
|
|
25
8
|
|
|
26
9
|
```sh
|
|
27
|
-
|
|
28
|
-
npm run build
|
|
29
|
-
node dist/commerce/cli.js init --directory ./private
|
|
30
|
-
node dist/commerce/cli.js validate --config ./private/seller.json
|
|
10
|
+
envarpay setup --file /private/path/envar-setup.json
|
|
31
11
|
```
|
|
32
12
|
|
|
33
|
-
|
|
34
|
-
funds a wallet. Replace example addresses/hosts and review service limits before
|
|
35
|
-
starting. Keep Agent model credentials in the Agent, and signer keys in a separate
|
|
36
|
-
private wallet boundary.
|
|
13
|
+
Follow the terminal prompts to select a Hermes or OpenClaw runtime and its text Skills. Setup creates an owner-only directory containing `seller.json`, `credentials.json`, `envar.json`, `envar.token`, `vault.key`, `connection.json`, and `start.mjs`. Run the printed `node .../start.mjs` command, import `connection.json` in Envar, and select **Check connection**. The public [Agent guide](https://envar.ai/guide/agents) gives the platform-specific steps and the test message to send.
|
|
37
14
|
|
|
38
|
-
|
|
39
|
-
node dist/commerce/cli.js serve --config seller.json \
|
|
40
|
-
--credentials private/seller-auth.json --state private/seller.sqlite3 \
|
|
41
|
-
--origin https://seller.example --port 4020
|
|
42
|
-
node dist/commerce/cli.js buyer-serve --config buyer-policy.json \
|
|
43
|
-
--credentials private/buyer-auth.json --state private/buyer.sqlite3 \
|
|
44
|
-
--origin https://buyer-management.example --port 4021
|
|
45
|
-
```
|
|
15
|
+
The generated seller starts with free services and a local balance receiving reference of `envar`. A paid service becomes available only after Envar approves this seller account and the service editor verifies the actual unpaid USD quote. In Envar, choose a Skill, enter its USD price, select **Test service**, then **Publish**. There is no separate payment setup command.
|
|
46
16
|
|
|
47
|
-
|
|
48
|
-
quote and confirm an already-reviewed purchase; it cannot change peers, recipient
|
|
49
|
-
allowlists, budgets or signing keys. Unknown outcomes retain the original request,
|
|
50
|
-
signature/SPT and reserved budget. Never create a replacement purchase after a
|
|
51
|
-
timeout.
|
|
17
|
+
## Manual seller commands
|
|
52
18
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
19
|
+
```sh
|
|
20
|
+
envarpay init --directory /private/envarpay
|
|
21
|
+
envarpay validate --config /private/envarpay/seller.json
|
|
22
|
+
envarpay card --config /private/envarpay/seller.json --service SERVICE --offer OFFER --origin https://seller.example
|
|
23
|
+
envarpay serve --config /private/envarpay/seller.json --credentials /private/envarpay/credentials.json --state /private/envarpay/seller.sqlite3 --origin https://seller.example --envar-config /private/envarpay/envar.json --envar-credentials /private/envarpay/envar.token
|
|
24
|
+
envarpay inspect --state /private/envarpay/seller.sqlite3
|
|
25
|
+
```
|
|
57
26
|
|
|
58
|
-
|
|
27
|
+
`init` creates a sample seller config and vault key; it does not authorize paid work. Put secrets only in owner-only files (`chmod 600`), keep the state directory private, and use one running process per ledger. The runtime verifies the configured service revision, original A2A Task and one captured Envar balance claim before calling the private Agent. It stores the original claim and Task handles so a lost response can be recovered without charging twice.
|
|
59
28
|
|
|
60
|
-
|
|
61
|
-
charges upfront for a new task; reads and permitted clarification use that same
|
|
62
|
-
purchase. It does not provide escrow, automatic refunds, subscriptions or billing
|
|
63
|
-
for arbitrary internal tools. A2A compatibility alone does not give an Agent a
|
|
64
|
-
wallet or authorize payment.
|
|
29
|
+
The seller configuration schema is [`schemas/envarpay.config.schema.json`](schemas/envarpay.config.schema.json). Paid amounts are integer **micro-USD** (`100000` = USD 0.10); free offers have no payment profile. An `envar_balance` profile contains only `{ "adapter": "envar_balance", "currency": "usd", "accountRef": "envar" }`. The private Envar policy must allow that exact account reference.
|
|
65
30
|
|
|
66
|
-
|
|
67
|
-
explicit test/live mode and any required customer authentication. Simulator tests
|
|
68
|
-
are not card charges. Provider and chain acceptance must be recorded separately
|
|
69
|
-
from SDK tests and build results.
|
|
31
|
+
For development, run `npm ci --ignore-scripts`, `npm run build`, and `npm test` from this directory. Node.js 22.14+ is required; Node 24 is recommended.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface Invitation {
|
|
2
|
+
version: 1;
|
|
3
|
+
agentId: string;
|
|
4
|
+
platformOrigin: string;
|
|
5
|
+
setupToken: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function invitation(value: any): Invitation;
|
|
8
|
+
export declare function agentSetup(options: {
|
|
9
|
+
file: string;
|
|
10
|
+
directory?: string;
|
|
11
|
+
python?: string;
|
|
12
|
+
home?: string;
|
|
13
|
+
container?: string;
|
|
14
|
+
origin?: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
checkOnly?: boolean;
|
|
17
|
+
framework?: "hermes" | "openclaw";
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/** Agent-driven installation. Secrets stay in local files; receipts come from Envar. */
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, openSync, closeSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { createServer } from 'node:net';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { activateManagedProfile, managedProfile } from './managed.js';
|
|
8
|
+
import { setupAgent, parseSetupBundle } from './setup.js';
|
|
9
|
+
import { CommerceError } from './types.js';
|
|
10
|
+
function privateJson(file) {
|
|
11
|
+
const s = lstatSync(file);
|
|
12
|
+
if (!s.isFile() || s.isSymbolicLink() || s.size > 16384 || (s.mode & 0o077))
|
|
13
|
+
throw new CommerceError('setup_private_file', 'Save the setup invitation in an owner-only file (chmod600).');
|
|
14
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
export function invitation(value) {
|
|
17
|
+
if (value?.version !== 1 || !/^[0-9a-f-]{36}$/.test(value.agentId) || !/^envar_setup_[A-Za-z0-9_-]{43}$/.test(value.setupToken))
|
|
18
|
+
throw new CommerceError('setup_invitation', 'Copy a fresh setup prompt.');
|
|
19
|
+
const url = new URL(value.platformOrigin);
|
|
20
|
+
if (url.protocol !== 'https:' || url.origin !== value.platformOrigin || url.username || url.password)
|
|
21
|
+
throw new CommerceError('setup_origin', 'Use the exact Envar HTTPS origin.');
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
async function request(invite, path, data) {
|
|
25
|
+
const r = await fetch(invite.platformOrigin + '/api/v1/agent-setup/' + path, { method: 'POST', redirect: 'error', headers: { Authorization: 'Bearer ' + invite.setupToken, 'Content-Type': 'application/json' }, body: JSON.stringify(data), signal: AbortSignal.timeout(45000) });
|
|
26
|
+
if (!r.ok)
|
|
27
|
+
throw new CommerceError('setup_' + path, `Envar ${path} returned ${r.status}. Check connection or copy a fresh prompt; do not report success.`);
|
|
28
|
+
const text = await r.text();
|
|
29
|
+
if (text.length > 65536)
|
|
30
|
+
throw new CommerceError('setup_response', 'Envar returned an oversized response.');
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
async function freePort() {
|
|
34
|
+
const socket = createServer();
|
|
35
|
+
await new Promise((resolve, reject) => { socket.once('error', reject); socket.listen(0, '127.0.0.1', resolve); });
|
|
36
|
+
const port = socket.address().port;
|
|
37
|
+
await new Promise(resolve => socket.close(() => resolve()));
|
|
38
|
+
return port;
|
|
39
|
+
}
|
|
40
|
+
function save(file, data) { writeFileSync(file, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' }); }
|
|
41
|
+
async function publicOrigin(root, port, agentId, supplied) {
|
|
42
|
+
if (supplied) {
|
|
43
|
+
const u = new URL(supplied);
|
|
44
|
+
if (u.protocol !== 'https:' || u.origin !== supplied)
|
|
45
|
+
throw Error('Use a public HTTPS origin');
|
|
46
|
+
return supplied;
|
|
47
|
+
}
|
|
48
|
+
// Use an available existing tunnel session, without deleting or rebinding any endpoint.
|
|
49
|
+
for (const apiPort of [4040, 4041, 4042, 4043]) {
|
|
50
|
+
try {
|
|
51
|
+
const api = `http://127.0.0.1:${apiPort}`;
|
|
52
|
+
const r = await fetch(api + '/api/tunnels', { signal: AbortSignal.timeout(1000) });
|
|
53
|
+
if (!r.ok)
|
|
54
|
+
continue;
|
|
55
|
+
const list = (await r.json());
|
|
56
|
+
const name = 'envarpay-' + agentId, addr = `http://127.0.0.1:${port}`;
|
|
57
|
+
const prior = list.tunnels?.find((t) => t.name === name);
|
|
58
|
+
if (prior) {
|
|
59
|
+
if (prior.config.addr !== addr)
|
|
60
|
+
continue;
|
|
61
|
+
return prior.public_url;
|
|
62
|
+
}
|
|
63
|
+
if (list.tunnels?.length >= 3)
|
|
64
|
+
continue;
|
|
65
|
+
const created = await fetch(api + '/api/tunnels', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, proto: 'http', addr, schemes: ['https'], inspect: false }), signal: AbortSignal.timeout(12000) });
|
|
66
|
+
if (created.ok)
|
|
67
|
+
return (await created.json()).public_url;
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
// Outbound HTTPS works inside Docker too; never requires a Docker socket or host port mapping.
|
|
72
|
+
const log = join(root, 'tunnel.log');
|
|
73
|
+
if (existsSync(log))
|
|
74
|
+
privateLog(log);
|
|
75
|
+
const offset = existsSync(log) ? readFileSync(log, 'utf8').length : 0, fd = openSync(log, 'a', 0o600);
|
|
76
|
+
const tunnel = spawn('cloudflared', ['tunnel', '--url', `http://127.0.0.1:${port}`, '--no-autoupdate'], { detached: true, stdio: ['ignore', fd, fd] });
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
let failed = false;
|
|
79
|
+
tunnel.on('error', () => { failed = true; });
|
|
80
|
+
tunnel.unref();
|
|
81
|
+
for (let i = 0; i < 45; i++) {
|
|
82
|
+
if (failed)
|
|
83
|
+
break;
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
85
|
+
const url = readFileSync(log, 'utf8').slice(offset).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
|
|
86
|
+
if (url) {
|
|
87
|
+
writeFileSync(join(root, 'tunnel-process.json'), JSON.stringify({ pid: tunnel.pid, origin: url, temporary: true }), { mode: 0o600 });
|
|
88
|
+
return url;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (tunnel.pid)
|
|
92
|
+
try {
|
|
93
|
+
tunnel.kill();
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
throw new CommerceError('setup_https', 'Install cloudflared from its official distribution, or supply --origin with an existing HTTPS forwarder. Preserve other tunnels.');
|
|
97
|
+
}
|
|
98
|
+
function privateLog(file) { const s = lstatSync(file); if (!s.isFile() || s.isSymbolicLink() || (s.mode & 0o077))
|
|
99
|
+
throw new CommerceError('setup_private_file', 'Runtime log must be an owner-only regular file.'); }
|
|
100
|
+
export async function agentSetup(options) {
|
|
101
|
+
const invite = invitation(privateJson(resolve(options.file)));
|
|
102
|
+
const managedRoot = process.env.ENVARPAY_MANAGED_ROOT ? resolve(process.env.ENVARPAY_MANAGED_ROOT) : undefined;
|
|
103
|
+
const managedDirectory = managedRoot ? join(managedRoot, 'profiles', invite.agentId) : undefined;
|
|
104
|
+
const profile = resolve(options.directory ?? managedDirectory ?? join(homedir(), '.envarpay', invite.agentId));
|
|
105
|
+
if (managedRoot) {
|
|
106
|
+
if (options.container || profile !== managedDirectory)
|
|
107
|
+
throw new CommerceError('managed_profile', 'Use this managed runtime and its existing profile location');
|
|
108
|
+
const active = managedProfile(managedRoot);
|
|
109
|
+
if (active !== managedRoot && active !== profile)
|
|
110
|
+
throw new CommerceError('managed_bound', 'This runtime belongs to another Agent; preserve its original profile');
|
|
111
|
+
const origin = process.env.ENVARPAY_ORIGIN, port = Number(process.env.ENVARPAY_PORT ?? '4020');
|
|
112
|
+
if (!origin || new URL(origin).protocol !== 'https:' || new URL(origin).origin !== origin || !Number.isInteger(port) || port < 1024 || port > 65535 || (options.origin && options.origin !== origin) || (options.port && options.port !== port))
|
|
113
|
+
throw new CommerceError('managed_origin', 'Use the managed HTTPS origin and port');
|
|
114
|
+
options = { ...options, origin, port };
|
|
115
|
+
}
|
|
116
|
+
const root = profile + '.onboarding';
|
|
117
|
+
if (options.checkOnly) {
|
|
118
|
+
const result = await request(invite, 'status', {});
|
|
119
|
+
if (result.agent_id !== invite.agentId)
|
|
120
|
+
throw new CommerceError('setup_identity', 'Receipt belongs to another Agent.');
|
|
121
|
+
console.log(JSON.stringify(result, null, 2));
|
|
122
|
+
if (result.status !== 'connected')
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
127
|
+
if (lstatSync(root).isSymbolicLink() || (lstatSync(root).mode & 0o077))
|
|
128
|
+
throw new CommerceError('setup_directory', 'Use a private directory owned by this user.');
|
|
129
|
+
const manifest = join(root, 'setup.json');
|
|
130
|
+
if (existsSync(manifest) && privateJson(manifest).agentId !== invite.agentId)
|
|
131
|
+
throw new CommerceError('setup_identity', 'This directory belongs to another Agent.');
|
|
132
|
+
const bundle = parseSetupBundle(await request(invite, 'claim', {}));
|
|
133
|
+
if (bundle.agentId !== invite.agentId || bundle.platformOrigin !== invite.platformOrigin)
|
|
134
|
+
throw new CommerceError('setup_identity', 'Envar returned another Agent identity.');
|
|
135
|
+
const bundleFile = join(root, 'bundle.json');
|
|
136
|
+
if (existsSync(bundleFile))
|
|
137
|
+
privateJson(bundleFile);
|
|
138
|
+
writeFileSync(bundleFile, JSON.stringify(bundle), { mode: 0o600 });
|
|
139
|
+
if (!existsSync(profile)) {
|
|
140
|
+
let settings = existsSync(manifest) ? privateJson(manifest) : undefined;
|
|
141
|
+
if (!settings) {
|
|
142
|
+
const port = options.port ?? await freePort();
|
|
143
|
+
const origin = await publicOrigin(root, port, invite.agentId, options.origin);
|
|
144
|
+
settings = { agentId: invite.agentId, port, origin };
|
|
145
|
+
save(manifest, settings);
|
|
146
|
+
}
|
|
147
|
+
await setupAgent({ framework: options.framework, managed: !!managedRoot, file: bundleFile, directory: profile, python: options.python, home: options.home, container: options.container, automatic: settings });
|
|
148
|
+
}
|
|
149
|
+
if (lstatSync(profile).isSymbolicLink() || (lstatSync(profile).mode & 0o077))
|
|
150
|
+
throw new CommerceError('setup_directory', 'The runtime must be a private directory.');
|
|
151
|
+
const connection = privateJson(join(profile, 'connection.json'));
|
|
152
|
+
privateLog(join(profile, 'envar.token'));
|
|
153
|
+
const launcher = lstatSync(join(profile, 'start.mjs'));
|
|
154
|
+
if (!launcher.isFile() || launcher.isSymbolicLink() || (launcher.mode & 0o077))
|
|
155
|
+
throw new CommerceError('setup_private_file', 'Use the original private runtime launcher.');
|
|
156
|
+
if (connection.agentId !== invite.agentId)
|
|
157
|
+
throw new CommerceError('setup_identity', 'Existing runtime belongs to another Agent.');
|
|
158
|
+
const policy = privateJson(join(profile, 'envar.json')).policy;
|
|
159
|
+
if (policy.agentId !== invite.agentId || policy.platformOrigin !== invite.platformOrigin)
|
|
160
|
+
throw new CommerceError('setup_identity', 'Existing integration belongs to another Agent.');
|
|
161
|
+
if (managedRoot) {
|
|
162
|
+
if (readFileSync(join(profile, 'envar.token'), 'utf8') !== bundle.token)
|
|
163
|
+
writeFileSync(join(profile, 'envar.token'), bundle.token, { mode: 0o600 });
|
|
164
|
+
activateManagedProfile(managedRoot, invite.agentId, profile);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const running = join(root, 'process.json');
|
|
168
|
+
let started = false;
|
|
169
|
+
if (existsSync(running))
|
|
170
|
+
try {
|
|
171
|
+
process.kill(privateJson(running).pid, 0);
|
|
172
|
+
started = true;
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
if (started && readFileSync(join(profile, 'envar.token'), 'utf8') !== bundle.token)
|
|
176
|
+
throw new CommerceError('setup_running', 'Existing runtime uses an earlier credential. Stop its recorded process, keep its ledger, then retry this invitation.');
|
|
177
|
+
if (!started) {
|
|
178
|
+
privateLog(join(profile, 'envar.token'));
|
|
179
|
+
writeFileSync(join(profile, 'envar.token'), bundle.token, { mode: 0o600 });
|
|
180
|
+
const log = join(root, 'runtime.log');
|
|
181
|
+
if (existsSync(log))
|
|
182
|
+
privateLog(log);
|
|
183
|
+
const fd = openSync(log, 'a', 0o600), child = spawn(process.execPath, [join(profile, 'start.mjs')], { detached: true, stdio: ['ignore', fd, fd] });
|
|
184
|
+
closeSync(fd);
|
|
185
|
+
child.unref();
|
|
186
|
+
writeFileSync(running, JSON.stringify({ pid: child.pid }), { mode: 0o600 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let result;
|
|
190
|
+
for (let attempt = 0; attempt < 12; attempt++) {
|
|
191
|
+
try {
|
|
192
|
+
result = await request(invite, 'connect', { agentId: invite.agentId, url: connection.url, authToken: connection.authToken });
|
|
193
|
+
if (result.status === 'connected')
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
if (attempt === 11)
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
201
|
+
}
|
|
202
|
+
result = await request(invite, 'status', {});
|
|
203
|
+
if (result.status !== 'connected' || result.agent_id !== invite.agentId || !result.verified_at)
|
|
204
|
+
throw new CommerceError('setup_unverified', 'Platform did not verify this setup. Keep files and report the blocker.');
|
|
205
|
+
const output = { ...result, local_directory: profile, receipt_file: join(root, 'receipt.json'), scope: 'connected; not published; no payment', restart: managedRoot ? 'managed by the deployment' : `node ${JSON.stringify(join(profile, 'start.mjs'))}`, temporary_tunnel: !options.origin, automatic_restart: !!managedRoot };
|
|
206
|
+
if (existsSync(join(root, 'receipt.json')))
|
|
207
|
+
privateJson(join(root, 'receipt.json'));
|
|
208
|
+
writeFileSync(join(root, 'receipt.json'), JSON.stringify(output, null, 2) + '\n', { mode: 0o600 });
|
|
209
|
+
console.log(JSON.stringify(output, null, 2));
|
|
210
|
+
}
|
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
type
|
|
1
|
+
type Name = 'ledger_read' | 'payment_unknown' | 'execution_unknown' | 'report_backlog' | 'report_failure';
|
|
2
2
|
type Signal = {
|
|
3
|
-
name:
|
|
3
|
+
name: Name;
|
|
4
4
|
status: 'clear' | 'alert' | 'unavailable' | 'not_applicable';
|
|
5
5
|
severity: 'info' | 'warning' | 'critical';
|
|
6
6
|
count: number | null;
|
|
7
7
|
};
|
|
8
|
-
type Budget = {
|
|
9
|
-
unit: string;
|
|
10
|
-
maxTotal: string;
|
|
11
|
-
reserved: string;
|
|
12
|
-
spent: string;
|
|
13
|
-
remaining: string;
|
|
14
|
-
state: 'available' | 'near_limit' | 'exhausted';
|
|
15
|
-
};
|
|
16
8
|
export interface LocalAlertOptions {
|
|
17
|
-
policyPath?: string;
|
|
18
9
|
integrationPath?: string;
|
|
19
10
|
integrationDisabled?: boolean;
|
|
20
11
|
}
|
|
21
|
-
/** Owner-local aggregate evidence only. Never opens a runtime store, signer, vault or network. */
|
|
22
12
|
export declare function inspectLocalAlerts(path: string, options?: LocalAlertOptions): {
|
|
23
|
-
role: "unavailable" | "
|
|
13
|
+
role: "unavailable" | "seller";
|
|
24
14
|
view: "alerts";
|
|
25
15
|
observation: "local_persisted_state_only";
|
|
26
|
-
status:
|
|
16
|
+
status: "unavailable" | "partial" | "available";
|
|
27
17
|
signals: Signal[];
|
|
28
|
-
budgets:
|
|
18
|
+
budgets: never[];
|
|
29
19
|
alertCount: number;
|
|
30
20
|
unavailableCount: number;
|
|
31
21
|
providerHealth: "unavailable_not_probed";
|
package/dist/commerce/alerts.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
+
/** Read-only health signals for one seller ledger and its Envar report queue. */
|
|
1
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { lstatSync
|
|
3
|
-
|
|
4
|
-
const CURRENCIES = {
|
|
5
|
-
usd: 'usd_cents',
|
|
6
|
-
'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 'base_usdc_atomic',
|
|
7
|
-
'eip155:84532/erc20:0x036cbd53842c5426634e7929541ec2318f3dcf7e': 'base_sepolia_usdc_atomic',
|
|
8
|
-
};
|
|
9
|
-
function privateFile(path, limit) {
|
|
3
|
+
import { lstatSync } from 'node:fs';
|
|
4
|
+
function database(path) {
|
|
10
5
|
const stat = lstatSync(path);
|
|
11
|
-
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid())
|
|
6
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
12
7
|
throw new Error('private_source_unavailable');
|
|
13
|
-
}
|
|
14
|
-
function database(path) {
|
|
15
|
-
privateFile(path);
|
|
16
8
|
const db = new DatabaseSync(path, { readOnly: true });
|
|
17
9
|
try {
|
|
18
10
|
db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN');
|
|
@@ -27,61 +19,19 @@ const count = (db, sql) => Number(db.prepare(sql).get().n);
|
|
|
27
19
|
const signal = (name, value, severity = 'warning') => ({ name, status: value ? 'alert' : 'clear', severity: value ? severity : 'info', count: value });
|
|
28
20
|
const missing = (name) => ({ name, status: 'unavailable', severity: 'warning', count: null });
|
|
29
21
|
const irrelevant = (name) => ({ name, status: 'not_applicable', severity: 'info', count: null });
|
|
30
|
-
/** Owner-local aggregate evidence only. Never opens a runtime store, signer, vault or network. */
|
|
31
22
|
export function inspectLocalAlerts(path, options = {}) {
|
|
32
|
-
const signals = []
|
|
33
|
-
let
|
|
23
|
+
const signals = [];
|
|
24
|
+
let db;
|
|
25
|
+
let available = false;
|
|
34
26
|
try {
|
|
35
27
|
db = database(path);
|
|
36
|
-
|
|
37
|
-
if (tables.has('buyer_purchases') === tables.has('commerce_orders'))
|
|
38
|
-
throw new Error('unknown_ledger');
|
|
39
|
-
role = tables.has('buyer_purchases') ? 'buyer' : 'seller';
|
|
40
|
-
if (db.prepare(`SELECT version FROM ${role === 'buyer' ? 'buyer_meta' : 'commerce_meta'}`).get()?.version !== (role === 'buyer' ? 1 : 2))
|
|
28
|
+
if (db.prepare("SELECT version FROM commerce_meta").get()?.version !== 2)
|
|
41
29
|
throw new Error('unknown_version');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Only concrete persisted provider errors count. Generic unknown is not proof of provider failure.
|
|
45
|
-
const providerFailures = count(db, role === 'buyer'
|
|
46
|
-
? "SELECT COUNT(*) n FROM buyer_purchases WHERE json_extract(record_json,'$.paymentState')!='confirmed' AND json_extract(record_json,'$.errorCode') IN ('stripe_transport_unknown','stripe_request_failed','stripe_rate_limit','stripe_response','stripe_response_size')"
|
|
47
|
-
: "SELECT COUNT(*) n FROM commerce_payment_attempts WHERE state!='confirmed' AND json_extract(receipt_json,'$.success')=0 AND length(json_extract(receipt_json,'$.errorReason'))>0");
|
|
48
|
-
signals.push(signal('ledger_read', 0), signal('payment_unknown', unknownPayments), signal('execution_unknown', unknownExecution), signal('recorded_provider_failure', providerFailures));
|
|
49
|
-
if (role === 'seller')
|
|
50
|
-
signals.push(irrelevant('budget_near_limit'), irrelevant('budget_exhausted'));
|
|
51
|
-
else {
|
|
52
|
-
try {
|
|
53
|
-
if (!options.policyPath)
|
|
54
|
-
throw new Error('policy_required');
|
|
55
|
-
privateFile(options.policyPath, 1024 * 1024);
|
|
56
|
-
const policy = loadBuyerPolicy(JSON.parse(readFileSync(options.policyPath, 'utf8')));
|
|
57
|
-
const usage = new Map();
|
|
58
|
-
for (const b of policy.budgets)
|
|
59
|
-
usage.set(b.currency, { reserved: 0n, spent: 0n });
|
|
60
|
-
for (const row of db.prepare("SELECT currency,amount,budget_state FROM buyer_purchases WHERE budget_state!='none'").iterate()) {
|
|
61
|
-
const currency = String(row.currency), amount = String(row.amount), state = row.budget_state;
|
|
62
|
-
const target = usage.get(currency);
|
|
63
|
-
if (!target || !/^[1-9][0-9]{0,77}$/.test(amount) || !['spent', 'reserved'].includes(String(state)))
|
|
64
|
-
throw new Error('budget_binding');
|
|
65
|
-
target[state === 'spent' ? 'spent' : 'reserved'] += BigInt(amount);
|
|
66
|
-
}
|
|
67
|
-
const checked = policy.budgets.map(b => {
|
|
68
|
-
const u = usage.get(b.currency), cap = BigInt(b.maxTotal), used = u.reserved + u.spent;
|
|
69
|
-
const state = used >= cap ? 'exhausted' : used * 10n >= cap * 9n ? 'near_limit' : 'available';
|
|
70
|
-
return { unit: CURRENCIES[b.currency], maxTotal: cap.toString(), reserved: u.reserved.toString(), spent: u.spent.toString(), remaining: (used < cap ? cap - used : 0n).toString(), state };
|
|
71
|
-
});
|
|
72
|
-
budgets.push(...checked);
|
|
73
|
-
signals.push(signal('budget_near_limit', checked.filter(b => b.state === 'near_limit').length), signal('budget_exhausted', checked.filter(b => b.state === 'exhausted').length, 'critical'));
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
signals.push(missing('budget_near_limit'), missing('budget_exhausted'));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
30
|
+
signals.push(signal('ledger_read', 0), signal('payment_unknown', count(db, "SELECT COUNT(*) n FROM commerce_orders WHERE payment_state='unknown'"), 'critical'), signal('execution_unknown', count(db, "SELECT COUNT(*) n FROM commerce_orders WHERE execution_state='unknown'")));
|
|
31
|
+
available = true;
|
|
79
32
|
}
|
|
80
33
|
catch {
|
|
81
|
-
|
|
82
|
-
signals.length = 0;
|
|
83
|
-
budgets.length = 0;
|
|
84
|
-
signals.push(...['ledger_read', 'payment_unknown', 'execution_unknown', 'recorded_provider_failure', 'budget_near_limit', 'budget_exhausted'].map(missing));
|
|
34
|
+
signals.push(missing('ledger_read'), missing('payment_unknown'), missing('execution_unknown'));
|
|
85
35
|
}
|
|
86
36
|
finally {
|
|
87
37
|
db?.close();
|
|
@@ -92,21 +42,12 @@ export function inspectLocalAlerts(path, options = {}) {
|
|
|
92
42
|
signals.push(irrelevant('report_backlog'), irrelevant('report_failure'));
|
|
93
43
|
}
|
|
94
44
|
else {
|
|
95
|
-
if (options.integrationDisabled)
|
|
96
|
-
throw new Error('ambiguous_integration');
|
|
97
|
-
if (!options.integrationPath)
|
|
45
|
+
if (options.integrationDisabled || !options.integrationPath)
|
|
98
46
|
throw new Error('integration_not_supplied');
|
|
99
47
|
integration = database(options.integrationPath);
|
|
100
|
-
|
|
101
|
-
const source = integration.prepare("SELECT value FROM meta WHERE key='source_instance'").get();
|
|
102
|
-
if (!binding || !source || typeof binding.value !== 'string' || typeof source.value !== 'string')
|
|
103
|
-
throw new Error('integration_schema');
|
|
104
|
-
const parsed = JSON.parse(binding.value);
|
|
105
|
-
if (!parsed || typeof parsed.platformOrigin !== 'string' || typeof parsed.agentId !== 'string' || typeof parsed.runtimeAgentId !== 'string')
|
|
48
|
+
if (!integration.prepare("SELECT value FROM meta WHERE key='binding'").get() || !integration.prepare("SELECT value FROM meta WHERE key='source_instance'").get())
|
|
106
49
|
throw new Error('integration_schema');
|
|
107
|
-
|
|
108
|
-
const failed = count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending' AND attempts>0 AND last_error<>''");
|
|
109
|
-
signals.push(signal('report_backlog', pending), signal('report_failure', failed));
|
|
50
|
+
signals.push(signal('report_backlog', count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending'")), signal('report_failure', count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending' AND attempts>0 AND last_error<>''")));
|
|
110
51
|
}
|
|
111
52
|
}
|
|
112
53
|
catch {
|
|
@@ -115,11 +56,14 @@ export function inspectLocalAlerts(path, options = {}) {
|
|
|
115
56
|
finally {
|
|
116
57
|
integration?.close();
|
|
117
58
|
}
|
|
118
|
-
const alertCount = signals.filter(s => s.status === 'alert').length
|
|
59
|
+
const alertCount = signals.filter(s => s.status === 'alert').length;
|
|
60
|
+
const unavailableCount = signals.filter(s => s.status === 'unavailable').length;
|
|
119
61
|
return {
|
|
120
|
-
role
|
|
121
|
-
|
|
122
|
-
|
|
62
|
+
role: available ? 'seller' : 'unavailable',
|
|
63
|
+
view: 'alerts',
|
|
64
|
+
observation: 'local_persisted_state_only',
|
|
65
|
+
status: !available ? 'unavailable' : unavailableCount ? 'partial' : 'available',
|
|
66
|
+
signals, budgets: [], alertCount, unavailableCount,
|
|
123
67
|
providerHealth: 'unavailable_not_probed',
|
|
124
68
|
recommendedExitCode: unavailableCount ? 3 : alertCount ? 2 : 0,
|
|
125
69
|
paymentPerformed: false, refundPerformed: false, networkRequests: 0,
|
package/dist/commerce/card.js
CHANGED
|
@@ -14,7 +14,7 @@ export function createOfferCard(config, serviceId, offerId, origin) {
|
|
|
14
14
|
if (!service || !offer)
|
|
15
15
|
throw new CommerceError('offer_not_found', 'Service offer not found');
|
|
16
16
|
const profile = offer.paymentProfile ? config.paymentProfiles[offer.paymentProfile] : null;
|
|
17
|
-
const payment = profile
|
|
17
|
+
const payment = profile ? { adapter: 'envar_balance', currency: 'usd', accountRef: profile.accountRef } : null;
|
|
18
18
|
return AgentCard.fromJSON({
|
|
19
19
|
name: service.name,
|
|
20
20
|
description: `Deliverables: ${service.contract.deliverables.join(', ')}`,
|