@envarai/envarpay 0.2.0-alpha.2 → 0.2.0-alpha.20
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 +27 -0
- package/dist/commerce/alerts.js +71 -0
- package/dist/commerce/card.js +3 -3
- package/dist/commerce/cli.js +91 -119
- package/dist/commerce/config.d.ts +3 -2
- package/dist/commerce/config.js +19 -77
- 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 +3 -1
- package/dist/commerce/envar-runtime.js +28 -1
- 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/index.d.ts +1 -0
- package/dist/commerce/index.js +1 -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 +32 -0
- package/dist/commerce/native-skills.js +174 -0
- package/dist/commerce/openclaw-setup.d.ts +28 -0
- package/dist/commerce/openclaw-setup.js +43 -0
- package/dist/commerce/operations.d.ts +8 -3
- package/dist/commerce/operations.js +21 -18
- package/dist/commerce/pricing.d.ts +0 -1
- package/dist/commerce/pricing.js +3 -5
- package/dist/commerce/runtime.d.ts +4 -2
- package/dist/commerce/runtime.js +4 -2
- package/dist/commerce/server.d.ts +3 -2
- package/dist/commerce/server.js +26 -10
- package/dist/commerce/setup.d.ts +25 -0
- package/dist/commerce/setup.js +379 -0
- package/dist/commerce/skill-gate.d.ts +54 -0
- package/dist/commerce/skill-gate.js +153 -0
- package/dist/commerce/store.d.ts +11 -1
- package/dist/commerce/store.js +36 -2
- package/dist/commerce/types.d.ts +6 -57
- package/examples/envar-integration-policy.json +1 -9
- package/examples/hermes-setup-probe.py +109 -0
- package/examples/hermes-skill-inventory.py +108 -0
- package/examples/native-skill-runner.py +197 -0
- package/examples/seller.json +143 -26
- package/package.json +5 -16
- package/schemas/envarpay.config.schema.json +28 -87
- package/dist/commerce/buyer-store.d.ts +0 -101
- package/dist/commerce/buyer-store.js +0 -168
- package/dist/commerce/buyer.d.ts +0 -132
- package/dist/commerce/buyer.js +0 -755
- 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 -138
- package/dist/commerce/mpp-client.d.ts +0 -59
- package/dist/commerce/mpp-client.js +0 -24
- package/dist/commerce/mpp-runtime.d.ts +0 -3
- package/dist/commerce/mpp-runtime.js +0 -3
- 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-provider.d.ts +0 -9
- package/dist/commerce/stripe-provider.js +0 -97
- package/dist/commerce/x402.d.ts +0 -37
- package/dist/commerce/x402.js +0 -174
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/** Owner-operated, instruction-only skill runtime. No model file, shell or network tools. */
|
|
2
|
+
import { AgentCard, Task } from '@a2a-js/sdk';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { lstatSync, readFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { resolve, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
9
|
+
import { digest } from './config.js';
|
|
10
|
+
import { CommerceError } from './types.js';
|
|
11
|
+
import { skillScopedExecutor } from './skill-gate.js';
|
|
12
|
+
const namePattern = /^[a-z][a-z0-9-]{0,63}$/;
|
|
13
|
+
/** Hash every resource; reject symlinks, unbounded packages and executable skill requirements. */
|
|
14
|
+
export function readInstalledSkills(root, freeSkills, cardUrl) {
|
|
15
|
+
const base = resolve(root), rootStat = lstatSync(base);
|
|
16
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
17
|
+
throw new CommerceError('skill_directory', 'Use a real installed skill directory');
|
|
18
|
+
const output = [];
|
|
19
|
+
for (const entry of readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
20
|
+
if (entry.name.startsWith('.'))
|
|
21
|
+
continue;
|
|
22
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !namePattern.test(entry.name))
|
|
23
|
+
throw new CommerceError('skill_package', 'Use one real directory per exact skill name');
|
|
24
|
+
const files = [];
|
|
25
|
+
let total = 0;
|
|
26
|
+
function walk(folder, prefix = '') {
|
|
27
|
+
if (prefix.split('/').length > 12)
|
|
28
|
+
throw new CommerceError('skill_size', 'Skill package nesting exceeds the supported limit');
|
|
29
|
+
for (const item of readdirSync(folder, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
30
|
+
const path = join(folder, item.name), relative = prefix + item.name;
|
|
31
|
+
if (item.isSymbolicLink())
|
|
32
|
+
throw new CommerceError('skill_symlink', 'Skill packages cannot contain symlinks');
|
|
33
|
+
if (item.isDirectory()) {
|
|
34
|
+
walk(path, relative + '/');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const stat = lstatSync(path);
|
|
38
|
+
if (!stat.isFile() || stat.size > 512 * 1024 || files.length >= 128 || (total += stat.size) > 1024 * 1024)
|
|
39
|
+
throw new CommerceError('skill_size', 'Skill package exceeds supported size');
|
|
40
|
+
files.push({ path: relative, sha256: createHash('sha256').update(readFileSync(path)).digest('hex') });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(join(base, entry.name));
|
|
44
|
+
const instructions = readFileSync(join(base, entry.name, 'SKILL.md'), 'utf8');
|
|
45
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(instructions)?.[1];
|
|
46
|
+
const name = /^name:\s*["']?([a-z][a-z0-9-]*)["']?\s*$/m.exec(front ?? '')?.[1];
|
|
47
|
+
if (name !== entry.name || instructions.length > 48000)
|
|
48
|
+
throw new CommerceError('skill_name_mismatch', 'SKILL.md name must match its installed directory');
|
|
49
|
+
// This adapter deliberately supports only text-in/text-out workflows. Owners must use
|
|
50
|
+
// a separately reviewed adapter for skills needing code, browsing, external APIs or files.
|
|
51
|
+
if (!/^envar-runtime:\s*instruction-only\s*$/m.test(front ?? ''))
|
|
52
|
+
throw new CommerceError('skill_runtime_required', 'Review and mark each compatible skill envar-runtime: instruction-only before publication');
|
|
53
|
+
const description = /^description:\s*(.+)$/m.exec(front ?? '')?.[1]?.replace(/^["']|["']$/g, '') ?? name;
|
|
54
|
+
const references = files.filter(file => file.path !== 'SKILL.md' && file.path.endsWith('.md')).map(file => `\n\n<skill-reference path="${file.path}">\n${readFileSync(join(base, entry.name, file.path), 'utf8')}\n</skill-reference>`).join('');
|
|
55
|
+
if (instructions.length + references.length > 120000)
|
|
56
|
+
throw new CommerceError('skill_size', 'Instruction resources exceed the supported context size');
|
|
57
|
+
output.push({ descriptor: { name, digest: digest(files), cardUrl, access: freeSkills.includes(name) ? 'free' : 'paid', description }, instructions: instructions + references });
|
|
58
|
+
}
|
|
59
|
+
if (output.length > 32 || freeSkills.some(name => !output.some(s => s.descriptor.name === name)))
|
|
60
|
+
throw new CommerceError('skill_inventory', 'Install at most 32 skills and declare only installed free skills');
|
|
61
|
+
// Free helpers are executable dependencies of every purchase, so their versions
|
|
62
|
+
// and access-policy changes must invalidate the quote's selected skill digest too.
|
|
63
|
+
const helpers = output.filter(s => s.descriptor.access === 'free').map(s => ({ name: s.descriptor.name, digest: s.descriptor.digest }));
|
|
64
|
+
for (const skill of output)
|
|
65
|
+
skill.descriptor.digest = digest({ package: skill.descriptor.digest, helpers });
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
export function createNativeSkillExecutor(settings, origin, agentName) {
|
|
69
|
+
if (!['hermes', 'openclaw'].includes(settings.framework) || !Array.isArray(settings.freeSkills) || !settings.python || !settings.model || !settings.baseUrl)
|
|
70
|
+
throw new CommerceError('skill_settings', 'Configure a supported native skill runtime');
|
|
71
|
+
const cardUrl = new URL('/.well-known/agent-card.json', origin).href;
|
|
72
|
+
const installed = () => readInstalledSkills(settings.skillsDirectory, settings.freeSkills, cardUrl);
|
|
73
|
+
installed();
|
|
74
|
+
mkdirSync(settings.stateDirectory, { recursive: true, mode: 0o700 });
|
|
75
|
+
const stateStat = lstatSync(settings.stateDirectory);
|
|
76
|
+
if (!stateStat.isDirectory() || stateStat.isSymbolicLink() || (stateStat.mode & 0o077) !== 0)
|
|
77
|
+
throw new CommerceError('skill_state', 'Skill state must be owner-only');
|
|
78
|
+
const db = new DatabaseSync(join(settings.stateDirectory, 'skills.sqlite3'));
|
|
79
|
+
db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; CREATE TABLE IF NOT EXISTS skill_tasks (id TEXT PRIMARY KEY, order_id TEXT UNIQUE NOT NULL, binding TEXT NOT NULL, task_json TEXT NOT NULL)');
|
|
80
|
+
for (const row of db.prepare('SELECT id,task_json FROM skill_tasks').all()) {
|
|
81
|
+
const task = JSON.parse(String(row.task_json));
|
|
82
|
+
if (task.status?.state === 'TASK_STATE_WORKING') {
|
|
83
|
+
task.status = { state: 'TASK_STATE_FAILED', message: { role: 'ROLE_AGENT', messageId: randomUUID(), parts: [{ text: 'Native execution interrupted; inspect this original order' }] } };
|
|
84
|
+
db.prepare('UPDATE skill_tasks SET task_json=? WHERE id=?').run(JSON.stringify(task), row.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const inFlight = new Map();
|
|
88
|
+
const runner = fileURLToPath(new URL('../../examples/native-skill-runner.py', import.meta.url));
|
|
89
|
+
function readTask(id, invocation) {
|
|
90
|
+
const row = db.prepare('SELECT order_id,task_json FROM skill_tasks WHERE id=?').get(id);
|
|
91
|
+
if (!row || row.order_id !== invocation.orderId)
|
|
92
|
+
throw new CommerceError('skill_task_not_found', 'Original skill Task not found');
|
|
93
|
+
return Task.fromJSON(JSON.parse(String(row.task_json)));
|
|
94
|
+
}
|
|
95
|
+
function executeProcess(payload) {
|
|
96
|
+
return new Promise((accept, reject) => {
|
|
97
|
+
const docker = settings.dockerContainer;
|
|
98
|
+
if (docker && !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(docker))
|
|
99
|
+
throw new CommerceError('skill_container', 'Invalid Docker container name');
|
|
100
|
+
const child = docker
|
|
101
|
+
? spawn('docker', ['exec', '-i', docker, settings.python, '-c', readFileSync(runner, 'utf8')], { stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: process.env.PATH, HOME: process.env.HOME, DOCKER_CONFIG: process.env.DOCKER_CONFIG, DOCKER_HOST: process.env.DOCKER_HOST } })
|
|
102
|
+
: spawn(settings.python, [runner], { stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: process.env.PATH, LANG: 'C.UTF-8', PYTHONUNBUFFERED: '1' } });
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let size = 0;
|
|
105
|
+
const timeout = setTimeout(() => child.kill('SIGTERM'), 240000);
|
|
106
|
+
child.stdout.on('data', (chunk) => { size += chunk.length; if (size > 1024 * 1024)
|
|
107
|
+
child.kill('SIGTERM');
|
|
108
|
+
else
|
|
109
|
+
chunks.push(chunk); });
|
|
110
|
+
child.stderr.resume();
|
|
111
|
+
child.on('error', () => { clearTimeout(timeout); reject(new CommerceError('skill_runtime_start', 'Native skill runtime could not start')); });
|
|
112
|
+
child.on('close', code => {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
try {
|
|
115
|
+
if (code !== 0 || size > 1024 * 1024)
|
|
116
|
+
throw Error();
|
|
117
|
+
const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
118
|
+
if (typeof result.output !== 'string' || !result.output.trim() || result.framework !== settings.framework || !Array.isArray(result.tools) || result.tools.length)
|
|
119
|
+
throw Error();
|
|
120
|
+
accept(result);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
reject(new CommerceError('skill_runtime_failed', 'Native skill execution failed; inspect the original Task without resubmitting'));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
child.stdin.on('error', () => { });
|
|
127
|
+
child.stdin.end(JSON.stringify(payload));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const runtime = {
|
|
131
|
+
list: () => installed().map(s => s.descriptor),
|
|
132
|
+
async send(name, input, invocation) {
|
|
133
|
+
const skills = installed(), selected = skills.find(s => s.descriptor.name === name);
|
|
134
|
+
if (!selected || selected.descriptor.digest !== invocation.grant.skillDigest)
|
|
135
|
+
throw new CommerceError('skill_changed', 'The purchased skill version is no longer installed');
|
|
136
|
+
invocation.grant.require(name);
|
|
137
|
+
const allowed = skills.filter(s => s.descriptor.name === name || s.descriptor.access === 'free');
|
|
138
|
+
allowed.forEach(s => invocation.grant.require(s.descriptor.name));
|
|
139
|
+
const binding = digest({ name, version: selected.descriptor.digest, caller: invocation.grant.caller, input, allowed: allowed.map(s => s.descriptor) });
|
|
140
|
+
const prior = db.prepare('SELECT id,binding FROM skill_tasks WHERE order_id=?').get(invocation.orderId);
|
|
141
|
+
if (prior) {
|
|
142
|
+
if (prior.binding !== binding)
|
|
143
|
+
throw new CommerceError('skill_task_conflict', 'An order already identifies a different skill input');
|
|
144
|
+
return readTask(String(prior.id), invocation);
|
|
145
|
+
}
|
|
146
|
+
const id = randomUUID();
|
|
147
|
+
// A crash cannot cause a second native dispatch. The durable Task remains failed until owner inspection.
|
|
148
|
+
const pending = Task.fromJSON({ id, status: { state: 'TASK_STATE_WORKING' } });
|
|
149
|
+
db.prepare('INSERT INTO skill_tasks VALUES (?,?,?,?)').run(id, invocation.orderId, binding, JSON.stringify(Task.toJSON(pending)));
|
|
150
|
+
const operation = (async () => {
|
|
151
|
+
let task;
|
|
152
|
+
try {
|
|
153
|
+
const result = await executeProcess({ settings: { ...settings, stateDirectory: join(settings.dockerStateDirectory ?? settings.stateDirectory, id) }, skillName: name, skills: allowed.map(s => ({ name: s.descriptor.name, instructions: s.instructions })), input });
|
|
154
|
+
task = Task.fromJSON({ id, status: { state: 'TASK_STATE_COMPLETED' }, artifacts: [{ artifactId: 'result', name: name + '.md', parts: [{ text: result.output, mediaType: 'text/markdown' }] }], metadata: { skillName: name, skillDigest: selected.descriptor.digest, allowedSkills: allowed.map(s => s.descriptor.name), framework: settings.framework, tools: result.tools, sessionId: result.sessionId ?? id } });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
task = Task.fromJSON({ id, status: { state: 'TASK_STATE_FAILED' }, metadata: { errorCode: error instanceof CommerceError ? error.code : 'skill_runtime_failed' } });
|
|
158
|
+
}
|
|
159
|
+
db.prepare('UPDATE skill_tasks SET task_json=? WHERE id=?').run(JSON.stringify(Task.toJSON(task)), id);
|
|
160
|
+
return task;
|
|
161
|
+
})();
|
|
162
|
+
inFlight.set(id, operation);
|
|
163
|
+
void operation.finally(() => inFlight.delete(id));
|
|
164
|
+
return pending;
|
|
165
|
+
},
|
|
166
|
+
async getTask(_name, id, invocation) { return readTask(id, invocation); },
|
|
167
|
+
};
|
|
168
|
+
const execute = skillScopedExecutor(runtime);
|
|
169
|
+
execute.skillCard = () => {
|
|
170
|
+
const skills = installed();
|
|
171
|
+
return AgentCard.fromJSON({ name: agentName, description: 'Installed Agent Skills', version: '1.0', supportedInterfaces: [{ url: new URL('/a2a', origin).href, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }], capabilities: { extensions: [{ uri: 'urn:envarpay:skill-gate:1', required: false, params: { runtime: 'instruction-only-v1', skills: skills.map(s => ({ name: s.descriptor.name, digest: s.descriptor.digest, access: s.descriptor.access })) } }] }, defaultInputModes: ['application/json'], defaultOutputModes: ['text/markdown'], skills: skills.map(s => ({ id: s.descriptor.name, name: s.descriptor.name, description: s.descriptor.description, tags: [s.descriptor.name] })) });
|
|
172
|
+
};
|
|
173
|
+
return { execute, close: async () => { await Promise.allSettled(inFlight.values()); db.close(); } };
|
|
174
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
1
|
+
export { inspectLocalAlerts, type LocalAlertOptions } from './alerts.js';
|
|
2
|
+
export type InspectionView = 'attention' | 'refund-review';
|
|
3
|
+
export declare function inspectLedger(path: string, after?: number, limit?: number, view?: InspectionView): {
|
|
4
|
+
role: "seller";
|
|
5
|
+
view: InspectionView;
|
|
4
6
|
total: number;
|
|
5
7
|
attentionCount: number;
|
|
8
|
+
refundReviewCount: number;
|
|
6
9
|
items: {
|
|
10
|
+
reason?: "confirmed_payment_failed_execution" | undefined;
|
|
7
11
|
cursor: number;
|
|
8
12
|
id: string;
|
|
9
13
|
paymentState: string;
|
|
@@ -13,4 +17,5 @@ export declare function inspectLedger(path: string, after?: number, limit?: numb
|
|
|
13
17
|
nextCursor: number;
|
|
14
18
|
hasMore: boolean;
|
|
15
19
|
paymentPerformed: boolean;
|
|
20
|
+
refundPerformed: boolean;
|
|
16
21
|
};
|
|
@@ -1,31 +1,34 @@
|
|
|
1
|
+
/** Owner-only, read-only inspection of original seller orders. */
|
|
1
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
3
|
import { lstatSync } from 'node:fs';
|
|
3
4
|
import { CommerceError } from './types.js';
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
export { inspectLocalAlerts } from './alerts.js';
|
|
6
|
+
const ATTENTION = "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
|
|
7
|
+
const REFUND_REVIEW = "payment_state='confirmed' AND execution_state='failed'";
|
|
8
|
+
export function inspectLedger(path, after = 0, limit = 100, view = 'attention') {
|
|
6
9
|
const stat = lstatSync(path);
|
|
7
10
|
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
8
11
|
throw new CommerceError('private_file', 'Inspect an existing ledger owned only by the current user');
|
|
9
12
|
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
10
|
-
throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit
|
|
13
|
+
throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit from 1 to 100');
|
|
14
|
+
if (!['attention', 'refund-review'].includes(view))
|
|
15
|
+
throw new CommerceError('inspection_view', 'Use attention or refund-review inspection');
|
|
11
16
|
const db = new DatabaseSync(path, { readOnly: true });
|
|
12
17
|
try {
|
|
13
18
|
db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${filter}`).get().n);
|
|
28
|
-
return { role, total, attentionCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false };
|
|
19
|
+
if (db.prepare('SELECT version FROM commerce_meta').get()?.version !== 2)
|
|
20
|
+
throw new CommerceError('store_version', 'Unsupported seller ledger version');
|
|
21
|
+
const total = Number(db.prepare('SELECT COUNT(*) AS n FROM commerce_orders').get().n);
|
|
22
|
+
const filter = view === 'refund-review' ? REFUND_REVIEW : ATTENTION;
|
|
23
|
+
const rows = db.prepare(`SELECT rowid AS cursor,id,payment_state,execution_state,task_id FROM commerce_orders WHERE rowid>? AND (${filter}) ORDER BY rowid LIMIT ?`).all(after, limit + 1);
|
|
24
|
+
const page = rows.slice(0, limit).map(row => ({
|
|
25
|
+
cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state),
|
|
26
|
+
executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null,
|
|
27
|
+
...(view === 'refund-review' ? { reason: 'confirmed_payment_failed_execution' } : {}),
|
|
28
|
+
}));
|
|
29
|
+
const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM commerce_orders WHERE ${ATTENTION}`).get().n);
|
|
30
|
+
const refundReviewCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM commerce_orders WHERE ${REFUND_REVIEW}`).get().n);
|
|
31
|
+
return { role: 'seller', view, total, attentionCount, refundReviewCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false, refundPerformed: false };
|
|
29
32
|
}
|
|
30
33
|
finally {
|
|
31
34
|
db.close();
|
|
@@ -7,6 +7,5 @@ export declare function buildQuote(config: CommerceConfig, args: {
|
|
|
7
7
|
input: Input;
|
|
8
8
|
now?: Date;
|
|
9
9
|
ttlSeconds?: number;
|
|
10
|
-
stripeRecipient?: string;
|
|
11
10
|
}): Readonly<PriceQuote>;
|
|
12
11
|
export declare function assertQuoteRequest(quote: Readonly<PriceQuote>, caller: string, messageId: string, input: Input, now?: Date): void;
|
package/dist/commerce/pricing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
3
3
|
import addFormatsImport from 'ajv-formats';
|
|
4
|
-
import { atomic, currencyOf, digest } from './config.js';
|
|
4
|
+
import { atomic, currencyOf, digest, serviceTermsDigest } from './config.js';
|
|
5
5
|
import { CommerceError } from './types.js';
|
|
6
6
|
const addFormats = addFormatsImport;
|
|
7
7
|
export function buildQuote(config, args) {
|
|
@@ -33,9 +33,7 @@ export function buildQuote(config, args) {
|
|
|
33
33
|
if (amount >= 2n ** 256n)
|
|
34
34
|
throw new CommerceError('invalid_amount', 'Quoted amount is too large');
|
|
35
35
|
const profile = offer.paymentProfile ? config.paymentProfiles[offer.paymentProfile] : null;
|
|
36
|
-
const recipient = profile?.
|
|
37
|
-
if (profile?.adapter === 'mpp' && !recipient)
|
|
38
|
-
throw new CommerceError('merchant_not_ready', 'Resolve and verify the Stripe merchant before quoting');
|
|
36
|
+
const recipient = profile?.accountRef ?? null;
|
|
39
37
|
const now = args.now ?? new Date();
|
|
40
38
|
const ttl = args.ttlSeconds ?? 600;
|
|
41
39
|
if (!Number.isInteger(ttl) || ttl < 1 || ttl > 3600 || !Number.isFinite(now.getTime()))
|
|
@@ -43,7 +41,7 @@ export function buildQuote(config, args) {
|
|
|
43
41
|
const quote = {
|
|
44
42
|
quoteVersion: 1, quoteId: randomUUID(), caller: args.caller, serviceId: service.id,
|
|
45
43
|
serviceRevision: service.revision, offerId: offer.id, messageId: args.messageId,
|
|
46
|
-
inputDigest: digest(args.input), termsDigest:
|
|
44
|
+
inputDigest: digest(args.input), termsDigest: serviceTermsDigest(config.configVersion, service, offer, profile),
|
|
47
45
|
issuedAt: now.toISOString(), expiresAt: new Date(now.getTime() + ttl * 1000).toISOString(),
|
|
48
46
|
amount: amount.toString(), currency: profile ? currencyOf(profile) : null,
|
|
49
47
|
recipient: recipient ?? null, paymentProfile: profile ? structuredClone(profile) : null,
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
export * from './store.js';
|
|
3
3
|
export * from './server.js';
|
|
4
4
|
export * from './upstream.js';
|
|
5
|
+
export * from './skill-gate.js';
|
|
6
|
+
export * from './native-skills.js';
|
|
5
7
|
export * from './http.js';
|
|
6
|
-
export * from './
|
|
8
|
+
export * from './envar-balance.js';
|
|
7
9
|
export * from './vault.js';
|
|
8
|
-
export * from './
|
|
10
|
+
export * from './external-agent.js';
|
package/dist/commerce/runtime.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
export * from './store.js';
|
|
3
3
|
export * from './server.js';
|
|
4
4
|
export * from './upstream.js';
|
|
5
|
+
export * from './skill-gate.js';
|
|
6
|
+
export * from './native-skills.js';
|
|
5
7
|
export * from './http.js';
|
|
6
|
-
export * from './
|
|
8
|
+
export * from './envar-balance.js';
|
|
7
9
|
export * from './vault.js';
|
|
8
|
-
export * from './
|
|
10
|
+
export * from './external-agent.js';
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Task, SendMessageRequest } from '@a2a-js/sdk';
|
|
1
|
+
import { AgentCard, Task, SendMessageRequest } from '@a2a-js/sdk';
|
|
2
2
|
import { ServerCallContext } from '@a2a-js/sdk/server';
|
|
3
3
|
import { CommerceStore, type OrderRecord, type ContinuationRecord, type RemoteTaskReference } from './store.js';
|
|
4
4
|
import { type CommerceConfig, type Input, type Service } from './types.js';
|
|
5
5
|
export interface ExecuteOrder {
|
|
6
6
|
(order: OrderRecord, service: Service): AsyncIterable<Task>;
|
|
7
|
+
assertSkill?: (service: Service) => unknown;
|
|
8
|
+
skillCard?: () => AgentCard;
|
|
7
9
|
continue?: (order: OrderRecord, service: Service, continuation: ContinuationRecord, remote: RemoteTaskReference) => AsyncIterable<Task>;
|
|
8
10
|
recover?: (order: OrderRecord, service: Service, remote: RemoteTaskReference) => Promise<Task>;
|
|
9
11
|
remoteInterface?: (task: Task) => string | undefined;
|
|
@@ -21,7 +23,6 @@ export interface CommerceServerOptions {
|
|
|
21
23
|
headers: Record<string, string>;
|
|
22
24
|
}>;
|
|
23
25
|
};
|
|
24
|
-
stripeRecipientFor?: (accountRef: string) => string | undefined;
|
|
25
26
|
ownershipChallenges?: Readonly<Record<string, string>>;
|
|
26
27
|
onError?: (code: string, orderId?: string) => void;
|
|
27
28
|
}
|
package/dist/commerce/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { AgentCard, Role, Task, TaskState, SendMessageRequest } from '@a2a-js/sdk';
|
|
3
3
|
import { DefaultRequestHandler, JsonRpcTransportHandler, ServerCallContext } from '@a2a-js/sdk/server';
|
|
4
|
-
import { RequestMalformedError, TaskNotFoundError, UnsupportedOperationError } from '@a2a-js/sdk/errors';
|
|
4
|
+
import { ContentTypeNotSupportedError, RequestMalformedError, TaskNotFoundError, UnsupportedOperationError, VersionNotSupportedError } from '@a2a-js/sdk/errors';
|
|
5
5
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
6
6
|
import addFormatsImport from 'ajv-formats';
|
|
7
7
|
import { createOfferCard, offerPath } from './card.js';
|
|
@@ -188,6 +188,11 @@ export class CommerceServer {
|
|
|
188
188
|
if (this.stopped)
|
|
189
189
|
throw new CommerceError('server_draining', 'Cannot apply configuration while stopping');
|
|
190
190
|
const config = loadCommerceConfig(value);
|
|
191
|
+
for (const service of config.services.filter(s => s.execution.type === 'skill')) {
|
|
192
|
+
if (!this.options.execute.assertSkill)
|
|
193
|
+
throw new CommerceError('skill_gate_required', 'Skill services require an enforcing skill runtime');
|
|
194
|
+
this.options.execute.assertSkill(service);
|
|
195
|
+
}
|
|
191
196
|
if (config.agent.id !== this.currentConfig.agent.id)
|
|
192
197
|
throw new CommerceError('config_agent_changed', 'Runtime Agent identity cannot change');
|
|
193
198
|
const history = this.store.catalogHistory();
|
|
@@ -207,7 +212,7 @@ export class CommerceServer {
|
|
|
207
212
|
const routes = new Map();
|
|
208
213
|
for (const entry of snapshots.values())
|
|
209
214
|
for (const offer of entry.service.offers) {
|
|
210
|
-
const service = entry.service, historical = { ...config, paymentProfiles: entry.profiles, services: [service] };
|
|
215
|
+
const service = entry.service, historical = { ...config, configVersion: (service.execution.type === 'skill' ? 2 : 1), paymentProfiles: entry.profiles, services: [service] };
|
|
211
216
|
const card = createOfferCard(historical, service.id, offer.id, this.options.origin);
|
|
212
217
|
const active = config.services.some(s => s.id === service.id && s.revision === service.revision);
|
|
213
218
|
routes.set(offerPath(service.id, service.revision, offer.id), { card, rpc: new JsonRpcTransportHandler(new OrderHandler(this, service, offer.id, card, this.store, active)), tenant: `${service.id}:${service.revision}`, service, offerId: offer.id, active });
|
|
@@ -223,17 +228,22 @@ export class CommerceServer {
|
|
|
223
228
|
quote(service, offerId, caller, messageId, input) {
|
|
224
229
|
if (!this.activeRevision(service))
|
|
225
230
|
throw new CommerceError('retired_revision', 'This service revision is retired; recover original Tasks');
|
|
231
|
+
if (service.execution.type === 'skill')
|
|
232
|
+
this.options.execute.assertSkill(service);
|
|
226
233
|
const frozen = this.store.catalogHistory().find(entry => entry.service.id === service.id && entry.service.revision === service.revision);
|
|
227
234
|
if (!frozen)
|
|
228
235
|
throw new CommerceError('unknown_revision', 'Service revision is not registered');
|
|
229
|
-
const config = { ...this.currentConfig, paymentProfiles: frozen.profiles, services: [frozen.service] }
|
|
230
|
-
|
|
231
|
-
return buildQuote(config, { serviceId: service.id, offerId, caller, messageId, input, ...(profile?.adapter === 'mpp' ? { stripeRecipient: this.options.stripeRecipientFor?.(profile.accountRef) } : {}) });
|
|
236
|
+
const config = { ...this.currentConfig, paymentProfiles: frozen.profiles, services: [frozen.service] };
|
|
237
|
+
return buildQuote(config, { serviceId: service.id, offerId, caller, messageId, input });
|
|
232
238
|
}
|
|
233
239
|
async handle(request) {
|
|
234
240
|
const url = new URL(request.url);
|
|
235
241
|
if (url.origin !== new URL(this.options.origin).origin)
|
|
236
242
|
return Response.json({ error: 'invalid_host' }, { status: 421 });
|
|
243
|
+
if (request.method === 'GET' && url.pathname === '/.well-known/agent-card.json' && this.options.execute.skillCard)
|
|
244
|
+
return Response.json(AgentCard.toJSON(this.options.execute.skillCard()), { headers: { 'Cache-Control': 'no-store' } });
|
|
245
|
+
if (url.pathname === '/a2a' && this.options.execute.skillCard)
|
|
246
|
+
return Response.json({ error: 'service_required', message: 'Select a named service offer to invoke a skill' }, { status: 400 });
|
|
237
247
|
if (request.method === 'GET' && ['/healthz', '/readyz'].includes(url.pathname)) {
|
|
238
248
|
let ready = !this.stopped;
|
|
239
249
|
if (url.pathname === '/readyz') {
|
|
@@ -267,8 +277,6 @@ export class CommerceServer {
|
|
|
267
277
|
const caller = await this.options.authenticate(request);
|
|
268
278
|
if (!caller)
|
|
269
279
|
return Response.json({ error: 'authentication_required' }, { status: 401, headers: { 'WWW-Authenticate': 'Bearer', 'Cache-Control': 'no-store' } });
|
|
270
|
-
if (request.headers.get('A2A-Version') !== '1.0')
|
|
271
|
-
return Response.json({ error: 'A2A-Version 1.0 required' }, { status: 400 });
|
|
272
280
|
if (Number(request.headers.get('Content-Length') ?? 0) > 1024 * 1024)
|
|
273
281
|
return Response.json({ error: 'request_too_large' }, { status: 413 });
|
|
274
282
|
let size = 0;
|
|
@@ -291,12 +299,18 @@ export class CommerceServer {
|
|
|
291
299
|
let requestId = null;
|
|
292
300
|
try {
|
|
293
301
|
const raw = JSON.parse(body);
|
|
294
|
-
if (raw.method === 'SendMessage' && this.workerFailed)
|
|
295
|
-
return Response.json({ error: 'worker_unavailable' }, { status: 503, headers: { 'Cache-Control': 'no-store' } });
|
|
296
302
|
if (!raw || Array.isArray(raw) || typeof raw !== 'object')
|
|
297
303
|
throw new RequestMalformedError('A single JSONRPC object is required');
|
|
298
304
|
requestId = raw.id ?? null;
|
|
299
305
|
const params = raw.params;
|
|
306
|
+
// Parse only the bounded envelope to preserve its request ID. Reject these
|
|
307
|
+
// transport conditions before any quote, payment gate or Task dispatch.
|
|
308
|
+
if (request.headers.get('A2A-Version') !== '1.0')
|
|
309
|
+
throw new VersionNotSupportedError('This service supports A2A-Version 1.0');
|
|
310
|
+
if (request.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json')
|
|
311
|
+
throw new ContentTypeNotSupportedError('Send application/json to this JSON-RPC endpoint');
|
|
312
|
+
if (raw.method === 'SendMessage' && this.workerFailed)
|
|
313
|
+
return Response.json({ error: 'worker_unavailable' }, { status: 503, headers: { 'Cache-Control': 'no-store' } });
|
|
300
314
|
if (params && typeof params === 'object' && 'tenant' in params && params.tenant !== route.tenant)
|
|
301
315
|
return Response.json({ jsonrpc: '2.0', id: raw.id ?? null, error: { code: -32602, message: 'Invalid tenant' } }, { status: 200 });
|
|
302
316
|
let paymentHeaders = {};
|
|
@@ -313,6 +327,8 @@ export class CommerceServer {
|
|
|
313
327
|
if (!prior && !this.activeRevision(route.service))
|
|
314
328
|
throw new UnsupportedOperationError('This service revision is retired');
|
|
315
329
|
const order = prior ?? this.store.ensureQuote(this.quote(route.service, route.offerId, caller, message.message.messageId, input), input);
|
|
330
|
+
if (route.service.execution.type === 'skill' && !['confirmed', 'not_required'].includes(order.paymentState))
|
|
331
|
+
this.options.execute.assertSkill(route.service);
|
|
316
332
|
if (order.paymentState !== 'not_required') {
|
|
317
333
|
const gate = await this.options.paymentGate.handle(new Request(request.url, { method: 'POST', headers: request.headers, body }), order);
|
|
318
334
|
if ('response' in gate)
|
|
@@ -329,7 +345,7 @@ export class CommerceServer {
|
|
|
329
345
|
}
|
|
330
346
|
catch (error) {
|
|
331
347
|
const mapped = error instanceof CommerceError ? { code: -32000, message: error.message, data: { code: error.code } } : JsonRpcTransportHandler.mapToJSONRPCError(error);
|
|
332
|
-
return Response.json({ jsonrpc: '2.0', id: requestId, error: mapped }, { headers: { 'Cache-Control': 'no-store' } });
|
|
348
|
+
return Response.json({ jsonrpc: '2.0', id: requestId, error: mapped }, { headers: { 'A2A-Version': '1.0', 'Cache-Control': 'no-store' } });
|
|
333
349
|
}
|
|
334
350
|
}
|
|
335
351
|
async continueTask(params, input, service, offerId, context) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface SetupBundle {
|
|
2
|
+
version: 1;
|
|
3
|
+
agentId: string;
|
|
4
|
+
agentName: string;
|
|
5
|
+
runtimeAgentId: string;
|
|
6
|
+
platformOrigin: string;
|
|
7
|
+
token: string;
|
|
8
|
+
expiresAt: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseSetupBundle(raw: unknown): SetupBundle;
|
|
11
|
+
export declare function prepareTextSkill(source: string, destination: string, name: string): void;
|
|
12
|
+
export declare function setupAgent(options: {
|
|
13
|
+
framework?: "hermes" | "openclaw";
|
|
14
|
+
managed?: boolean;
|
|
15
|
+
file: string;
|
|
16
|
+
directory?: string;
|
|
17
|
+
container?: string;
|
|
18
|
+
python?: string;
|
|
19
|
+
home?: string;
|
|
20
|
+
automatic?: {
|
|
21
|
+
port: number;
|
|
22
|
+
origin: string;
|
|
23
|
+
};
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
export declare const setupHermes: (options: Parameters<typeof setupAgent>[0]) => Promise<void>;
|