@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,379 @@
|
|
|
1
|
+
/** Guided local installation. Platform input is data, never a shell command or wallet policy. */
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, cpSync, mkdtempSync, renameSync, rmSync, } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve, relative, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { probeOpenClaw } from "./openclaw-setup.js";
|
|
10
|
+
import { readInstalledSkills } from "./native-skills.js";
|
|
11
|
+
import { loadCommerceConfig, validateUrl } from "./config.js";
|
|
12
|
+
import { CommerceError } from "./types.js";
|
|
13
|
+
import { isSkillInstallerMetadata } from './native-inventory.js';
|
|
14
|
+
const ID = /^[a-z][a-z0-9-]{0,63}$/, CONTAINER = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
|
|
15
|
+
export function parseSetupBundle(raw) {
|
|
16
|
+
const v = raw;
|
|
17
|
+
if (!v ||
|
|
18
|
+
v.version !== 1 ||
|
|
19
|
+
!/^[0-9a-f-]{36}$/.test(v.agentId) ||
|
|
20
|
+
!ID.test(v.runtimeAgentId) ||
|
|
21
|
+
typeof v.agentName !== "string" ||
|
|
22
|
+
v.agentName.length > 120 ||
|
|
23
|
+
!/^envar_agent_[A-Za-z0-9_-]{32,100}$/.test(v.token) ||
|
|
24
|
+
!Number.isFinite(Date.parse(v.expiresAt)) ||
|
|
25
|
+
Date.parse(v.expiresAt) <= Date.now())
|
|
26
|
+
throw new CommerceError("setup_file", "Download a fresh Agent setup file from Envar");
|
|
27
|
+
const origin = validateUrl(v.platformOrigin);
|
|
28
|
+
if (origin.origin !== v.platformOrigin)
|
|
29
|
+
throw new CommerceError("setup_origin", "Use the platform HTTPS origin");
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
export function prepareTextSkill(source, destination, name) {
|
|
33
|
+
if (!ID.test(name) || existsSync(destination))
|
|
34
|
+
throw new CommerceError("setup_skill", "Choose a new exact skill name");
|
|
35
|
+
const stat = lstatSync(source);
|
|
36
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
37
|
+
throw new CommerceError("setup_skill", "Choose a real skill directory");
|
|
38
|
+
cpSync(source, destination, {
|
|
39
|
+
recursive: true,
|
|
40
|
+
filter: (path) => {
|
|
41
|
+
const item = lstatSync(path);
|
|
42
|
+
if (item.isSymbolicLink())
|
|
43
|
+
throw new CommerceError("setup_skill", "Skill symlinks are unsupported");
|
|
44
|
+
if (item.isFile() && isSkillInstallerMetadata(relative(source, path).split(sep).join('/')))
|
|
45
|
+
return false;
|
|
46
|
+
if (item.isFile() &&
|
|
47
|
+
!/\.(md|txt)$/i.test(path) &&
|
|
48
|
+
!path.endsWith("/LICENSE"))
|
|
49
|
+
throw new CommerceError("setup_skill", "Choose a text-only package without scripts or binaries");
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const file = join(destination, "SKILL.md"), original = readFileSync(file, "utf8");
|
|
54
|
+
if (!original.startsWith("---\n") && !original.startsWith("---\r\n"))
|
|
55
|
+
throw new CommerceError("setup_skill", "Skill needs YAML frontmatter");
|
|
56
|
+
const adapted = original.replace(/^(---\r?\n)([\s\S]*?)(\r?\n---)/, (_all, open, front, close) => open +
|
|
57
|
+
front.replace(/^envar-runtime:.*\r?\n?/gm, "") +
|
|
58
|
+
"\nenvar-runtime: instruction-only" +
|
|
59
|
+
close);
|
|
60
|
+
writeFileSync(file, adapted +
|
|
61
|
+
"\n\n## Service execution\nUse the supplied buyer input as the complete product context. Bundled Markdown references are included in the task context. Return text in the A2A result. Do not browse, access files, run scripts, or invoke other skills. If information is missing, state what is missing; do not invent it.\n", { mode: 0o600 });
|
|
62
|
+
}
|
|
63
|
+
function privateJson(path, value) {
|
|
64
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n", {
|
|
65
|
+
mode: 0o600,
|
|
66
|
+
flag: "wx",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function setupAgent(options) {
|
|
70
|
+
const file = resolve(options.file), info = lstatSync(file);
|
|
71
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 16384)
|
|
72
|
+
throw new CommerceError("setup_file", "Use the downloaded setup JSON file");
|
|
73
|
+
const bundle = parseSetupBundle(JSON.parse(readFileSync(file, "utf8")));
|
|
74
|
+
if (options.container && !CONTAINER.test(options.container))
|
|
75
|
+
throw new CommerceError("setup_container", "Use a Docker container name");
|
|
76
|
+
if (!process.stdin.isTTY && !options.automatic)
|
|
77
|
+
throw new CommerceError("setup_terminal", "Run setup in an interactive terminal to select skills");
|
|
78
|
+
chmodSync(file, 0o600);
|
|
79
|
+
const prompt = options.automatic ? undefined : createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
const output = options.automatic ? (..._args) => { } : console.log;
|
|
81
|
+
const ask = async (label, fallback = "") => (await prompt.question(`${label}${fallback ? ` [${fallback}]` : ""}: `)).trim() || fallback;
|
|
82
|
+
let staging;
|
|
83
|
+
try {
|
|
84
|
+
output(`\nConnect ${bundle.agentName} to ${bundle.platformOrigin}\nModel credentials and tasks remain on this machine. Start with free services; no wallet is created.\n`);
|
|
85
|
+
if (!options.automatic && (await ask("Continue? (yes/no)", "no")).toLowerCase() !== "yes")
|
|
86
|
+
return;
|
|
87
|
+
const directory = resolve(options.directory ?? join(homedir(), ".envarpay", bundle.agentId));
|
|
88
|
+
if (existsSync(directory))
|
|
89
|
+
throw new CommerceError("setup_exists", "This Agent already has a local directory. Use its start script; never reset its ledger.");
|
|
90
|
+
const configuredFramework = process.env.ENVARPAY_FRAMEWORK;
|
|
91
|
+
const framework = options.framework ?? (configuredFramework === "hermes" || configuredFramework === "openclaw" ? configuredFramework : process.env.OPENCLAW_STATE_DIR ? "openclaw" : "hermes");
|
|
92
|
+
let python = options.python;
|
|
93
|
+
let probe;
|
|
94
|
+
if (framework === "openclaw") {
|
|
95
|
+
if (options.container)
|
|
96
|
+
throw new CommerceError("setup_runtime", "Run OpenClaw setup inside its own runtime");
|
|
97
|
+
python ??= "python3";
|
|
98
|
+
probe = probeOpenClaw(options.home, python);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
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"])';
|
|
102
|
+
if (!python && options.container) {
|
|
103
|
+
for (const candidate of [
|
|
104
|
+
"/opt/hermes-latest/bin/python",
|
|
105
|
+
"/opt/hermes/.venv/bin/python",
|
|
106
|
+
"python3",
|
|
107
|
+
]) {
|
|
108
|
+
try {
|
|
109
|
+
execFileSync("docker", ["exec", options.container, candidate, "-c", compatibility], { stdio: "pipe", timeout: 60000 });
|
|
110
|
+
python = candidate;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* try the next compatible installed interpreter */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!python)
|
|
118
|
+
throw new CommerceError("setup_runtime", "No compatible Hermes interpreter found in this container. Update Hermes or pass --python PATH.");
|
|
119
|
+
}
|
|
120
|
+
if (!python && options.automatic) {
|
|
121
|
+
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)) {
|
|
122
|
+
try {
|
|
123
|
+
execFileSync(candidate, ["-c", compatibility], { stdio: "pipe", timeout: 30000 });
|
|
124
|
+
python = candidate;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
catch { }
|
|
128
|
+
}
|
|
129
|
+
if (!python)
|
|
130
|
+
throw new CommerceError("setup_runtime", "No compatible Hermes Python found. Pass its executable using --python.");
|
|
131
|
+
}
|
|
132
|
+
python ??= await ask("Hermes Python executable", join(homedir(), "hermes-agent", ".venv", "bin", "python"));
|
|
133
|
+
const probeSource = readFileSync(new URL("../../examples/hermes-setup-probe.py", import.meta.url), "utf8");
|
|
134
|
+
try {
|
|
135
|
+
if (options.container)
|
|
136
|
+
execFileSync("docker", ["exec", options.container, python, "-c", compatibility], { stdio: "pipe", timeout: 60000 });
|
|
137
|
+
else
|
|
138
|
+
execFileSync(python, ["-c", compatibility], {
|
|
139
|
+
stdio: "pipe",
|
|
140
|
+
timeout: 60000,
|
|
141
|
+
});
|
|
142
|
+
const args = ["-c", probeSource, ...(options.home ? [options.home] : [])];
|
|
143
|
+
const data = options.container
|
|
144
|
+
? execFileSync("docker", ["exec", options.container, python, ...args], {
|
|
145
|
+
stdio: "pipe",
|
|
146
|
+
timeout: 30000,
|
|
147
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
148
|
+
})
|
|
149
|
+
: execFileSync(python, args, {
|
|
150
|
+
stdio: "pipe",
|
|
151
|
+
timeout: 30000,
|
|
152
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
153
|
+
});
|
|
154
|
+
probe = JSON.parse(data.toString());
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new CommerceError("setup_model", "Could not read Hermes. Check the container/Python path and configure an OpenAI-compatible model in Hermes first.");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const candidates = probe.skills.filter((s) => s.textPackage && ID.test(s.name));
|
|
161
|
+
output(`Model: ${probe.model}\nInstalled text packages (review their instructions before selecting):`);
|
|
162
|
+
candidates.forEach((skill, i) => output(`${i + 1}. ${skill.name} — ${skill.description.replace(/[\r\n\x1b]/g, " ").slice(0, 160)}`));
|
|
163
|
+
output('Newly installed Skills will be discovered privately. Enable each one from the Envar service page after reviewing text-only compatibility.');
|
|
164
|
+
const selected = (options.automatic ? "" : await ask("Optional: enable existing text Skills now (comma separated, Enter to skip)"))
|
|
165
|
+
.split(",")
|
|
166
|
+
.map((s) => s.trim()).filter(Boolean);
|
|
167
|
+
if (selected.length > 32 ||
|
|
168
|
+
new Set(selected).size !== selected.length ||
|
|
169
|
+
selected.some((name) => !candidates.some((s) => s.name === name)))
|
|
170
|
+
throw new CommerceError("setup_skills", "Choose installed skill names from the list");
|
|
171
|
+
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.");
|
|
172
|
+
if (selected.length &&
|
|
173
|
+
(await ask("Do these selected skills work with text input and text output only? (yes/no)", "no")).toLowerCase() !== "yes")
|
|
174
|
+
return;
|
|
175
|
+
const port = options.automatic?.port ?? Number(await ask("Local service port", "4020"));
|
|
176
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535)
|
|
177
|
+
throw new CommerceError("setup_port", "Choose a local port from 1024 to 65535");
|
|
178
|
+
let origin;
|
|
179
|
+
const tunnelMode = options.automatic ? "url" : await ask("Public HTTPS: use an existing ngrok session or your own reverse proxy? (ngrok/url)", "ngrok");
|
|
180
|
+
if (tunnelMode === "ngrok") {
|
|
181
|
+
const api = new URL(await ask("Local ngrok API", "http://127.0.0.1:4040"));
|
|
182
|
+
if (api.protocol !== "http:" ||
|
|
183
|
+
api.hostname !== "127.0.0.1" ||
|
|
184
|
+
api.username ||
|
|
185
|
+
api.password ||
|
|
186
|
+
api.pathname !== "/")
|
|
187
|
+
throw new CommerceError("setup_tunnel", "Use the local ngrok API on 127.0.0.1");
|
|
188
|
+
const name = `envarpay-${bundle.agentId}`, addr = `http://127.0.0.1:${port}`;
|
|
189
|
+
const inventory = await fetch(new URL("/api/tunnels", api), {
|
|
190
|
+
signal: AbortSignal.timeout(10000),
|
|
191
|
+
});
|
|
192
|
+
if (!inventory.ok)
|
|
193
|
+
throw new CommerceError("setup_tunnel", "Start ngrok first, or choose your own HTTPS URL");
|
|
194
|
+
const existing = (await inventory.json()).tunnels.find((t) => t.name === name);
|
|
195
|
+
if (existing) {
|
|
196
|
+
if (existing.config.addr !== addr)
|
|
197
|
+
throw new CommerceError("setup_tunnel", "The existing Agent tunnel uses another local port. Keep its original port.");
|
|
198
|
+
origin = existing.public_url;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const response = await fetch(new URL("/api/tunnels", api), {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
name,
|
|
206
|
+
proto: "http",
|
|
207
|
+
addr,
|
|
208
|
+
schemes: ["https"],
|
|
209
|
+
inspect: false,
|
|
210
|
+
}),
|
|
211
|
+
signal: AbortSignal.timeout(15000),
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok)
|
|
214
|
+
throw new CommerceError("setup_tunnel", "Could not create a dedicated tunnel. Check ngrok or choose your own HTTPS URL. Existing tunnels were preserved.");
|
|
215
|
+
origin = (await response.json()).public_url;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (tunnelMode === "url") {
|
|
219
|
+
origin = options.automatic?.origin ?? await ask(`Public HTTPS origin (forward to http://127.0.0.1:${port})`);
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
throw new CommerceError("setup_tunnel", "Choose ngrok or url");
|
|
223
|
+
const publicUrl = validateUrl(origin);
|
|
224
|
+
if (publicUrl.origin !== origin ||
|
|
225
|
+
(publicUrl.port && publicUrl.port !== "443"))
|
|
226
|
+
throw new CommerceError("setup_origin", "Use an HTTPS origin on port443");
|
|
227
|
+
// Only publish a complete new setup directory. An interrupted setup cannot strand
|
|
228
|
+
// the owner behind setup_exists or overwrite a prior ledger.
|
|
229
|
+
mkdirSync(resolve(directory, ".."), { recursive: true, mode: 0o700 });
|
|
230
|
+
staging = mkdtempSync(directory + ".setup-");
|
|
231
|
+
chmodSync(staging, 0o700);
|
|
232
|
+
const skillsDirectory = join(staging, "skills");
|
|
233
|
+
mkdirSync(skillsDirectory, { mode: 0o700 });
|
|
234
|
+
for (const name of selected) {
|
|
235
|
+
const skill = candidates.find((s) => s.name === name);
|
|
236
|
+
if (options.container) {
|
|
237
|
+
const incoming = join(staging, `incoming-${name}`);
|
|
238
|
+
execFileSync("docker", ["cp", `${options.container}:${skill.path}`, incoming], { stdio: "pipe", timeout: 30000 });
|
|
239
|
+
prepareTextSkill(incoming, join(skillsDirectory, name), name);
|
|
240
|
+
rmSync(incoming, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
prepareTextSkill(skill.path, join(skillsDirectory, name), name);
|
|
244
|
+
}
|
|
245
|
+
readInstalledSkills(skillsDirectory, [], origin + "/.well-known/agent-card.json");
|
|
246
|
+
const access = randomBytes(36).toString("base64url");
|
|
247
|
+
let apiKeyFile = probe.apiKeyFile;
|
|
248
|
+
if (options.managed && apiKeyFile) {
|
|
249
|
+
const stat = lstatSync(apiKeyFile);
|
|
250
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16384)
|
|
251
|
+
throw new CommerceError("setup_model", "Invalid local model key file");
|
|
252
|
+
probe.apiKey = readFileSync(apiKeyFile, "utf8");
|
|
253
|
+
apiKeyFile = null;
|
|
254
|
+
}
|
|
255
|
+
if (!apiKeyFile) {
|
|
256
|
+
if (options.container) {
|
|
257
|
+
apiKeyFile = join(probe.home, "envarpay-secrets", bundle.agentId, "model.key");
|
|
258
|
+
// Key bytes travel through stdin, never process arguments or console output.
|
|
259
|
+
const writeKey = "import sys,os;from pathlib import Path;p=Path(sys.argv[1]);p.parent.mkdir(parents=True,exist_ok=True,mode=0o700);fd=os.open(p,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600);os.write(fd,sys.stdin.buffer.read());os.close(fd)";
|
|
260
|
+
try {
|
|
261
|
+
execFileSync("docker", [
|
|
262
|
+
"exec",
|
|
263
|
+
"-i",
|
|
264
|
+
options.container,
|
|
265
|
+
python,
|
|
266
|
+
"-c",
|
|
267
|
+
writeKey,
|
|
268
|
+
apiKeyFile,
|
|
269
|
+
], {
|
|
270
|
+
input: probe.apiKey,
|
|
271
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
272
|
+
timeout: 15000,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
throw new CommerceError("setup_model", "Could not save the local model credential. Inspect the Agent-specific envarpay-secrets directory before retrying.");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
apiKeyFile = join(directory, "model.key");
|
|
281
|
+
writeFileSync(join(staging, "model.key"), probe.apiKey, {
|
|
282
|
+
mode: 0o600,
|
|
283
|
+
flag: "wx",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const stateDirectory = join(directory, "tasks");
|
|
288
|
+
mkdirSync(join(staging, "tasks"), { mode: 0o700 });
|
|
289
|
+
const dockerStateDirectory = options.container
|
|
290
|
+
? join(probe.home, "envarpay-tasks", bundle.agentId)
|
|
291
|
+
: undefined;
|
|
292
|
+
const config = loadCommerceConfig({
|
|
293
|
+
configVersion: 2,
|
|
294
|
+
agent: { id: bundle.runtimeAgentId, name: bundle.agentName },
|
|
295
|
+
paymentProfiles: {},
|
|
296
|
+
services: [],
|
|
297
|
+
});
|
|
298
|
+
privateJson(join(staging, "seller.json"), config);
|
|
299
|
+
writeFileSync(join(staging, "vault.key"), randomBytes(32), { mode: 0o600, flag: "wx" });
|
|
300
|
+
privateJson(join(staging, "credentials.json"), {
|
|
301
|
+
callers: { [access]: "owner" },
|
|
302
|
+
upstreams: {},
|
|
303
|
+
vaultKeyFile: join(directory, "vault.key"),
|
|
304
|
+
skills: {
|
|
305
|
+
framework,
|
|
306
|
+
...(probe.command ? { command: probe.command } : {}),
|
|
307
|
+
discovery: { hermesHome: probe.home, enabledFromWeb: true, ...(probe.skillRoots ? { roots: probe.skillRoots } : {}) },
|
|
308
|
+
skillsDirectory: join(directory, "skills"),
|
|
309
|
+
freeSkills: [],
|
|
310
|
+
stateDirectory,
|
|
311
|
+
python,
|
|
312
|
+
model: probe.model,
|
|
313
|
+
baseUrl: probe.baseUrl,
|
|
314
|
+
apiKeyFile,
|
|
315
|
+
...(options.container
|
|
316
|
+
? { dockerContainer: options.container, dockerStateDirectory }
|
|
317
|
+
: {}),
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
writeFileSync(join(staging, "envar.token"), bundle.token, {
|
|
321
|
+
mode: 0o600,
|
|
322
|
+
flag: "wx",
|
|
323
|
+
});
|
|
324
|
+
privateJson(join(staging, "envar.json"), {
|
|
325
|
+
policy: {
|
|
326
|
+
enabled: true,
|
|
327
|
+
platformOrigin: bundle.platformOrigin,
|
|
328
|
+
agentId: bundle.agentId,
|
|
329
|
+
runtimeAgentId: bundle.runtimeAgentId,
|
|
330
|
+
acceptUpdates: true,
|
|
331
|
+
allowedServices: selected,
|
|
332
|
+
allowedUpstreamOrigins: [origin],
|
|
333
|
+
allowedBalanceAccounts: ["envar"],
|
|
334
|
+
},
|
|
335
|
+
stateDirectory: join(directory, "envar-state"),
|
|
336
|
+
configDirectory: join(directory, "envar-configs"),
|
|
337
|
+
});
|
|
338
|
+
privateJson(join(staging, "connection.json"), {
|
|
339
|
+
version: 1,
|
|
340
|
+
agentId: bundle.agentId,
|
|
341
|
+
url: origin + "/.well-known/agent-card.json",
|
|
342
|
+
authToken: access,
|
|
343
|
+
});
|
|
344
|
+
const cli = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
345
|
+
const args = [
|
|
346
|
+
cli,
|
|
347
|
+
"serve",
|
|
348
|
+
"--config",
|
|
349
|
+
join(directory, "seller.json"),
|
|
350
|
+
"--credentials",
|
|
351
|
+
join(directory, "credentials.json"),
|
|
352
|
+
"--state",
|
|
353
|
+
join(directory, "seller.sqlite3"),
|
|
354
|
+
"--origin",
|
|
355
|
+
origin,
|
|
356
|
+
"--host",
|
|
357
|
+
"127.0.0.1",
|
|
358
|
+
"--port",
|
|
359
|
+
String(port),
|
|
360
|
+
"--envar-config",
|
|
361
|
+
join(directory, "envar.json"),
|
|
362
|
+
"--envar-credentials",
|
|
363
|
+
join(directory, "envar.token"),
|
|
364
|
+
];
|
|
365
|
+
const start = join(directory, "start.mjs");
|
|
366
|
+
writeFileSync(join(staging, "start.mjs"), `import {spawn} from 'node:child_process';\nconst child=spawn(${JSON.stringify(process.execPath)},${JSON.stringify(args)},{stdio:'inherit'});\nfor(const signal of ['SIGTERM','SIGINT'])process.on(signal,()=>child.kill(signal));\nchild.on('exit',code=>process.exit(code??1));\n`, { mode: 0o700, flag: "wx" });
|
|
367
|
+
writeFileSync(join(staging, "README.md"), `# ${bundle.agentName}\n\n1. Start: node ${JSON.stringify(start)}\n2. Keep this process, your Agent and the HTTPS tunnel running.\n3. In Envar, import connection.json and select Check connection.\n4. Choose a Skill. You can publish it free immediately.\n5. For a paid Skill, request Envar balance receiving approval in Envar, then enter the USD price. This setup already allows the local receiving reference \"envar\"; there is no chain wallet or payment protocol to configure.\n6. Select Test service, then Publish after Envar confirms the live unpaid quote.\n\nThe Envar Agent token expires ${bundle.expiresAt}. Keep the original files and ledger when restarting. Never share credentials.json, envar.token or vault.key.\n`, { mode: 0o600, flag: "wx" });
|
|
368
|
+
renameSync(staging, directory);
|
|
369
|
+
staging = undefined;
|
|
370
|
+
output(`\nReady. Start the local service:\n${process.execPath} ${JSON.stringify(start)}\n\nThen import this connection file in Envar:\n${join(directory, "connection.json")}\n\nKeep the service and HTTPS tunnel online. No task or payment was performed.`);
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
prompt?.close();
|
|
374
|
+
if (staging)
|
|
375
|
+
rmSync(staging, { recursive: true, force: true });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Preserve the existing SDK helper name for callers using Hermes explicitly.
|
|
379
|
+
export const setupHermes = (options) => setupAgent({ ...options, framework: "hermes" });
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AgentCard, Task } from '@a2a-js/sdk';
|
|
2
|
+
import { type Input } from './types.js';
|
|
3
|
+
import type { ExecuteOrder } from './server.js';
|
|
4
|
+
import type { OrderRecord } from './store.js';
|
|
5
|
+
/** The name is the service ID. The digest pins the installed skill version. */
|
|
6
|
+
export interface SkillDescriptor {
|
|
7
|
+
name: string;
|
|
8
|
+
digest: string;
|
|
9
|
+
cardUrl: string;
|
|
10
|
+
access: 'free' | 'paid';
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
/** The Agent-owned adapter may expose one private, authenticated A2A Card per skill. */
|
|
14
|
+
export declare function createSkillCard(descriptor: SkillDescriptor, interfaceUrl: string): AgentCard;
|
|
15
|
+
/** In-process authority passed only after the seller has confirmed payment or a free offer. */
|
|
16
|
+
export declare class SkillGrant {
|
|
17
|
+
#private;
|
|
18
|
+
readonly skillName: string;
|
|
19
|
+
readonly skillDigest: string;
|
|
20
|
+
readonly orderId: string;
|
|
21
|
+
readonly caller: string;
|
|
22
|
+
readonly messageId: string;
|
|
23
|
+
readonly inputDigest: string;
|
|
24
|
+
constructor(order: OrderRecord, descriptor: SkillDescriptor, freeSkills: ReadonlySet<string>, messageId?: string);
|
|
25
|
+
/** A purchased task may use its own skill and declared free skills, never another paid skill. */
|
|
26
|
+
require(skillName: string): void;
|
|
27
|
+
}
|
|
28
|
+
export interface SkillInvocation {
|
|
29
|
+
orderId: string;
|
|
30
|
+
messageId: string;
|
|
31
|
+
taskId?: string;
|
|
32
|
+
contextId?: string;
|
|
33
|
+
grant: SkillGrant;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An Agent-owned adapter. It must route by the passed exact skill name and use
|
|
37
|
+
* grant.require() for every nested skill invocation. Raw paid skills stay private.
|
|
38
|
+
*/
|
|
39
|
+
export interface SkillRuntime {
|
|
40
|
+
list(): readonly SkillDescriptor[];
|
|
41
|
+
send(skillName: string, input: Input, invocation: SkillInvocation): Promise<Task>;
|
|
42
|
+
getTask(skillName: string, taskId: string, invocation: SkillInvocation): Promise<Task>;
|
|
43
|
+
}
|
|
44
|
+
export interface SkillHandler {
|
|
45
|
+
descriptor: SkillDescriptor;
|
|
46
|
+
send(input: Input, invocation: SkillInvocation & {
|
|
47
|
+
callSkill(name: string, input: Input): Promise<Task>;
|
|
48
|
+
}): Promise<Task>;
|
|
49
|
+
getTask(taskId: string, invocation: SkillInvocation): Promise<Task>;
|
|
50
|
+
}
|
|
51
|
+
/** Reference in-process adapter: every nested skill call passes the same purchase gate. */
|
|
52
|
+
export declare function createSkillRegistry(handlers: readonly SkillHandler[]): SkillRuntime;
|
|
53
|
+
/** External buyer/seller communication remains ordinary A2A; only local skill dispatch changes. */
|
|
54
|
+
export declare function skillScopedExecutor(runtime: SkillRuntime, signal?: AbortSignal): ExecuteOrder;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { AgentCard, TaskState } from '@a2a-js/sdk';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { CommerceError } from './types.js';
|
|
4
|
+
import { validateUrl } from './config.js';
|
|
5
|
+
/** The Agent-owned adapter may expose one private, authenticated A2A Card per skill. */
|
|
6
|
+
export function createSkillCard(descriptor, interfaceUrl) {
|
|
7
|
+
const card = validateUrl(descriptor.cardUrl, true), endpoint = validateUrl(interfaceUrl, true);
|
|
8
|
+
if (card.origin !== endpoint.origin || !/^[a-z][a-z0-9-]{0,63}$/.test(descriptor.name) ||
|
|
9
|
+
!/^[0-9a-f]{64}$/.test(descriptor.digest))
|
|
10
|
+
throw new CommerceError('invalid_skill_card', 'Skill Card needs one exact named skill, pinned digest and same-origin A2A interface');
|
|
11
|
+
return AgentCard.fromJSON({
|
|
12
|
+
name: descriptor.name, description: `Agent Skill: ${descriptor.name}`, version: descriptor.digest,
|
|
13
|
+
supportedInterfaces: [{ url: endpoint.href, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }],
|
|
14
|
+
capabilities: { streaming: false, pushNotifications: false, extensions: [{
|
|
15
|
+
uri: 'urn:envarpay:skill-gate:1', required: false,
|
|
16
|
+
description: 'The owner-operated adapter invokes this exact installed skill and gates other paid skills',
|
|
17
|
+
params: { skillName: descriptor.name, skillDigest: descriptor.digest, access: descriptor.access },
|
|
18
|
+
}] },
|
|
19
|
+
defaultInputModes: ['application/json'], defaultOutputModes: ['application/json', 'text/plain'],
|
|
20
|
+
skills: [{ id: descriptor.name, name: descriptor.name, description: `Invoke ${descriptor.name}`, tags: [descriptor.name] }],
|
|
21
|
+
securitySchemes: { bearer: { httpAuthSecurityScheme: { scheme: 'bearer' } } },
|
|
22
|
+
securityRequirements: [{ schemes: { bearer: { list: [] } } }],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** In-process authority passed only after the seller has confirmed payment or a free offer. */
|
|
26
|
+
export class SkillGrant {
|
|
27
|
+
skillName;
|
|
28
|
+
skillDigest;
|
|
29
|
+
orderId;
|
|
30
|
+
caller;
|
|
31
|
+
messageId;
|
|
32
|
+
inputDigest;
|
|
33
|
+
#freeSkills;
|
|
34
|
+
constructor(order, descriptor, freeSkills, messageId = order.messageId) {
|
|
35
|
+
if (!['confirmed', 'not_required'].includes(order.paymentState))
|
|
36
|
+
throw new CommerceError('payment_required', 'An unpaid order cannot authorize a skill');
|
|
37
|
+
this.skillName = descriptor.name;
|
|
38
|
+
this.skillDigest = descriptor.digest;
|
|
39
|
+
this.orderId = order.id;
|
|
40
|
+
this.caller = order.caller;
|
|
41
|
+
this.messageId = messageId;
|
|
42
|
+
this.inputDigest = order.inputDigest;
|
|
43
|
+
this.#freeSkills = new Set(freeSkills);
|
|
44
|
+
Object.freeze(this);
|
|
45
|
+
}
|
|
46
|
+
/** A purchased task may use its own skill and declared free skills, never another paid skill. */
|
|
47
|
+
require(skillName) {
|
|
48
|
+
if (skillName !== this.skillName && !this.#freeSkills.has(skillName))
|
|
49
|
+
throw new CommerceError('skill_payment_required', 'Purchase the other paid skill as a separate service');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Reference in-process adapter: every nested skill call passes the same purchase gate. */
|
|
53
|
+
export function createSkillRegistry(handlers) {
|
|
54
|
+
const byName = new Map();
|
|
55
|
+
for (const handler of handlers) {
|
|
56
|
+
const { name, digest, cardUrl } = handler.descriptor;
|
|
57
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name) || !/^[0-9a-f]{64}$/.test(digest) || !cardUrl || byName.has(name))
|
|
58
|
+
throw new CommerceError('invalid_skill_registry', 'Skill names and pinned versions must be unique');
|
|
59
|
+
byName.set(name, { ...handler, descriptor: Object.freeze({ ...handler.descriptor }) });
|
|
60
|
+
}
|
|
61
|
+
const select = (name, grant) => {
|
|
62
|
+
grant.require(name);
|
|
63
|
+
const handler = byName.get(name);
|
|
64
|
+
if (!handler)
|
|
65
|
+
throw new CommerceError('skill_unavailable', 'The requested skill is not installed');
|
|
66
|
+
return handler;
|
|
67
|
+
};
|
|
68
|
+
const dispatch = (name, input, invocation, depth) => {
|
|
69
|
+
if (depth > 8)
|
|
70
|
+
throw new CommerceError('skill_depth', 'Nested skill calls exceeded the supported limit');
|
|
71
|
+
const handler = select(name, invocation.grant);
|
|
72
|
+
return handler.send(input, { ...invocation, callSkill: async (child, childInput) => dispatch(child, childInput, invocation, depth + 1) });
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
list: () => [...byName.values()].map(h => ({ ...h.descriptor })),
|
|
76
|
+
send: (name, input, invocation) => dispatch(name, input, invocation, 0),
|
|
77
|
+
getTask: (name, taskId, invocation) => select(name, invocation.grant).getTask(taskId, invocation),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const TERMINAL = new Set([
|
|
81
|
+
TaskState.TASK_STATE_COMPLETED, TaskState.TASK_STATE_FAILED, TaskState.TASK_STATE_CANCELED,
|
|
82
|
+
TaskState.TASK_STATE_REJECTED, TaskState.TASK_STATE_INPUT_REQUIRED, TaskState.TASK_STATE_AUTH_REQUIRED,
|
|
83
|
+
]);
|
|
84
|
+
/** External buyer/seller communication remains ordinary A2A; only local skill dispatch changes. */
|
|
85
|
+
export function skillScopedExecutor(runtime, signal) {
|
|
86
|
+
function descriptor(service) {
|
|
87
|
+
const execution = service.execution;
|
|
88
|
+
if (execution.type !== 'skill' || service.id !== service.name)
|
|
89
|
+
throw new CommerceError('skill_name_mismatch', 'The service must use an exact skill name');
|
|
90
|
+
const matches = runtime.list().filter(s => s.name === service.id && s.digest === execution.skillDigest);
|
|
91
|
+
if (matches.length !== 1 || matches[0].cardUrl !== execution.cardUrl)
|
|
92
|
+
throw new CommerceError('skill_unavailable', 'The named installed skill or its pinned version is unavailable');
|
|
93
|
+
if (matches[0].access === 'free' && service.offers.some(offer => offer.pricing.kind !== 'free'))
|
|
94
|
+
throw new CommerceError('free_skill_price', 'A declared free helper cannot have paid offers');
|
|
95
|
+
return matches[0];
|
|
96
|
+
}
|
|
97
|
+
function grant(order, service, messageId = order.messageId) {
|
|
98
|
+
const selected = descriptor(service);
|
|
99
|
+
const free = new Set(runtime.list().filter(s => s.access === 'free').map(s => s.name));
|
|
100
|
+
return new SkillGrant(order, selected, free, messageId);
|
|
101
|
+
}
|
|
102
|
+
function checked(task, expectedId) {
|
|
103
|
+
if (!task || typeof task.id !== 'string' || !task.id || !task.status || (expectedId && task.id !== expectedId))
|
|
104
|
+
throw new CommerceError('invalid_agent_response', 'The skill runtime returned no stable A2A Task');
|
|
105
|
+
return task;
|
|
106
|
+
}
|
|
107
|
+
async function* observe(initial, order, service, authority) {
|
|
108
|
+
let task = checked(initial);
|
|
109
|
+
const deadline = Date.now() + service.contract.targetDurationSeconds * 1000;
|
|
110
|
+
for (;;) {
|
|
111
|
+
yield task;
|
|
112
|
+
if (TERMINAL.has(task.status?.state ?? 0))
|
|
113
|
+
return;
|
|
114
|
+
if (Date.now() >= deadline)
|
|
115
|
+
throw new CommerceError('execution_pending', 'Read the original Task to continue observing execution');
|
|
116
|
+
await sleep(1000, undefined, { signal });
|
|
117
|
+
task = checked(await runtime.getTask(service.id, task.id, {
|
|
118
|
+
orderId: order.id, messageId: authority.messageId, grant: authority,
|
|
119
|
+
}), task.id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const execute = async function* (order, service) {
|
|
123
|
+
const authority = grant(order, service);
|
|
124
|
+
authority.require(service.id);
|
|
125
|
+
const task = await runtime.send(service.id, order.input, {
|
|
126
|
+
orderId: order.id, messageId: order.messageId, grant: authority,
|
|
127
|
+
});
|
|
128
|
+
yield* observe(task, order, service, authority);
|
|
129
|
+
};
|
|
130
|
+
execute.continue = async function* (order, service, continuation, remote) {
|
|
131
|
+
if (remote.origin !== service.execution.cardUrl)
|
|
132
|
+
throw new CommerceError('skill_origin_changed', 'The original skill Task belongs to another execution entry');
|
|
133
|
+
const authority = grant(order, service, continuation.messageId);
|
|
134
|
+
const task = checked(await runtime.send(service.id, continuation.input, {
|
|
135
|
+
orderId: order.id, messageId: continuation.messageId, taskId: remote.taskId,
|
|
136
|
+
contextId: remote.contextId, grant: authority,
|
|
137
|
+
}), remote.taskId);
|
|
138
|
+
yield* observe(task, order, service, authority);
|
|
139
|
+
};
|
|
140
|
+
execute.recover = async (order, service, remote) => {
|
|
141
|
+
if (remote.origin !== service.execution.cardUrl)
|
|
142
|
+
throw new CommerceError('skill_origin_changed', 'The original skill Task belongs to another execution entry');
|
|
143
|
+
// Reading an original Task must survive a removed/upgraded installed package.
|
|
144
|
+
if (service.execution.type !== 'skill')
|
|
145
|
+
throw new CommerceError('skill_required', 'Expected a skill Task');
|
|
146
|
+
const authority = new SkillGrant(order, { name: service.id, digest: service.execution.skillDigest, cardUrl: service.execution.cardUrl, access: 'paid' }, new Set());
|
|
147
|
+
return checked(await runtime.getTask(service.id, remote.taskId, {
|
|
148
|
+
orderId: order.id, messageId: order.messageId, grant: authority,
|
|
149
|
+
}), remote.taskId);
|
|
150
|
+
};
|
|
151
|
+
execute.assertSkill = descriptor;
|
|
152
|
+
return execute;
|
|
153
|
+
}
|
package/dist/commerce/store.d.ts
CHANGED
|
@@ -80,8 +80,18 @@ export declare class CommerceStore implements TaskStore {
|
|
|
80
80
|
hasUnresolvedContinuation(orderId: string): boolean;
|
|
81
81
|
acknowledgeRecoveredContinuations(orderId: string, task: Task): void;
|
|
82
82
|
recoverInterruptedDispatches(): number;
|
|
83
|
+
prepareExternalTask(orderId: string, principal: string): void;
|
|
84
|
+
externalTasks(principal: string, services: readonly string[]): {
|
|
85
|
+
order: OrderRecord;
|
|
86
|
+
claimId: string;
|
|
87
|
+
}[];
|
|
88
|
+
claimExternalTask(taskId: string, principal: string, services: readonly string[]): {
|
|
89
|
+
order: OrderRecord;
|
|
90
|
+
claimId: string;
|
|
91
|
+
};
|
|
92
|
+
externalClaim(taskId: string, principal: string, claimId: string, services: readonly string[]): OrderRecord;
|
|
83
93
|
private scope;
|
|
84
|
-
save(task: Task, context: ServerCallContext): Promise<void>;
|
|
94
|
+
save(task: Task, context: ServerCallContext, expectedOriginal?: Task): Promise<void>;
|
|
85
95
|
load(taskId: string, context: ServerCallContext): Promise<Task | undefined>;
|
|
86
96
|
list(params: ListTasksRequest, context: ServerCallContext): Promise<ListTasksResponse>;
|
|
87
97
|
fingerprint(order: OrderRecord): string;
|