@envarai/envarpay 0.2.0-alpha.8 → 2026.1025.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -56
- package/dist/commerce/agent-setup.d.ts +19 -0
- package/dist/commerce/agent-setup.js +210 -0
- package/dist/commerce/alerts.d.ts +5 -15
- package/dist/commerce/alerts.js +21 -77
- package/dist/commerce/card.js +1 -1
- package/dist/commerce/cli.js +74 -137
- package/dist/commerce/config.d.ts +1 -4
- package/dist/commerce/config.js +5 -81
- package/dist/commerce/configure.d.ts +2 -0
- package/dist/commerce/configure.js +78 -0
- package/dist/commerce/discovery.d.ts +20 -0
- package/dist/commerce/discovery.js +162 -0
- package/dist/commerce/envar-balance.d.ts +25 -0
- package/dist/commerce/envar-balance.js +65 -0
- package/dist/commerce/envar-runtime.d.ts +1 -0
- package/dist/commerce/envar-runtime.js +7 -0
- package/dist/commerce/envar.d.ts +13 -31
- package/dist/commerce/envar.js +35 -83
- package/dist/commerce/external-agent.d.ts +26 -0
- package/dist/commerce/external-agent.js +129 -0
- package/dist/commerce/external-events.d.ts +76 -0
- package/dist/commerce/external-events.js +273 -0
- package/dist/commerce/external-mcp.d.ts +10 -0
- package/dist/commerce/external-mcp.js +199 -0
- package/dist/commerce/external-oauth.d.ts +12 -0
- package/dist/commerce/external-oauth.js +230 -0
- package/dist/commerce/external-runtime.d.ts +25 -0
- package/dist/commerce/external-runtime.js +244 -0
- package/dist/commerce/managed.d.ts +3 -0
- package/dist/commerce/managed.js +143 -0
- package/dist/commerce/native-inventory.d.ts +9 -0
- package/dist/commerce/native-inventory.js +75 -0
- package/dist/commerce/native-skills.d.ts +8 -0
- package/dist/commerce/native-skills.js +11 -8
- package/dist/commerce/openclaw-setup.d.ts +28 -0
- package/dist/commerce/openclaw-setup.js +43 -0
- package/dist/commerce/operations.d.ts +1 -2
- package/dist/commerce/operations.js +17 -20
- package/dist/commerce/pricing.d.ts +0 -1
- package/dist/commerce/pricing.js +1 -3
- package/dist/commerce/runtime.d.ts +2 -2
- package/dist/commerce/runtime.js +2 -2
- package/dist/commerce/server.d.ts +0 -1
- package/dist/commerce/server.js +2 -3
- package/dist/commerce/setup.d.ts +25 -0
- package/dist/commerce/setup.js +379 -0
- package/dist/commerce/store.d.ts +11 -1
- package/dist/commerce/store.js +36 -2
- package/dist/commerce/types.d.ts +1 -56
- package/examples/envar-integration-policy.json +1 -9
- package/examples/seller.json +143 -26
- package/package.json +11 -20
- package/runtime/hermes-setup-probe.py +109 -0
- package/runtime/hermes-skill-inventory.py +108 -0
- package/schemas/envarpay.config.schema.json +7 -73
- package/dist/commerce/buyer-store.d.ts +0 -101
- package/dist/commerce/buyer-store.js +0 -168
- package/dist/commerce/buyer.d.ts +0 -138
- package/dist/commerce/buyer.js +0 -775
- package/dist/commerce/chain.d.ts +0 -24
- package/dist/commerce/chain.js +0 -146
- package/dist/commerce/client-runtime.d.ts +0 -8
- package/dist/commerce/client-runtime.js +0 -7
- package/dist/commerce/management.d.ts +0 -14
- package/dist/commerce/management.js +0 -140
- package/dist/commerce/mpp-client.d.ts +0 -70
- package/dist/commerce/mpp-client.js +0 -29
- package/dist/commerce/mpp-runtime.d.ts +0 -4
- package/dist/commerce/mpp-runtime.js +0 -4
- package/dist/commerce/mpp.d.ts +0 -53
- package/dist/commerce/mpp.js +0 -163
- package/dist/commerce/peer-proxy.d.ts +0 -44
- package/dist/commerce/peer-proxy.js +0 -673
- package/dist/commerce/stripe-issuer.d.ts +0 -41
- package/dist/commerce/stripe-issuer.js +0 -351
- package/dist/commerce/stripe-provider.d.ts +0 -11
- package/dist/commerce/stripe-provider.js +0 -132
- package/dist/commerce/x402.d.ts +0 -37
- package/dist/commerce/x402.js +0 -174
- package/dist/index.d.ts +0 -95
- package/dist/index.js +0 -163
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
- /package/{examples → runtime}/native-skill-runner.py +0 -0
|
@@ -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("../../runtime/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" });
|
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;
|
package/dist/commerce/store.js
CHANGED
|
@@ -97,7 +97,11 @@ export class CommerceStore {
|
|
|
97
97
|
state TEXT NOT NULL CHECK(state IN ('ready','dispatching','done','unknown')), UNIQUE(order_id,message_id)
|
|
98
98
|
);
|
|
99
99
|
CREATE INDEX IF NOT EXISTS commerce_tasks_owner_tenant_updated ON commerce_tasks(owner,tenant,updated_at DESC,id);
|
|
100
|
-
CREATE INDEX IF NOT EXISTS commerce_continuations_order_state ON commerce_continuations(order_id,state)
|
|
100
|
+
CREATE INDEX IF NOT EXISTS commerce_continuations_order_state ON commerce_continuations(order_id,state);
|
|
101
|
+
CREATE TABLE IF NOT EXISTS commerce_external_claims(
|
|
102
|
+
order_id TEXT PRIMARY KEY REFERENCES commerce_orders(id),
|
|
103
|
+
principal TEXT NOT NULL, claim_id TEXT NOT NULL DEFAULT ''
|
|
104
|
+
);`);
|
|
101
105
|
}
|
|
102
106
|
migrateV1() {
|
|
103
107
|
// Rebuild only the state constraint. Preserve original IDs, credentials and every payment row.
|
|
@@ -394,17 +398,47 @@ export class CommerceStore {
|
|
|
394
398
|
return pending.length;
|
|
395
399
|
});
|
|
396
400
|
}
|
|
401
|
+
prepareExternalTask(orderId, principal) {
|
|
402
|
+
const order = this.order(this.db.prepare('SELECT * FROM commerce_orders WHERE id=?').get(orderId));
|
|
403
|
+
if (!order || !['confirmed', 'not_required'].includes(order.paymentState) || order.executionState !== 'dispatching')
|
|
404
|
+
throw new CommerceError('external_payment_required', 'Only a dispatched and payment-approved original order can be delegated');
|
|
405
|
+
const existing = this.db.prepare('SELECT principal FROM commerce_external_claims WHERE order_id=?').get(orderId);
|
|
406
|
+
if (existing && existing.principal !== principal)
|
|
407
|
+
throw new CommerceError('external_owner', 'Original execution belongs to another external agent');
|
|
408
|
+
this.db.prepare('INSERT OR IGNORE INTO commerce_external_claims(order_id,principal) VALUES (?,?)').run(orderId, principal);
|
|
409
|
+
}
|
|
410
|
+
externalTasks(principal, services) {
|
|
411
|
+
return this.db.prepare("SELECT o.*,c.claim_id FROM commerce_orders o JOIN commerce_external_claims c ON c.order_id=o.id WHERE c.principal=? AND o.payment_state IN ('confirmed','not_required') AND o.execution_state IN ('input_required','working') ORDER BY o.rowid LIMIT 100").all(principal).filter(r => services.includes(String(r.service_revision).split(':')[0])).map(r => ({ order: this.order(r), claimId: String(r.claim_id) }));
|
|
412
|
+
}
|
|
413
|
+
claimExternalTask(taskId, principal, services) {
|
|
414
|
+
return this.transaction(() => {
|
|
415
|
+
const row = this.db.prepare("SELECT o.*,c.claim_id FROM commerce_orders o JOIN commerce_external_claims c ON c.order_id=o.id WHERE o.task_id=? AND c.principal=? AND o.payment_state IN ('confirmed','not_required') AND o.execution_state IN ('input_required','working','completed','failed')").get(taskId, principal);
|
|
416
|
+
if (!row || !services.includes(String(row.service_revision).split(':')[0]))
|
|
417
|
+
throw new CommerceError('external_task', 'Original Task is outside this external agent scope');
|
|
418
|
+
const claimId = String(row.claim_id) || randomUUID();
|
|
419
|
+
this.db.prepare('UPDATE commerce_external_claims SET claim_id=? WHERE order_id=?').run(claimId, String(row.id));
|
|
420
|
+
return { order: this.order(row), claimId };
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
externalClaim(taskId, principal, claimId, services) {
|
|
424
|
+
const row = this.db.prepare("SELECT o.* FROM commerce_orders o JOIN commerce_external_claims c ON c.order_id=o.id WHERE o.task_id=? AND c.principal=? AND c.claim_id=? AND c.claim_id!='' AND o.payment_state IN ('confirmed','not_required')").get(taskId, principal, claimId);
|
|
425
|
+
if (!row || !services.includes(String(row.service_revision).split(':')[0]))
|
|
426
|
+
throw new CommerceError('external_claim', 'Claim does not identify an authorized original Task');
|
|
427
|
+
return this.order(row);
|
|
428
|
+
}
|
|
397
429
|
scope(context) {
|
|
398
430
|
if (!context.user?.isAuthenticated || !context.user.userName)
|
|
399
431
|
throw new CommerceError('authentication_required', 'Authenticated task owner required');
|
|
400
432
|
return { owner: context.user.userName, tenant: context.tenant ?? '' };
|
|
401
433
|
}
|
|
402
|
-
async save(task, context) {
|
|
434
|
+
async save(task, context, expectedOriginal) {
|
|
403
435
|
const scope = this.scope(context);
|
|
404
436
|
this.transaction(() => {
|
|
405
437
|
const current = this.db.prepare('SELECT owner,tenant,task_json FROM commerce_tasks WHERE id=?').get(task.id);
|
|
406
438
|
if (current && (current.owner !== scope.owner || current.tenant !== scope.tenant))
|
|
407
439
|
throw new CommerceError('task_owner_conflict', 'Cannot replace another owner task');
|
|
440
|
+
if (expectedOriginal && current?.task_json !== JSON.stringify(Task.toJSON(expectedOriginal)))
|
|
441
|
+
throw new CommerceError('external_result_conflict', 'Original Task changed while preparing this result; read and recover it before retrying');
|
|
408
442
|
const order = this.db.prepare('SELECT payment_state FROM commerce_orders WHERE task_id=?').get(task.id);
|
|
409
443
|
if (order && !['confirmed', 'not_required'].includes(String(order.payment_state)))
|
|
410
444
|
throw new CommerceError('payment_required', 'Cannot publish task execution before payment');
|
package/dist/commerce/types.d.ts
CHANGED
|
@@ -27,16 +27,7 @@ export interface Offer {
|
|
|
27
27
|
paymentProfile?: string;
|
|
28
28
|
}
|
|
29
29
|
export type PaymentProfile = {
|
|
30
|
-
adapter: '
|
|
31
|
-
scheme: 'exact';
|
|
32
|
-
network: string;
|
|
33
|
-
asset: string;
|
|
34
|
-
payTo: string;
|
|
35
|
-
facilitatorUrl: string;
|
|
36
|
-
} | {
|
|
37
|
-
adapter: 'mpp';
|
|
38
|
-
method: 'stripe';
|
|
39
|
-
intent: 'charge';
|
|
30
|
+
adapter: 'envar_balance';
|
|
40
31
|
currency: 'usd';
|
|
41
32
|
accountRef: string;
|
|
42
33
|
};
|
|
@@ -69,52 +60,6 @@ export interface CommerceConfig {
|
|
|
69
60
|
paymentProfiles: Record<string, PaymentProfile>;
|
|
70
61
|
services: Service[];
|
|
71
62
|
}
|
|
72
|
-
/** A separately reviewed transport profile; local terms are never advertised as seller promises. */
|
|
73
|
-
export interface StandardA2APeer {
|
|
74
|
-
id: string;
|
|
75
|
-
cardUrl: string;
|
|
76
|
-
mode: 'standard-a2a';
|
|
77
|
-
protocol: 'x402';
|
|
78
|
-
currency: string;
|
|
79
|
-
recipient: string;
|
|
80
|
-
maxPerPurchase: string;
|
|
81
|
-
endpoint: string;
|
|
82
|
-
authentication: 'none' | 'bearer';
|
|
83
|
-
localContract: {
|
|
84
|
-
revision: number;
|
|
85
|
-
offerId: string;
|
|
86
|
-
inputSchema: Record<string, unknown>;
|
|
87
|
-
amount: string;
|
|
88
|
-
paidOnly: true;
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
export interface BuyerPolicy {
|
|
92
|
-
policyVersion: 1;
|
|
93
|
-
paymentsEnabled: boolean;
|
|
94
|
-
approval: 'per_purchase' | 'within_preapproved_limits';
|
|
95
|
-
peers: (StandardA2APeer | {
|
|
96
|
-
id: string;
|
|
97
|
-
cardUrl: string;
|
|
98
|
-
mode?: 'envarpay';
|
|
99
|
-
protocol: 'x402' | 'mpp';
|
|
100
|
-
currency: string;
|
|
101
|
-
recipient: string;
|
|
102
|
-
maxPerPurchase: string;
|
|
103
|
-
} | {
|
|
104
|
-
id: string;
|
|
105
|
-
cardUrl: string;
|
|
106
|
-
mode?: 'envarpay';
|
|
107
|
-
protocol: 'free';
|
|
108
|
-
currency: null;
|
|
109
|
-
recipient: null;
|
|
110
|
-
maxPerPurchase: '0';
|
|
111
|
-
})[];
|
|
112
|
-
budgets: {
|
|
113
|
-
currency: string;
|
|
114
|
-
maxTotal: string;
|
|
115
|
-
period: 'cumulative';
|
|
116
|
-
}[];
|
|
117
|
-
}
|
|
118
63
|
export interface PriceQuote {
|
|
119
64
|
quoteVersion: 1;
|
|
120
65
|
quoteId: string;
|
|
@@ -6,13 +6,5 @@
|
|
|
6
6
|
"acceptUpdates": true,
|
|
7
7
|
"allowedServices": ["research"],
|
|
8
8
|
"allowedUpstreamOrigins": ["http://hermes:9000"],
|
|
9
|
-
"
|
|
10
|
-
{
|
|
11
|
-
"network": "eip155:84532",
|
|
12
|
-
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
13
|
-
"payTo": "0x2222222222222222222222222222222222222222",
|
|
14
|
-
"facilitatorOrigin": "https://facilitator.example"
|
|
15
|
-
}
|
|
16
|
-
],
|
|
17
|
-
"allowedMppAccounts": ["seller-stripe"]
|
|
9
|
+
"allowedBalanceAccounts": ["envar"]
|
|
18
10
|
}
|