@envarai/envarpay 0.2.0-alpha.12 → 0.2.0-alpha.14
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/dist/commerce/agent-setup.d.ts +1 -0
- package/dist/commerce/agent-setup.js +44 -23
- package/dist/commerce/chain.d.ts +1 -1
- package/dist/commerce/chain.js +39 -13
- package/dist/commerce/cli.js +14 -5
- package/dist/commerce/discovery.d.ts +1 -0
- package/dist/commerce/discovery.js +3 -0
- package/dist/commerce/managed.d.ts +3 -0
- package/dist/commerce/managed.js +143 -0
- package/dist/commerce/native-inventory.d.ts +8 -0
- package/dist/commerce/native-inventory.js +72 -0
- package/dist/commerce/native-skills.d.ts +1 -0
- package/dist/commerce/openclaw-setup.d.ts +28 -0
- package/dist/commerce/openclaw-setup.js +43 -0
- package/dist/commerce/setup.d.ts +4 -1
- package/dist/commerce/setup.js +79 -58
- package/examples/hermes-setup-probe.py +5 -1
- package/package.json +3 -2
|
@@ -4,7 +4,8 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { createServer } from 'node:net';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
-
import {
|
|
7
|
+
import { activateManagedProfile, managedProfile } from './managed.js';
|
|
8
|
+
import { setupAgent, parseSetupBundle } from './setup.js';
|
|
8
9
|
import { CommerceError } from './types.js';
|
|
9
10
|
function privateJson(file) {
|
|
10
11
|
const s = lstatSync(file);
|
|
@@ -98,7 +99,20 @@ function privateLog(file) { const s = lstatSync(file); if (!s.isFile() || s.isSy
|
|
|
98
99
|
throw new CommerceError('setup_private_file', 'Runtime log must be an owner-only regular file.'); }
|
|
99
100
|
export async function agentSetup(options) {
|
|
100
101
|
const invite = invitation(privateJson(resolve(options.file)));
|
|
101
|
-
const
|
|
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
|
+
}
|
|
102
116
|
const root = profile + '.onboarding';
|
|
103
117
|
if (options.checkOnly) {
|
|
104
118
|
const result = await request(invite, 'status', {});
|
|
@@ -130,7 +144,7 @@ export async function agentSetup(options) {
|
|
|
130
144
|
settings = { agentId: invite.agentId, port, origin };
|
|
131
145
|
save(manifest, settings);
|
|
132
146
|
}
|
|
133
|
-
await
|
|
147
|
+
await setupAgent({ framework: options.framework, managed: !!managedRoot, file: bundleFile, directory: profile, python: options.python, home: options.home, container: options.container, automatic: settings });
|
|
134
148
|
}
|
|
135
149
|
if (lstatSync(profile).isSymbolicLink() || (lstatSync(profile).mode & 0o077))
|
|
136
150
|
throw new CommerceError('setup_directory', 'The runtime must be a private directory.');
|
|
@@ -144,26 +158,33 @@ export async function agentSetup(options) {
|
|
|
144
158
|
const policy = privateJson(join(profile, 'envar.json')).policy;
|
|
145
159
|
if (policy.agentId !== invite.agentId || policy.platformOrigin !== invite.platformOrigin)
|
|
146
160
|
throw new CommerceError('setup_identity', 'Existing integration belongs to another Agent.');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 });
|
|
153
187
|
}
|
|
154
|
-
catch { }
|
|
155
|
-
if (started && readFileSync(join(profile, 'envar.token'), 'utf8') !== bundle.token)
|
|
156
|
-
throw new CommerceError('setup_running', 'Existing runtime uses an earlier credential. Stop its recorded process, keep its ledger, then retry this invitation.');
|
|
157
|
-
if (!started) {
|
|
158
|
-
privateLog(join(profile, 'envar.token'));
|
|
159
|
-
writeFileSync(join(profile, 'envar.token'), bundle.token, { mode: 0o600 });
|
|
160
|
-
const log = join(root, 'runtime.log');
|
|
161
|
-
if (existsSync(log))
|
|
162
|
-
privateLog(log);
|
|
163
|
-
const fd = openSync(log, 'a', 0o600), child = spawn(process.execPath, [join(profile, 'start.mjs')], { detached: true, stdio: ['ignore', fd, fd] });
|
|
164
|
-
closeSync(fd);
|
|
165
|
-
child.unref();
|
|
166
|
-
writeFileSync(running, JSON.stringify({ pid: child.pid }), { mode: 0o600 });
|
|
167
188
|
}
|
|
168
189
|
let result;
|
|
169
190
|
for (let attempt = 0; attempt < 12; attempt++) {
|
|
@@ -181,7 +202,7 @@ export async function agentSetup(options) {
|
|
|
181
202
|
result = await request(invite, 'status', {});
|
|
182
203
|
if (result.status !== 'connected' || result.agent_id !== invite.agentId || !result.verified_at)
|
|
183
204
|
throw new CommerceError('setup_unverified', 'Platform did not verify this setup. Keep files and report the blocker.');
|
|
184
|
-
const output = { ...result, local_directory: profile, receipt_file: join(root, 'receipt.json'), scope: 'connected; not published; no payment', restart: `node ${JSON.stringify(join(profile, 'start.mjs'))}`, temporary_tunnel: !options.origin, automatic_restart:
|
|
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 };
|
|
185
206
|
if (existsSync(join(root, 'receipt.json')))
|
|
186
207
|
privateJson(join(root, 'receipt.json'));
|
|
187
208
|
writeFileSync(join(root, 'receipt.json'), JSON.stringify(output, null, 2) + '\n', { mode: 0o600 });
|
package/dist/commerce/chain.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface ExpiredUnusedProof {
|
|
|
14
14
|
authorizationUsed: false;
|
|
15
15
|
}
|
|
16
16
|
/** Checks the canonical chain receipt and exact official-token Transfer. */
|
|
17
|
-
export declare function evmReceiptVerifier(rpcUrls: Readonly<Record<string, string>>, confirmations?: number): (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
|
|
17
|
+
export declare function evmReceiptVerifier(rpcUrls: Readonly<Record<string, string>>, confirmations?: number, confirmationWaitMs?: number): (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
|
|
18
18
|
/** Locate an original EIP-3009 settlement even when facilitator lost its tx response. */
|
|
19
19
|
export declare function evmSettlementRecovery(rpcUrls: Readonly<Record<string, string>>): {
|
|
20
20
|
expiredUnusedProof: (payload: PaymentPayload, requirements: PaymentRequirements) => Promise<ExpiredUnusedProof | undefined>;
|
package/dist/commerce/chain.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPublicClient, http, decodeEventLog, parseAbi } from 'viem';
|
|
1
|
+
import { createPublicClient, http, decodeEventLog, parseAbi, TransactionReceiptNotFoundError } from 'viem';
|
|
2
2
|
import { CommerceError } from './types.js';
|
|
3
3
|
import { validateUrl } from './config.js';
|
|
4
4
|
const abi = parseAbi(['event Transfer(address indexed from, address indexed to, uint256 value)']);
|
|
@@ -9,9 +9,11 @@ const officialUsdc = {
|
|
|
9
9
|
'eip155:8453': '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
10
10
|
};
|
|
11
11
|
/** Checks the canonical chain receipt and exact official-token Transfer. */
|
|
12
|
-
export function evmReceiptVerifier(rpcUrls, confirmations = 2) {
|
|
12
|
+
export function evmReceiptVerifier(rpcUrls, confirmations = 2, confirmationWaitMs = 12000) {
|
|
13
13
|
if (!Number.isInteger(confirmations) || confirmations < 1)
|
|
14
14
|
throw new CommerceError('confirmation_policy', 'Positive confirmation depth required');
|
|
15
|
+
if (!Number.isInteger(confirmationWaitMs) || confirmationWaitMs < 0 || confirmationWaitMs > 30000)
|
|
16
|
+
throw new CommerceError('confirmation_policy', 'Confirmation wait must be between 0 and 30000 milliseconds');
|
|
15
17
|
return async (receipt, payload, requirements) => {
|
|
16
18
|
const rpc = rpcUrls[requirements.network];
|
|
17
19
|
if (!rpc)
|
|
@@ -23,15 +25,38 @@ export function evmReceiptVerifier(rpcUrls, confirmations = 2) {
|
|
|
23
25
|
const client = createPublicClient({ transport: http(rpc, { timeout: 15000, retryCount: 0 }) });
|
|
24
26
|
if (await client.getChainId() !== chainId)
|
|
25
27
|
return false;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
const deadline = Date.now() + confirmationWaitMs;
|
|
29
|
+
const wait = async () => {
|
|
30
|
+
const remaining = deadline - Date.now();
|
|
31
|
+
if (remaining <= 0)
|
|
32
|
+
return false;
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(1000, remaining)));
|
|
34
|
+
return Date.now() < deadline;
|
|
35
|
+
};
|
|
36
|
+
let mined;
|
|
37
|
+
for (;;) {
|
|
38
|
+
try {
|
|
39
|
+
mined = await client.getTransactionReceipt({ hash: receipt.transaction });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof TransactionReceiptNotFoundError) {
|
|
43
|
+
if (await wait())
|
|
44
|
+
continue;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (mined.status !== 'success' || mined.transactionHash.toLowerCase() !== receipt.transaction.toLowerCase())
|
|
50
|
+
return false;
|
|
51
|
+
const block = await client.getBlock({ blockNumber: mined.blockNumber });
|
|
52
|
+
if (block.hash !== mined.blockHash)
|
|
53
|
+
return false;
|
|
54
|
+
const latest = await client.getBlockNumber({ cacheTime: 0 });
|
|
55
|
+
if (latest - mined.blockNumber + 1n >= BigInt(confirmations))
|
|
56
|
+
break;
|
|
57
|
+
if (!await wait())
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
35
60
|
const auth = payload.payload.authorization;
|
|
36
61
|
if (!auth?.from || !auth.nonce || auth.to?.toLowerCase() !== requirements.payTo.toLowerCase() || auth.value !== requirements.amount)
|
|
37
62
|
return false;
|
|
@@ -131,8 +156,9 @@ export function evmSettlementRecovery(rpcUrls) {
|
|
|
131
156
|
// RPC lookup; never reinterpret incomplete search as proof of non-payment.
|
|
132
157
|
if (latest - BigInt(fromBlock) > 100000n)
|
|
133
158
|
throw new CommerceError('archive_reconciliation_required', 'Original authorization requires an archive log lookup');
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
// The default public Base RPCs limit eth_getLogs to 200-block ranges.
|
|
160
|
+
for (let start = BigInt(fromBlock); start <= latest; start += 200n) {
|
|
161
|
+
const end = start + 199n < latest ? start + 199n : latest;
|
|
136
162
|
const logs = await client.getLogs({ address: requirements.asset, event: authorizationAbi[0], args: { authorizer: auth.from, nonce: auth.nonce }, fromBlock: start, toBlock: end, strict: true });
|
|
137
163
|
const settled = logs.filter(log => !log.removed && log.transactionHash);
|
|
138
164
|
if (settled.length > 1)
|
package/dist/commerce/cli.js
CHANGED
|
@@ -21,7 +21,8 @@ import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
|
|
|
21
21
|
import { CommerceError } from './types.js';
|
|
22
22
|
import { createNativeSkillExecutor } from './native-skills.js';
|
|
23
23
|
import { inspectLedger, inspectLocalAlerts } from './operations.js';
|
|
24
|
-
import {
|
|
24
|
+
import { serveManaged } from './managed.js';
|
|
25
|
+
import { setupAgent } from './setup.js';
|
|
25
26
|
import { SkillDiscovery } from './discovery.js';
|
|
26
27
|
import { upgradeDiscovery, configurePayments } from './configure.js';
|
|
27
28
|
import { agentSetup } from './agent-setup.js';
|
|
@@ -30,9 +31,9 @@ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
|
30
31
|
version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
|
|
31
32
|
port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { type: 'string' },
|
|
32
33
|
after: { type: 'string' }, limit: { type: 'string' }, 'refund-review': { type: 'boolean' }, alerts: { type: 'boolean' }, 'fail-on-alert': { type: 'boolean' }, 'integration-state': { type: 'string' }, 'no-integration': { type: 'boolean' },
|
|
33
|
-
file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' }, check: { type: 'boolean' },
|
|
34
|
+
framework: { type: 'string' }, file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' }, check: { type: 'boolean' },
|
|
34
35
|
} });
|
|
35
|
-
const usage = 'envarpay onboard --file invitation.json [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
|
|
36
|
+
const usage = 'envarpay onboard --file invitation.json [--framework hermes|openclaw] [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
|
|
36
37
|
const closers = [];
|
|
37
38
|
function privateFile(path) {
|
|
38
39
|
const stat = lstatSync(path);
|
|
@@ -92,10 +93,18 @@ async function run() {
|
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
95
|
const command = positionals[0];
|
|
96
|
+
if (values.framework && !['hermes', 'openclaw'].includes(values.framework))
|
|
97
|
+
throw new CommerceError('setup_runtime', 'Use hermes or openclaw');
|
|
98
|
+
if (command === 'managed-serve') {
|
|
99
|
+
if (!values.directory || !values.origin)
|
|
100
|
+
throw new CommerceError('managed_configuration', 'Managed serve requires its directory and HTTPS origin');
|
|
101
|
+
await serveManaged(values.directory, values.origin, values.host ?? '127.0.0.1', port(4020));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
95
104
|
if (command === 'onboard') {
|
|
96
105
|
if (!values.file)
|
|
97
106
|
throw new CommerceError('setup_file', 'Save the private invitation and pass --file PATH.');
|
|
98
|
-
await agentSetup({ file: values.file, directory: values.directory, python: values.python, home: values['hermes-home'], container: values['docker-container'], origin: values.origin, port: values.port ? port(4020) : undefined, checkOnly: values.check });
|
|
107
|
+
await agentSetup({ framework: values.framework, file: values.file, directory: values.directory, python: values.python, home: values['hermes-home'], container: values['docker-container'], origin: values.origin, port: values.port ? port(4020) : undefined, checkOnly: values.check });
|
|
99
108
|
return;
|
|
100
109
|
}
|
|
101
110
|
if (command === 'upgrade' && values.directory) {
|
|
@@ -109,7 +118,7 @@ async function run() {
|
|
|
109
118
|
if (command === 'setup') {
|
|
110
119
|
if (!values.file)
|
|
111
120
|
throw new CommerceError('setup_file', 'Download the setup file from your Agent service page and pass --file PATH');
|
|
112
|
-
await
|
|
121
|
+
await setupAgent({ framework: values.framework, file: values.file, directory: values.directory, container: values['docker-container'], python: values.python, home: values['hermes-home'] });
|
|
113
122
|
return;
|
|
114
123
|
}
|
|
115
124
|
if (values.alerts || values['fail-on-alert'] || values['integration-state'] || values['no-integration']) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readSkillInventory } from "./native-inventory.js";
|
|
1
2
|
import { execFile } from "node:child_process";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
4
|
import { readFileSync, writeFileSync, existsSync, mkdtempSync, mkdirSync, renameSync, rmSync, readdirSync, lstatSync, } from "node:fs";
|
|
@@ -50,6 +51,8 @@ export class SkillDiscovery {
|
|
|
50
51
|
: {};
|
|
51
52
|
}
|
|
52
53
|
async scan() {
|
|
54
|
+
if (this.discovery.roots && !this.settings.dockerContainer)
|
|
55
|
+
return readSkillInventory(this.discovery.roots);
|
|
53
56
|
const code = readFileSync(new URL("../../examples/hermes-skill-inventory.py", import.meta.url), "utf8");
|
|
54
57
|
const args = ["-c", code, this.discovery.hermesHome];
|
|
55
58
|
const { stdout } = this.settings.dockerContainer
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function managedProfile(root: string): string;
|
|
2
|
+
export declare function activateManagedProfile(root: string, agentId: string, profile: string): void;
|
|
3
|
+
export declare function serveManaged(root: string, origin: string, host: string, port: number): Promise<void>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** A persistent native-runtime host for the existing seller CLI. No wallet authority is added. */
|
|
2
|
+
import { chownSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { once } from 'node:events';
|
|
7
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
8
|
+
import { CommerceError } from './types.js';
|
|
9
|
+
const idPattern = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/;
|
|
10
|
+
function rootDirectory(root) {
|
|
11
|
+
const path = resolve(root), stat = lstatSync(path);
|
|
12
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077))
|
|
13
|
+
throw new CommerceError('managed_directory', 'Use the existing private managed EnvarPay directory');
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
export function managedProfile(root) {
|
|
17
|
+
const base = rootDirectory(root), file = join(base, 'active-profile.json');
|
|
18
|
+
const profiles = join(base, 'profiles');
|
|
19
|
+
if (existsSync(profiles)) {
|
|
20
|
+
const stat = lstatSync(profiles);
|
|
21
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077))
|
|
22
|
+
throw new CommerceError('managed_profile', 'Use a private profile directory without links');
|
|
23
|
+
}
|
|
24
|
+
if (!existsSync(file))
|
|
25
|
+
return base;
|
|
26
|
+
const stat = lstatSync(file);
|
|
27
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 || (stat.mode & 0o077))
|
|
28
|
+
throw new CommerceError('managed_profile', 'Invalid managed profile');
|
|
29
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
30
|
+
if (value.version !== 1 || !idPattern.test(value.agentId))
|
|
31
|
+
throw new CommerceError('managed_profile', 'Invalid managed Agent identity');
|
|
32
|
+
const path = join(base, 'profiles', value.agentId), p = lstatSync(path);
|
|
33
|
+
if (!p.isDirectory() || p.isSymbolicLink() || (p.mode & 0o077))
|
|
34
|
+
throw new CommerceError('managed_profile', 'Use the original private profile');
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
export function activateManagedProfile(root, agentId, profile) {
|
|
38
|
+
const base = rootDirectory(root);
|
|
39
|
+
if (!idPattern.test(agentId) || resolve(profile) !== join(base, 'profiles', agentId))
|
|
40
|
+
throw new CommerceError('managed_profile', 'Profile does not belong to this managed Agent');
|
|
41
|
+
const previous = managedProfile(base);
|
|
42
|
+
if (previous !== base && previous !== resolve(profile))
|
|
43
|
+
throw new CommerceError('managed_bound', 'This runtime is already bound to another Agent; preserve its ledger');
|
|
44
|
+
if (previous === base) {
|
|
45
|
+
const config = JSON.parse(readFileSync(join(base, 'seller.json'), 'utf8'));
|
|
46
|
+
if (config.services?.length || Object.keys(config.paymentProfiles ?? {}).length)
|
|
47
|
+
throw new CommerceError('managed_in_use', 'Existing seller configuration requires review');
|
|
48
|
+
const ledger = join(base, 'seller.sqlite3');
|
|
49
|
+
if (existsSync(ledger)) {
|
|
50
|
+
const db = new DatabaseSync(ledger, { readOnly: true });
|
|
51
|
+
try {
|
|
52
|
+
for (const table of ['commerce_orders', 'commerce_service_revisions']) {
|
|
53
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table) && Number(db.prepare(`SELECT count(*) AS n FROM ${table}`).get()?.n))
|
|
54
|
+
throw new CommerceError('managed_in_use', 'Keep the existing seller ledger and inspect its operations before connecting');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
db.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const owner = lstatSync(base);
|
|
63
|
+
function ownership(path) {
|
|
64
|
+
const s = lstatSync(path);
|
|
65
|
+
if (s.isSymbolicLink() || (!s.isDirectory() && !s.isFile()))
|
|
66
|
+
throw new CommerceError('managed_profile', 'Managed profiles cannot contain links or special files');
|
|
67
|
+
if (process.getuid?.() === 0)
|
|
68
|
+
chownSync(path, owner.uid, owner.gid);
|
|
69
|
+
if (s.isDirectory())
|
|
70
|
+
for (const name of readdirSync(path))
|
|
71
|
+
ownership(join(path, name));
|
|
72
|
+
}
|
|
73
|
+
ownership(join(base, 'profiles'));
|
|
74
|
+
const connection = JSON.parse(readFileSync(join(profile, 'connection.json'), 'utf8'));
|
|
75
|
+
if (connection.agentId !== agentId)
|
|
76
|
+
throw new CommerceError('managed_profile', 'Connection belongs to another Agent');
|
|
77
|
+
if (previous === resolve(profile))
|
|
78
|
+
return;
|
|
79
|
+
const temp = join(base, `.active-${process.pid}.json`);
|
|
80
|
+
writeFileSync(temp, JSON.stringify({ version: 1, agentId }), { mode: 0o600, flag: 'wx', flush: true });
|
|
81
|
+
if (process.getuid?.() === 0)
|
|
82
|
+
chownSync(temp, owner.uid, owner.gid);
|
|
83
|
+
try {
|
|
84
|
+
// Atomic create prevents two setup processes from replacing one another's binding.
|
|
85
|
+
linkSync(temp, join(base, 'active-profile.json'));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== 'EEXIST' || managedProfile(base) !== resolve(profile))
|
|
89
|
+
throw new CommerceError('managed_bound', 'This runtime was bound by another setup; preserve its original profile');
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
unlinkSync(temp);
|
|
93
|
+
}
|
|
94
|
+
const directory = openSync(base, 'r');
|
|
95
|
+
try {
|
|
96
|
+
fsyncSync(directory);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
closeSync(directory);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function serveManaged(root, origin, host, port) {
|
|
103
|
+
const base = rootDirectory(root), cli = fileURLToPath(new URL('./cli.js', import.meta.url));
|
|
104
|
+
let child, stopping = false;
|
|
105
|
+
const stop = () => { stopping = true; child?.kill('SIGTERM'); };
|
|
106
|
+
process.once('SIGTERM', stop);
|
|
107
|
+
process.once('SIGINT', stop);
|
|
108
|
+
try {
|
|
109
|
+
while (!stopping) {
|
|
110
|
+
const profile = managedProfile(base);
|
|
111
|
+
const stamp = () => ['credentials.json', 'envar.json', 'envar.token'].map(name => { const p = join(profile, name); return existsSync(p) ? lstatSync(p).mtimeMs : 0; }).join(':');
|
|
112
|
+
const initialStamp = stamp();
|
|
113
|
+
const args = [cli, 'serve', '--config', join(profile, 'seller.json'), '--credentials', join(profile, 'credentials.json'), '--state', join(profile, 'seller.sqlite3'), '--origin', origin, '--host', host, '--port', String(port)];
|
|
114
|
+
if (profile !== base)
|
|
115
|
+
args.push('--envar-config', join(profile, 'envar.json'), '--envar-credentials', join(profile, 'envar.token'));
|
|
116
|
+
child = spawn(process.execPath, args, { stdio: 'inherit' });
|
|
117
|
+
let switching = false;
|
|
118
|
+
const timer = setInterval(() => {
|
|
119
|
+
try {
|
|
120
|
+
if (!switching && (managedProfile(base) !== profile || stamp() !== initialStamp)) {
|
|
121
|
+
switching = true;
|
|
122
|
+
child?.kill('SIGTERM');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
stop();
|
|
127
|
+
}
|
|
128
|
+
}, 1000);
|
|
129
|
+
try {
|
|
130
|
+
const [code] = await once(child, 'exit');
|
|
131
|
+
if (!stopping && !switching)
|
|
132
|
+
throw new CommerceError('managed_runtime_exit', `Seller stopped (${code}); keep its profile and inspect its configuration`);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
clearInterval(timer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
process.removeListener('SIGTERM', stop);
|
|
141
|
+
process.removeListener('SIGINT', stop);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { extname, join, resolve } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
const namePattern = /^[a-z][a-z0-9-]{0,63}$/;
|
|
7
|
+
export function readSkillInventory(roots) {
|
|
8
|
+
const found = [];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
function visit(folder, depth) {
|
|
11
|
+
if (depth > 10 || found.length >= 128 || !existsSync(folder))
|
|
12
|
+
return;
|
|
13
|
+
const stat = lstatSync(folder);
|
|
14
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
15
|
+
return;
|
|
16
|
+
const entry = join(folder, 'SKILL.md');
|
|
17
|
+
if (!existsSync(entry)) {
|
|
18
|
+
for (const child of readdirSync(folder).sort())
|
|
19
|
+
if (!child.startsWith('.'))
|
|
20
|
+
visit(join(folder, child), depth + 1);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const info = lstatSync(entry);
|
|
24
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 48000)
|
|
25
|
+
return;
|
|
26
|
+
const raw = readFileSync(entry, 'utf8'), header = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw)?.[1];
|
|
27
|
+
let meta;
|
|
28
|
+
try {
|
|
29
|
+
meta = parse(header ?? '') ?? {};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (typeof meta.name !== 'string' || !namePattern.test(meta.name) || seen.has(meta.name))
|
|
35
|
+
return;
|
|
36
|
+
const files = [];
|
|
37
|
+
let size = 0, supported = true;
|
|
38
|
+
function collect(path, prefix = '', level = 0) {
|
|
39
|
+
if (level > 10) {
|
|
40
|
+
supported = false;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const name of readdirSync(path).sort()) {
|
|
44
|
+
const file = join(path, name), s = lstatSync(file);
|
|
45
|
+
if (s.isSymbolicLink()) {
|
|
46
|
+
supported = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (s.isDirectory()) {
|
|
50
|
+
collect(file, prefix + name + '/', level + 1);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!s.isFile() || s.size > 512 * 1024 || (size += s.size) > 1024 * 1024 || files.length >= 128) {
|
|
54
|
+
supported = false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!['.md', '.txt'].includes(extname(name).toLowerCase()) && name !== 'LICENSE')
|
|
58
|
+
supported = false;
|
|
59
|
+
files.push([prefix + name, createHash('sha256').update(readFileSync(file)).digest('hex')]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
collect(folder);
|
|
63
|
+
files.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
64
|
+
const canonical = JSON.stringify(files).replace(/[\u007f-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
65
|
+
seen.add(meta.name);
|
|
66
|
+
found.push({ name: meta.name, path: resolve(folder), description: String(meta.description ?? '').slice(0, 500), supported, digest: createHash('sha256').update(canonical).digest('hex'), files: files.length });
|
|
67
|
+
}
|
|
68
|
+
for (const root of roots.slice(0, 16))
|
|
69
|
+
if (typeof root === 'string')
|
|
70
|
+
visit(resolve(root.replace(/^~(?=\/|$)/, homedir())), 0);
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare function openclawRoots(home?: string): string[];
|
|
2
|
+
export declare function openclawInventory(home?: string): {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
description: string;
|
|
6
|
+
supported: boolean;
|
|
7
|
+
digest: string;
|
|
8
|
+
files: number;
|
|
9
|
+
}[];
|
|
10
|
+
export declare function probeOpenClaw(home?: string, python?: string): {
|
|
11
|
+
home: string;
|
|
12
|
+
python: string;
|
|
13
|
+
command: string[];
|
|
14
|
+
model: string;
|
|
15
|
+
baseUrl: any;
|
|
16
|
+
apiKeyFile: null;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
skillRoots: string[];
|
|
19
|
+
skills: {
|
|
20
|
+
textPackage: boolean;
|
|
21
|
+
name: string;
|
|
22
|
+
path: string;
|
|
23
|
+
description: string;
|
|
24
|
+
supported: boolean;
|
|
25
|
+
digest: string;
|
|
26
|
+
files: number;
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Read local OpenClaw configuration and installed Skill metadata without invoking a model. */
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { readSkillInventory } from './native-inventory.js';
|
|
7
|
+
import { CommerceError } from './types.js';
|
|
8
|
+
function configuration(home) {
|
|
9
|
+
const path = home ? join(resolve(home), 'openclaw.json') : process.env.OPENCLAW_CONFIG_PATH ?? join(process.env.OPENCLAW_STATE_DIR ?? join(homedir(), '.openclaw'), 'openclaw.json');
|
|
10
|
+
const stat = lstatSync(path);
|
|
11
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
|
|
12
|
+
throw new CommerceError('setup_model', 'Use a bounded local OpenClaw configuration file');
|
|
13
|
+
return { home: dirname(resolve(path)), config: JSON.parse(readFileSync(path, 'utf8')) };
|
|
14
|
+
}
|
|
15
|
+
export function openclawRoots(home) {
|
|
16
|
+
const local = configuration(home), config = local.config, defaults = config.agents?.defaults ?? {};
|
|
17
|
+
const agent = config.agents?.list?.find((a) => a.default || a.id === 'main');
|
|
18
|
+
return [join(local.home, 'skills'), join(agent?.workspace ?? defaults.workspace ?? join(local.home, 'workspace'), 'skills'), ...(config.skills?.load?.extraDirs ?? [])].filter((r) => typeof r === 'string');
|
|
19
|
+
}
|
|
20
|
+
export function openclawInventory(home) { return readSkillInventory(openclawRoots(home)); }
|
|
21
|
+
export function probeOpenClaw(home, python = 'python3') {
|
|
22
|
+
const local = configuration(home), config = local.config;
|
|
23
|
+
const agent = config.agents?.list?.find((a) => a.default || a.id === 'main');
|
|
24
|
+
const selected = agent?.model ?? config.agents?.defaults?.model;
|
|
25
|
+
const primary = typeof selected === 'string' ? selected : selected?.primary;
|
|
26
|
+
if (typeof primary !== 'string' || !primary.includes('/'))
|
|
27
|
+
throw new CommerceError('setup_model', 'Configure an OpenAI-compatible OpenClaw model first');
|
|
28
|
+
const slash = primary.indexOf('/'), provider = config.models?.providers?.[primary.slice(0, slash)], model = primary.slice(slash + 1);
|
|
29
|
+
let key = provider?.apiKey;
|
|
30
|
+
if (typeof key === 'string') {
|
|
31
|
+
const env = /^\$\{([A-Z_][A-Z0-9_]*)\}$/.exec(key);
|
|
32
|
+
if (env)
|
|
33
|
+
key = process.env[env[1]];
|
|
34
|
+
}
|
|
35
|
+
else if (key?.source === 'env' && typeof key.id === 'string')
|
|
36
|
+
key = process.env[key.id];
|
|
37
|
+
if (typeof key !== 'string' || !key.trim() || typeof provider?.baseUrl !== 'string' || (provider.api ?? 'openai-completions') !== 'openai-completions')
|
|
38
|
+
throw new CommerceError('setup_model', 'Use an OpenAI-compatible provider with a locally available API key');
|
|
39
|
+
const command = existsSync('/app/openclaw.mjs') ? [process.execPath, '/app/openclaw.mjs'] : ['openclaw'];
|
|
40
|
+
execFileSync(python, ['--version'], { stdio: 'pipe', timeout: 10000 });
|
|
41
|
+
execFileSync(command[0], [...command.slice(1), '--version'], { stdio: 'pipe', timeout: 30000 });
|
|
42
|
+
return { home: local.home, python, command, model, baseUrl: provider.baseUrl, apiKeyFile: null, apiKey: key, skillRoots: openclawRoots(local.home), skills: openclawInventory(local.home).map(s => ({ ...s, textPackage: s.supported })) };
|
|
43
|
+
}
|
package/dist/commerce/setup.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export interface SetupBundle {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function parseSetupBundle(raw: unknown): SetupBundle;
|
|
11
11
|
export declare function prepareTextSkill(source: string, destination: string, name: string): void;
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function setupAgent(options: {
|
|
13
|
+
framework?: "hermes" | "openclaw";
|
|
14
|
+
managed?: boolean;
|
|
13
15
|
file: string;
|
|
14
16
|
directory?: string;
|
|
15
17
|
container?: string;
|
|
@@ -20,3 +22,4 @@ export declare function setupHermes(options: {
|
|
|
20
22
|
origin: string;
|
|
21
23
|
};
|
|
22
24
|
}): Promise<void>;
|
|
25
|
+
export declare const setupHermes: (options: Parameters<typeof setupAgent>[0]) => Promise<void>;
|
package/dist/commerce/setup.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { probeOpenClaw } from "./openclaw-setup.js";
|
|
9
10
|
import { readInstalledSkills } from "./native-skills.js";
|
|
10
11
|
import { loadCommerceConfig, validateUrl } from "./config.js";
|
|
11
12
|
import { CommerceError } from "./types.js";
|
|
@@ -62,7 +63,7 @@ function privateJson(path, value) {
|
|
|
62
63
|
flag: "wx",
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
|
-
export async function
|
|
66
|
+
export async function setupAgent(options) {
|
|
66
67
|
const file = resolve(options.file), info = lstatSync(file);
|
|
67
68
|
if (!info.isFile() || info.isSymbolicLink() || info.size > 16384)
|
|
68
69
|
throw new CommerceError("setup_file", "Use the downloaded setup JSON file");
|
|
@@ -83,65 +84,75 @@ export async function setupHermes(options) {
|
|
|
83
84
|
const directory = resolve(options.directory ?? join(homedir(), ".envarpay", bundle.agentId));
|
|
84
85
|
if (existsSync(directory))
|
|
85
86
|
throw new CommerceError("setup_exists", "This Agent already has a local directory. Use its start script; never reset its ledger.");
|
|
87
|
+
const configuredFramework = process.env.ENVARPAY_FRAMEWORK;
|
|
88
|
+
const framework = options.framework ?? (configuredFramework === "hermes" || configuredFramework === "openclaw" ? configuredFramework : process.env.OPENCLAW_STATE_DIR ? "openclaw" : "hermes");
|
|
86
89
|
let python = options.python;
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
let probe;
|
|
91
|
+
if (framework === "openclaw") {
|
|
92
|
+
if (options.container)
|
|
93
|
+
throw new CommerceError("setup_runtime", "Run OpenClaw setup inside its own runtime");
|
|
94
|
+
python ??= "python3";
|
|
95
|
+
probe = probeOpenClaw(options.home, python);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const compatibility = 'import inspect;from run_agent import AIAgent;assert all(k in inspect.signature(AIAgent).parameters for k in ["skip_context_files","skip_memory","skip_background_review","ephemeral_system_prompt","enabled_toolsets"])';
|
|
99
|
+
if (!python && options.container) {
|
|
100
|
+
for (const candidate of [
|
|
101
|
+
"/opt/hermes-latest/bin/python",
|
|
102
|
+
"/opt/hermes/.venv/bin/python",
|
|
103
|
+
"python3",
|
|
104
|
+
]) {
|
|
105
|
+
try {
|
|
106
|
+
execFileSync("docker", ["exec", options.container, candidate, "-c", compatibility], { stdio: "pipe", timeout: 60000 });
|
|
107
|
+
python = candidate;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* try the next compatible installed interpreter */
|
|
112
|
+
}
|
|
101
113
|
}
|
|
114
|
+
if (!python)
|
|
115
|
+
throw new CommerceError("setup_runtime", "No compatible Hermes interpreter found in this container. Update Hermes or pass --python PATH.");
|
|
102
116
|
}
|
|
103
|
-
if (!python)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
break;
|
|
117
|
+
if (!python && options.automatic) {
|
|
118
|
+
for (const candidate of [process.env.VIRTUAL_ENV && join(process.env.VIRTUAL_ENV, "bin/python"), "/opt/hermes-latest/bin/python", "/opt/hermes/.venv/bin/python", join(homedir(), "hermes-agent/.venv/bin/python"), "python3"].filter(Boolean)) {
|
|
119
|
+
try {
|
|
120
|
+
execFileSync(candidate, ["-c", compatibility], { stdio: "pipe", timeout: 30000 });
|
|
121
|
+
python = candidate;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
112
125
|
}
|
|
113
|
-
|
|
126
|
+
if (!python)
|
|
127
|
+
throw new CommerceError("setup_runtime", "No compatible Hermes Python found. Pass its executable using --python.");
|
|
128
|
+
}
|
|
129
|
+
python ??= await ask("Hermes Python executable", join(homedir(), "hermes-agent", ".venv", "bin", "python"));
|
|
130
|
+
const probeSource = readFileSync(new URL("../../examples/hermes-setup-probe.py", import.meta.url), "utf8");
|
|
131
|
+
try {
|
|
132
|
+
if (options.container)
|
|
133
|
+
execFileSync("docker", ["exec", options.container, python, "-c", compatibility], { stdio: "pipe", timeout: 60000 });
|
|
134
|
+
else
|
|
135
|
+
execFileSync(python, ["-c", compatibility], {
|
|
136
|
+
stdio: "pipe",
|
|
137
|
+
timeout: 60000,
|
|
138
|
+
});
|
|
139
|
+
const args = ["-c", probeSource, ...(options.home ? [options.home] : [])];
|
|
140
|
+
const data = options.container
|
|
141
|
+
? execFileSync("docker", ["exec", options.container, python, ...args], {
|
|
142
|
+
stdio: "pipe",
|
|
143
|
+
timeout: 30000,
|
|
144
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
145
|
+
})
|
|
146
|
+
: execFileSync(python, args, {
|
|
147
|
+
stdio: "pipe",
|
|
148
|
+
timeout: 30000,
|
|
149
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
150
|
+
});
|
|
151
|
+
probe = JSON.parse(data.toString());
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
throw new CommerceError("setup_model", "Could not read Hermes. Check the container/Python path and configure an OpenAI-compatible model in Hermes first.");
|
|
114
155
|
}
|
|
115
|
-
if (!python)
|
|
116
|
-
throw new CommerceError("setup_runtime", "No compatible Hermes Python found. Pass its executable using --python.");
|
|
117
|
-
}
|
|
118
|
-
python ??= await ask("Hermes Python executable", join(homedir(), "hermes-agent", ".venv", "bin", "python"));
|
|
119
|
-
const probeSource = readFileSync(new URL("../../examples/hermes-setup-probe.py", import.meta.url), "utf8");
|
|
120
|
-
let probe;
|
|
121
|
-
try {
|
|
122
|
-
if (options.container)
|
|
123
|
-
execFileSync("docker", ["exec", options.container, python, "-c", compatibility], { stdio: "pipe", timeout: 60000 });
|
|
124
|
-
else
|
|
125
|
-
execFileSync(python, ["-c", compatibility], {
|
|
126
|
-
stdio: "pipe",
|
|
127
|
-
timeout: 60000,
|
|
128
|
-
});
|
|
129
|
-
const args = ["-c", probeSource, ...(options.home ? [options.home] : [])];
|
|
130
|
-
const data = options.container
|
|
131
|
-
? execFileSync("docker", ["exec", options.container, python, ...args], {
|
|
132
|
-
stdio: "pipe",
|
|
133
|
-
timeout: 30000,
|
|
134
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
135
|
-
})
|
|
136
|
-
: execFileSync(python, args, {
|
|
137
|
-
stdio: "pipe",
|
|
138
|
-
timeout: 30000,
|
|
139
|
-
maxBuffer: 2 * 1024 * 1024,
|
|
140
|
-
});
|
|
141
|
-
probe = JSON.parse(data.toString());
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
throw new CommerceError("setup_model", "Could not read Hermes. Check the container/Python path and configure an OpenAI-compatible model in Hermes first.");
|
|
145
156
|
}
|
|
146
157
|
const candidates = probe.skills.filter((s) => s.textPackage && ID.test(s.name));
|
|
147
158
|
output(`Model: ${probe.model}\nInstalled text packages (review their instructions before selecting):`);
|
|
@@ -154,7 +165,7 @@ export async function setupHermes(options) {
|
|
|
154
165
|
new Set(selected).size !== selected.length ||
|
|
155
166
|
selected.some((name) => !candidates.some((s) => s.name === name)))
|
|
156
167
|
throw new CommerceError("setup_skills", "Choose installed skill names from the list");
|
|
157
|
-
output("This adapter supplies buyer text and bundled Markdown references. Browser, script, file and API tools are disabled. Original
|
|
168
|
+
output("This adapter supplies buyer text and bundled Markdown references. Browser, script, file and API tools are disabled. Original Agent skills are preserved; a service copy is created.");
|
|
158
169
|
if (selected.length &&
|
|
159
170
|
(await ask("Do these selected skills work with text input and text output only? (yes/no)", "no")).toLowerCase() !== "yes")
|
|
160
171
|
return;
|
|
@@ -231,6 +242,13 @@ export async function setupHermes(options) {
|
|
|
231
242
|
readInstalledSkills(skillsDirectory, [], origin + "/.well-known/agent-card.json");
|
|
232
243
|
const access = randomBytes(36).toString("base64url");
|
|
233
244
|
let apiKeyFile = probe.apiKeyFile;
|
|
245
|
+
if (options.managed && apiKeyFile) {
|
|
246
|
+
const stat = lstatSync(apiKeyFile);
|
|
247
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16384)
|
|
248
|
+
throw new CommerceError("setup_model", "Invalid local model key file");
|
|
249
|
+
probe.apiKey = readFileSync(apiKeyFile, "utf8");
|
|
250
|
+
apiKeyFile = null;
|
|
251
|
+
}
|
|
234
252
|
if (!apiKeyFile) {
|
|
235
253
|
if (options.container) {
|
|
236
254
|
apiKeyFile = join(probe.home, "envarpay-secrets", bundle.agentId, "model.key");
|
|
@@ -279,8 +297,9 @@ export async function setupHermes(options) {
|
|
|
279
297
|
callers: { [access]: "owner" },
|
|
280
298
|
upstreams: {},
|
|
281
299
|
skills: {
|
|
282
|
-
framework
|
|
283
|
-
|
|
300
|
+
framework,
|
|
301
|
+
...(probe.command ? { command: probe.command } : {}),
|
|
302
|
+
discovery: { hermesHome: probe.home, enabledFromWeb: true, ...(probe.skillRoots ? { roots: probe.skillRoots } : {}) },
|
|
284
303
|
skillsDirectory: join(directory, "skills"),
|
|
285
304
|
freeSkills: [],
|
|
286
305
|
stateDirectory,
|
|
@@ -352,3 +371,5 @@ export async function setupHermes(options) {
|
|
|
352
371
|
rmSync(staging, { recursive: true, force: true });
|
|
353
372
|
}
|
|
354
373
|
}
|
|
374
|
+
// Preserve the existing SDK helper name for callers using Hermes explicitly.
|
|
375
|
+
export const setupHermes = (options) => setupAgent({ ...options, framework: "hermes" });
|
|
@@ -18,7 +18,8 @@ def inspect(home):
|
|
|
18
18
|
model = config.get("model", {})
|
|
19
19
|
if isinstance(model, str):
|
|
20
20
|
model = {"default": model}
|
|
21
|
-
|
|
21
|
+
provider_id = str(model.get("provider", "")).removeprefix("custom:")
|
|
22
|
+
provider = config.get("providers", {}).get(provider_id, {})
|
|
22
23
|
dotenv = {}
|
|
23
24
|
if (home / ".env").exists():
|
|
24
25
|
for line in (home / ".env").read_text().splitlines():
|
|
@@ -32,6 +33,8 @@ def inspect(home):
|
|
|
32
33
|
key = (
|
|
33
34
|
provider.get("api_key")
|
|
34
35
|
or provider.get("key")
|
|
36
|
+
or dotenv.get(provider.get("key_env", ""))
|
|
37
|
+
or os.environ.get(provider.get("key_env", ""))
|
|
35
38
|
or dotenv.get("OPENAI_API_KEY")
|
|
36
39
|
or os.environ.get("OPENAI_API_KEY")
|
|
37
40
|
)
|
|
@@ -90,6 +93,7 @@ def inspect(home):
|
|
|
90
93
|
"apiKeyFile": key_file,
|
|
91
94
|
"apiKey": None if key_file else key,
|
|
92
95
|
"skills": skills,
|
|
96
|
+
"skillRoots": [str(root) for root in roots],
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envarai/envarpay",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.14",
|
|
4
4
|
"description": "Independent A2A service commerce, budgeted buyers and x402/MPP payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"ajv-formats": "3.0.1",
|
|
69
69
|
"canonicalize": "2.1.0",
|
|
70
70
|
"mppx": "0.13.1",
|
|
71
|
-
"viem": "2.57.1"
|
|
71
|
+
"viem": "2.57.1",
|
|
72
|
+
"yaml": "2.8.2"
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
|
74
75
|
"@types/node": "^22.0.0",
|