@envarai/envarpay 0.2.0-alpha.1 → 0.2.0-alpha.11

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.
Files changed (55) hide show
  1. package/README.md +2 -0
  2. package/dist/commerce/alerts.d.ts +37 -0
  3. package/dist/commerce/alerts.js +127 -0
  4. package/dist/commerce/buyer-store.d.ts +8 -0
  5. package/dist/commerce/buyer.d.ts +12 -3
  6. package/dist/commerce/buyer.js +151 -7
  7. package/dist/commerce/card.js +2 -2
  8. package/dist/commerce/cli.js +74 -9
  9. package/dist/commerce/client-runtime.d.ts +1 -1
  10. package/dist/commerce/config.d.ts +5 -1
  11. package/dist/commerce/config.js +37 -1
  12. package/dist/commerce/configure.d.ts +3 -0
  13. package/dist/commerce/configure.js +158 -0
  14. package/dist/commerce/discovery.d.ts +23 -0
  15. package/dist/commerce/discovery.js +162 -0
  16. package/dist/commerce/envar-runtime.d.ts +3 -1
  17. package/dist/commerce/envar-runtime.js +28 -1
  18. package/dist/commerce/envar.d.ts +5 -0
  19. package/dist/commerce/envar.js +13 -0
  20. package/dist/commerce/index.d.ts +1 -0
  21. package/dist/commerce/index.js +1 -0
  22. package/dist/commerce/management.js +3 -1
  23. package/dist/commerce/mpp-client.d.ts +11 -0
  24. package/dist/commerce/mpp-client.js +5 -0
  25. package/dist/commerce/mpp-runtime.d.ts +1 -0
  26. package/dist/commerce/mpp-runtime.js +1 -0
  27. package/dist/commerce/native-skills.d.ts +31 -0
  28. package/dist/commerce/native-skills.js +174 -0
  29. package/dist/commerce/operations.d.ts +22 -0
  30. package/dist/commerce/operations.js +39 -0
  31. package/dist/commerce/peer-proxy.js +11 -5
  32. package/dist/commerce/pricing.js +2 -2
  33. package/dist/commerce/runtime.d.ts +2 -0
  34. package/dist/commerce/runtime.js +2 -0
  35. package/dist/commerce/server.d.ts +4 -1
  36. package/dist/commerce/server.js +40 -9
  37. package/dist/commerce/setup.d.ts +18 -0
  38. package/dist/commerce/setup.js +344 -0
  39. package/dist/commerce/skill-gate.d.ts +54 -0
  40. package/dist/commerce/skill-gate.js +153 -0
  41. package/dist/commerce/store.d.ts +2 -0
  42. package/dist/commerce/store.js +7 -0
  43. package/dist/commerce/stripe-issuer.d.ts +41 -0
  44. package/dist/commerce/stripe-issuer.js +351 -0
  45. package/dist/commerce/stripe-provider.d.ts +2 -0
  46. package/dist/commerce/stripe-provider.js +57 -22
  47. package/dist/commerce/types.d.ts +27 -2
  48. package/dist/commerce/x402.d.ts +1 -0
  49. package/dist/commerce/x402.js +8 -1
  50. package/examples/hermes-setup-probe.py +105 -0
  51. package/examples/hermes-skill-inventory.py +107 -0
  52. package/examples/native-skill-runner.py +197 -0
  53. package/package.json +1 -1
  54. package/schemas/buyer-policy.schema.json +105 -0
  55. package/schemas/envarpay.config.schema.json +22 -15
@@ -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';
@@ -171,6 +171,7 @@ export class CommerceServer {
171
171
  running = false;
172
172
  kickPending = false;
173
173
  stopped = false;
174
+ workerFailed = false;
174
175
  waiters = new Map();
175
176
  activeOrders = new Set();
176
177
  recoveries = new Map();
@@ -187,6 +188,11 @@ export class CommerceServer {
187
188
  if (this.stopped)
188
189
  throw new CommerceError('server_draining', 'Cannot apply configuration while stopping');
189
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
+ }
190
196
  if (config.agent.id !== this.currentConfig.agent.id)
191
197
  throw new CommerceError('config_agent_changed', 'Runtime Agent identity cannot change');
192
198
  const history = this.store.catalogHistory();
@@ -206,7 +212,7 @@ export class CommerceServer {
206
212
  const routes = new Map();
207
213
  for (const entry of snapshots.values())
208
214
  for (const offer of entry.service.offers) {
209
- 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] };
210
216
  const card = createOfferCard(historical, service.id, offer.id, this.options.origin);
211
217
  const active = config.services.some(s => s.id === service.id && s.revision === service.revision);
212
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 });
@@ -222,6 +228,8 @@ export class CommerceServer {
222
228
  quote(service, offerId, caller, messageId, input) {
223
229
  if (!this.activeRevision(service))
224
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);
225
233
  const frozen = this.store.catalogHistory().find(entry => entry.service.id === service.id && entry.service.revision === service.revision);
226
234
  if (!frozen)
227
235
  throw new CommerceError('unknown_revision', 'Service revision is not registered');
@@ -233,8 +241,23 @@ export class CommerceServer {
233
241
  const url = new URL(request.url);
234
242
  if (url.origin !== new URL(this.options.origin).origin)
235
243
  return Response.json({ error: 'invalid_host' }, { status: 421 });
236
- if (request.method === 'GET' && ['/healthz', '/readyz'].includes(url.pathname))
237
- return Response.json({ status: this.stopped ? 'draining' : 'ready' }, { status: this.stopped ? 503 : 200, headers: { 'Cache-Control': 'no-store' } });
244
+ if (request.method === 'GET' && url.pathname === '/.well-known/agent-card.json' && this.options.execute.skillCard)
245
+ return Response.json(AgentCard.toJSON(this.options.execute.skillCard()), { headers: { 'Cache-Control': 'no-store' } });
246
+ if (url.pathname === '/a2a' && this.options.execute.skillCard)
247
+ return Response.json({ error: 'service_required', message: 'Select a named service offer to invoke a skill' }, { status: 400 });
248
+ if (request.method === 'GET' && ['/healthz', '/readyz'].includes(url.pathname)) {
249
+ let ready = !this.stopped;
250
+ if (url.pathname === '/readyz') {
251
+ ready = ready && !this.workerFailed;
252
+ try {
253
+ this.store.assertReady();
254
+ }
255
+ catch {
256
+ ready = false;
257
+ }
258
+ }
259
+ return Response.json({ status: ready ? 'ready' : this.stopped ? 'draining' : 'unavailable' }, { status: ready ? 200 : 503, headers: { 'Cache-Control': 'no-store' } });
260
+ }
238
261
  const proof = /^\/\.well-known\/envar\/([0-9a-f-]{36})$/.exec(url.pathname);
239
262
  if (request.method === 'GET' && proof) {
240
263
  const challenge = this.options.ownershipChallenges?.[proof[1]];
@@ -255,8 +278,6 @@ export class CommerceServer {
255
278
  const caller = await this.options.authenticate(request);
256
279
  if (!caller)
257
280
  return Response.json({ error: 'authentication_required' }, { status: 401, headers: { 'WWW-Authenticate': 'Bearer', 'Cache-Control': 'no-store' } });
258
- if (request.headers.get('A2A-Version') !== '1.0')
259
- return Response.json({ error: 'A2A-Version 1.0 required' }, { status: 400 });
260
281
  if (Number(request.headers.get('Content-Length') ?? 0) > 1024 * 1024)
261
282
  return Response.json({ error: 'request_too_large' }, { status: 413 });
262
283
  let size = 0;
@@ -283,6 +304,14 @@ export class CommerceServer {
283
304
  throw new RequestMalformedError('A single JSONRPC object is required');
284
305
  requestId = raw.id ?? null;
285
306
  const params = raw.params;
307
+ // Parse only the bounded envelope to preserve its request ID. Reject these
308
+ // transport conditions before any quote, payment gate or Task dispatch.
309
+ if (request.headers.get('A2A-Version') !== '1.0')
310
+ throw new VersionNotSupportedError('This service supports A2A-Version 1.0');
311
+ if (request.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json')
312
+ throw new ContentTypeNotSupportedError('Send application/json to this JSON-RPC endpoint');
313
+ if (raw.method === 'SendMessage' && this.workerFailed)
314
+ return Response.json({ error: 'worker_unavailable' }, { status: 503, headers: { 'Cache-Control': 'no-store' } });
286
315
  if (params && typeof params === 'object' && 'tenant' in params && params.tenant !== route.tenant)
287
316
  return Response.json({ jsonrpc: '2.0', id: raw.id ?? null, error: { code: -32602, message: 'Invalid tenant' } }, { status: 200 });
288
317
  let paymentHeaders = {};
@@ -299,6 +328,8 @@ export class CommerceServer {
299
328
  if (!prior && !this.activeRevision(route.service))
300
329
  throw new UnsupportedOperationError('This service revision is retired');
301
330
  const order = prior ?? this.store.ensureQuote(this.quote(route.service, route.offerId, caller, message.message.messageId, input), input);
331
+ if (route.service.execution.type === 'skill' && !['confirmed', 'not_required'].includes(order.paymentState))
332
+ this.options.execute.assertSkill(route.service);
302
333
  if (order.paymentState !== 'not_required') {
303
334
  const gate = await this.options.paymentGate.handle(new Request(request.url, { method: 'POST', headers: request.headers, body }), order);
304
335
  if ('response' in gate)
@@ -315,7 +346,7 @@ export class CommerceServer {
315
346
  }
316
347
  catch (error) {
317
348
  const mapped = error instanceof CommerceError ? { code: -32000, message: error.message, data: { code: error.code } } : JsonRpcTransportHandler.mapToJSONRPCError(error);
318
- return Response.json({ jsonrpc: '2.0', id: requestId, error: mapped }, { headers: { 'Cache-Control': 'no-store' } });
349
+ return Response.json({ jsonrpc: '2.0', id: requestId, error: mapped }, { headers: { 'A2A-Version': '1.0', 'Cache-Control': 'no-store' } });
319
350
  }
320
351
  }
321
352
  async continueTask(params, input, service, offerId, context) {
@@ -393,14 +424,14 @@ export class CommerceServer {
393
424
  return projected;
394
425
  }
395
426
  schedule() {
396
- if (this.stopped)
427
+ if (this.stopped || this.workerFailed)
397
428
  return;
398
429
  if (this.running) {
399
430
  this.kickPending = true;
400
431
  return;
401
432
  }
402
433
  this.running = true;
403
- void this.drain().catch(() => this.options.onError?.('worker_failure')).finally(() => { this.running = false; if (this.kickPending) {
434
+ void this.drain().catch(() => { this.workerFailed = true; this.options.onError?.('worker_failure'); }).finally(() => { this.running = false; if (this.kickPending) {
404
435
  this.kickPending = false;
405
436
  this.schedule();
406
437
  } });
@@ -0,0 +1,18 @@
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 setupHermes(options: {
13
+ file: string;
14
+ directory?: string;
15
+ container?: string;
16
+ python?: string;
17
+ home?: string;
18
+ }): Promise<void>;
@@ -0,0 +1,344 @@
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 } 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 { readInstalledSkills } from "./native-skills.js";
10
+ import { loadCommerceConfig, validateUrl } from "./config.js";
11
+ import { CommerceError } from "./types.js";
12
+ const ID = /^[a-z][a-z0-9-]{0,63}$/, CONTAINER = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
13
+ export function parseSetupBundle(raw) {
14
+ const v = raw;
15
+ if (!v ||
16
+ v.version !== 1 ||
17
+ !/^[0-9a-f-]{36}$/.test(v.agentId) ||
18
+ !ID.test(v.runtimeAgentId) ||
19
+ typeof v.agentName !== "string" ||
20
+ v.agentName.length > 120 ||
21
+ !/^envar_agent_[A-Za-z0-9_-]{32,100}$/.test(v.token) ||
22
+ !Number.isFinite(Date.parse(v.expiresAt)) ||
23
+ Date.parse(v.expiresAt) <= Date.now())
24
+ throw new CommerceError("setup_file", "Download a fresh Agent setup file from Envar");
25
+ const origin = validateUrl(v.platformOrigin);
26
+ if (origin.origin !== v.platformOrigin)
27
+ throw new CommerceError("setup_origin", "Use the platform HTTPS origin");
28
+ return v;
29
+ }
30
+ export function prepareTextSkill(source, destination, name) {
31
+ if (!ID.test(name) || existsSync(destination))
32
+ throw new CommerceError("setup_skill", "Choose a new exact skill name");
33
+ const stat = lstatSync(source);
34
+ if (!stat.isDirectory() || stat.isSymbolicLink())
35
+ throw new CommerceError("setup_skill", "Choose a real skill directory");
36
+ cpSync(source, destination, {
37
+ recursive: true,
38
+ filter: (path) => {
39
+ const item = lstatSync(path);
40
+ if (item.isSymbolicLink())
41
+ throw new CommerceError("setup_skill", "Skill symlinks are unsupported");
42
+ if (item.isFile() &&
43
+ !/\.(md|txt)$/i.test(path) &&
44
+ !path.endsWith("/LICENSE"))
45
+ throw new CommerceError("setup_skill", "Choose a text-only package without scripts or binaries");
46
+ return true;
47
+ },
48
+ });
49
+ const file = join(destination, "SKILL.md"), original = readFileSync(file, "utf8");
50
+ if (!original.startsWith("---\n") && !original.startsWith("---\r\n"))
51
+ throw new CommerceError("setup_skill", "Skill needs YAML frontmatter");
52
+ const adapted = original.replace(/^(---\r?\n)([\s\S]*?)(\r?\n---)/, (_all, open, front, close) => open +
53
+ front.replace(/^envar-runtime:.*\r?\n?/gm, "") +
54
+ "\nenvar-runtime: instruction-only" +
55
+ close);
56
+ writeFileSync(file, adapted +
57
+ "\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 });
58
+ }
59
+ function privateJson(path, value) {
60
+ writeFileSync(path, JSON.stringify(value, null, 2) + "\n", {
61
+ mode: 0o600,
62
+ flag: "wx",
63
+ });
64
+ }
65
+ export async function setupHermes(options) {
66
+ const file = resolve(options.file), info = lstatSync(file);
67
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 16384)
68
+ throw new CommerceError("setup_file", "Use the downloaded setup JSON file");
69
+ const bundle = parseSetupBundle(JSON.parse(readFileSync(file, "utf8")));
70
+ if (options.container && !CONTAINER.test(options.container))
71
+ throw new CommerceError("setup_container", "Use a Docker container name");
72
+ if (!process.stdin.isTTY)
73
+ throw new CommerceError("setup_terminal", "Run setup in an interactive terminal to select skills");
74
+ chmodSync(file, 0o600);
75
+ const prompt = createInterface({
76
+ input: process.stdin,
77
+ output: process.stdout,
78
+ });
79
+ const ask = async (label, fallback = "") => (await prompt.question(`${label}${fallback ? ` [${fallback}]` : ""}: `)).trim() || fallback;
80
+ let staging;
81
+ try {
82
+ console.log(`\nConnect ${bundle.agentName} to ${bundle.platformOrigin}\nModel credentials and tasks remain on this machine. Start with free services; no wallet is created.\n`);
83
+ if ((await ask("Continue? (yes/no)", "no")).toLowerCase() !== "yes")
84
+ return;
85
+ const directory = resolve(options.directory ?? join(homedir(), ".envarpay", bundle.agentId));
86
+ if (existsSync(directory))
87
+ throw new CommerceError("setup_exists", "This Agent already has a local directory. Use its start script; never reset its ledger.");
88
+ let python = options.python;
89
+ 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"])';
90
+ if (!python && options.container) {
91
+ for (const candidate of [
92
+ "/opt/hermes-latest/bin/python",
93
+ "/opt/hermes/.venv/bin/python",
94
+ "python3",
95
+ ]) {
96
+ try {
97
+ execFileSync("docker", ["exec", options.container, candidate, "-c", compatibility], { stdio: "pipe", timeout: 60000 });
98
+ python = candidate;
99
+ break;
100
+ }
101
+ catch {
102
+ /* try the next compatible installed interpreter */
103
+ }
104
+ }
105
+ if (!python)
106
+ throw new CommerceError("setup_runtime", "No compatible Hermes interpreter found in this container. Update Hermes or pass --python PATH.");
107
+ }
108
+ python ??= await ask("Hermes Python executable", join(homedir(), "hermes-agent", ".venv", "bin", "python"));
109
+ const probeSource = readFileSync(new URL("../../examples/hermes-setup-probe.py", import.meta.url), "utf8");
110
+ let probe;
111
+ try {
112
+ if (options.container)
113
+ execFileSync("docker", ["exec", options.container, python, "-c", compatibility], { stdio: "pipe", timeout: 60000 });
114
+ else
115
+ execFileSync(python, ["-c", compatibility], {
116
+ stdio: "pipe",
117
+ timeout: 60000,
118
+ });
119
+ const args = ["-c", probeSource, ...(options.home ? [options.home] : [])];
120
+ const data = options.container
121
+ ? execFileSync("docker", ["exec", options.container, python, ...args], {
122
+ stdio: "pipe",
123
+ timeout: 30000,
124
+ maxBuffer: 2 * 1024 * 1024,
125
+ })
126
+ : execFileSync(python, args, {
127
+ stdio: "pipe",
128
+ timeout: 30000,
129
+ maxBuffer: 2 * 1024 * 1024,
130
+ });
131
+ probe = JSON.parse(data.toString());
132
+ }
133
+ catch {
134
+ throw new CommerceError("setup_model", "Could not read Hermes. Check the container/Python path and configure an OpenAI-compatible model in Hermes first.");
135
+ }
136
+ const candidates = probe.skills.filter((s) => s.textPackage && ID.test(s.name));
137
+ console.log(`Model: ${probe.model}\nInstalled text packages (review their instructions before selecting):`);
138
+ candidates.forEach((skill, i) => console.log(`${i + 1}. ${skill.name} — ${skill.description.replace(/[\r\n\x1b]/g, " ").slice(0, 160)}`));
139
+ console.log('Newly installed Skills will be discovered privately. Enable each one from the Envar service page after reviewing text-only compatibility.');
140
+ const selected = (await ask("Optional: enable existing text Skills now (comma separated, Enter to skip)"))
141
+ .split(",")
142
+ .map((s) => s.trim()).filter(Boolean);
143
+ if (selected.length > 32 ||
144
+ new Set(selected).size !== selected.length ||
145
+ selected.some((name) => !candidates.some((s) => s.name === name)))
146
+ throw new CommerceError("setup_skills", "Choose installed skill names from the list");
147
+ console.log("This adapter supplies buyer text and bundled Markdown references. Browser, script, file and API tools are disabled. Original Hermes skills are preserved; a service copy is created.");
148
+ if (selected.length &&
149
+ (await ask("Do these selected skills work with text input and text output only? (yes/no)", "no")).toLowerCase() !== "yes")
150
+ return;
151
+ const port = Number(await ask("Local service port", "4020"));
152
+ if (!Number.isInteger(port) || port < 1024 || port > 65535)
153
+ throw new CommerceError("setup_port", "Choose a local port from 1024 to 65535");
154
+ let origin;
155
+ const tunnelMode = await ask("Public HTTPS: use an existing ngrok session or your own reverse proxy? (ngrok/url)", "ngrok");
156
+ if (tunnelMode === "ngrok") {
157
+ const api = new URL(await ask("Local ngrok API", "http://127.0.0.1:4040"));
158
+ if (api.protocol !== "http:" ||
159
+ api.hostname !== "127.0.0.1" ||
160
+ api.username ||
161
+ api.password ||
162
+ api.pathname !== "/")
163
+ throw new CommerceError("setup_tunnel", "Use the local ngrok API on 127.0.0.1");
164
+ const name = `envarpay-${bundle.agentId}`, addr = `http://127.0.0.1:${port}`;
165
+ const inventory = await fetch(new URL("/api/tunnels", api), {
166
+ signal: AbortSignal.timeout(10000),
167
+ });
168
+ if (!inventory.ok)
169
+ throw new CommerceError("setup_tunnel", "Start ngrok first, or choose your own HTTPS URL");
170
+ const existing = (await inventory.json()).tunnels.find((t) => t.name === name);
171
+ if (existing) {
172
+ if (existing.config.addr !== addr)
173
+ throw new CommerceError("setup_tunnel", "The existing Agent tunnel uses another local port. Keep its original port.");
174
+ origin = existing.public_url;
175
+ }
176
+ else {
177
+ const response = await fetch(new URL("/api/tunnels", api), {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify({
181
+ name,
182
+ proto: "http",
183
+ addr,
184
+ schemes: ["https"],
185
+ inspect: false,
186
+ }),
187
+ signal: AbortSignal.timeout(15000),
188
+ });
189
+ if (!response.ok)
190
+ throw new CommerceError("setup_tunnel", "Could not create a dedicated tunnel. Check ngrok or choose your own HTTPS URL. Existing tunnels were preserved.");
191
+ origin = (await response.json()).public_url;
192
+ }
193
+ }
194
+ else if (tunnelMode === "url") {
195
+ origin = await ask(`Public HTTPS origin (forward to http://127.0.0.1:${port})`);
196
+ }
197
+ else
198
+ throw new CommerceError("setup_tunnel", "Choose ngrok or url");
199
+ const publicUrl = validateUrl(origin);
200
+ if (publicUrl.origin !== origin ||
201
+ (publicUrl.port && publicUrl.port !== "443"))
202
+ throw new CommerceError("setup_origin", "Use an HTTPS origin on port443");
203
+ // Only publish a complete new setup directory. An interrupted setup cannot strand
204
+ // the owner behind setup_exists or overwrite a prior ledger.
205
+ mkdirSync(resolve(directory, ".."), { recursive: true, mode: 0o700 });
206
+ staging = mkdtempSync(directory + ".setup-");
207
+ chmodSync(staging, 0o700);
208
+ const skillsDirectory = join(staging, "skills");
209
+ mkdirSync(skillsDirectory, { mode: 0o700 });
210
+ for (const name of selected) {
211
+ const skill = candidates.find((s) => s.name === name);
212
+ if (options.container) {
213
+ const incoming = join(staging, `incoming-${name}`);
214
+ execFileSync("docker", ["cp", `${options.container}:${skill.path}`, incoming], { stdio: "pipe", timeout: 30000 });
215
+ prepareTextSkill(incoming, join(skillsDirectory, name), name);
216
+ rmSync(incoming, { recursive: true });
217
+ }
218
+ else
219
+ prepareTextSkill(skill.path, join(skillsDirectory, name), name);
220
+ }
221
+ readInstalledSkills(skillsDirectory, [], origin + "/.well-known/agent-card.json");
222
+ const access = randomBytes(36).toString("base64url");
223
+ let apiKeyFile = probe.apiKeyFile;
224
+ if (!apiKeyFile) {
225
+ if (options.container) {
226
+ apiKeyFile = join(probe.home, "envarpay-secrets", bundle.agentId, "model.key");
227
+ // Key bytes travel through stdin, never process arguments or console output.
228
+ 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)";
229
+ try {
230
+ execFileSync("docker", [
231
+ "exec",
232
+ "-i",
233
+ options.container,
234
+ python,
235
+ "-c",
236
+ writeKey,
237
+ apiKeyFile,
238
+ ], {
239
+ input: probe.apiKey,
240
+ stdio: ["pipe", "pipe", "pipe"],
241
+ timeout: 15000,
242
+ });
243
+ }
244
+ catch {
245
+ throw new CommerceError("setup_model", "Could not save the local model credential. Inspect the Agent-specific envarpay-secrets directory before retrying.");
246
+ }
247
+ }
248
+ else {
249
+ apiKeyFile = join(directory, "model.key");
250
+ writeFileSync(join(staging, "model.key"), probe.apiKey, {
251
+ mode: 0o600,
252
+ flag: "wx",
253
+ });
254
+ }
255
+ }
256
+ const stateDirectory = join(directory, "tasks");
257
+ mkdirSync(join(staging, "tasks"), { mode: 0o700 });
258
+ const dockerStateDirectory = options.container
259
+ ? join(probe.home, "envarpay-tasks", bundle.agentId)
260
+ : undefined;
261
+ const config = loadCommerceConfig({
262
+ configVersion: 2,
263
+ agent: { id: bundle.runtimeAgentId, name: bundle.agentName },
264
+ paymentProfiles: {},
265
+ services: [],
266
+ });
267
+ privateJson(join(staging, "seller.json"), config);
268
+ privateJson(join(staging, "credentials.json"), {
269
+ callers: { [access]: "owner" },
270
+ upstreams: {},
271
+ skills: {
272
+ framework: "hermes",
273
+ discovery: { hermesHome: probe.home, enabledFromWeb: true },
274
+ skillsDirectory: join(directory, "skills"),
275
+ freeSkills: [],
276
+ stateDirectory,
277
+ python,
278
+ model: probe.model,
279
+ baseUrl: probe.baseUrl,
280
+ apiKeyFile,
281
+ ...(options.container
282
+ ? { dockerContainer: options.container, dockerStateDirectory }
283
+ : {}),
284
+ },
285
+ });
286
+ writeFileSync(join(staging, "envar.token"), bundle.token, {
287
+ mode: 0o600,
288
+ flag: "wx",
289
+ });
290
+ privateJson(join(staging, "envar.json"), {
291
+ policy: {
292
+ enabled: true,
293
+ platformOrigin: bundle.platformOrigin,
294
+ agentId: bundle.agentId,
295
+ runtimeAgentId: bundle.runtimeAgentId,
296
+ acceptUpdates: true,
297
+ allowedServices: selected,
298
+ allowedUpstreamOrigins: [origin],
299
+ allowedX402: [],
300
+ allowedMppAccounts: [],
301
+ },
302
+ stateDirectory: join(directory, "envar-state"),
303
+ configDirectory: join(directory, "envar-configs"),
304
+ });
305
+ privateJson(join(staging, "connection.json"), {
306
+ version: 1,
307
+ agentId: bundle.agentId,
308
+ url: origin + "/.well-known/agent-card.json",
309
+ authToken: access,
310
+ });
311
+ const cli = fileURLToPath(new URL("./cli.js", import.meta.url));
312
+ const args = [
313
+ cli,
314
+ "serve",
315
+ "--config",
316
+ join(directory, "seller.json"),
317
+ "--credentials",
318
+ join(directory, "credentials.json"),
319
+ "--state",
320
+ join(directory, "seller.sqlite3"),
321
+ "--origin",
322
+ origin,
323
+ "--host",
324
+ "127.0.0.1",
325
+ "--port",
326
+ String(port),
327
+ "--envar-config",
328
+ join(directory, "envar.json"),
329
+ "--envar-credentials",
330
+ join(directory, "envar.token"),
331
+ ];
332
+ const start = join(directory, "start.mjs");
333
+ 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" });
334
+ writeFileSync(join(staging, "README.md"), `# ${bundle.agentName}\n\nStart: node ${JSON.stringify(start)}\n\nKeep the terminal, Hermes, and HTTPS tunnel running. Import connection.json in the Envar setup page, then check the connection. Choose a skill and save a free service. Request publication; this process applies the selected service automatically. Return to Envar and check/publish.\n\nOnly free service updates are authorized. Receiving and payment setup is separate. Platform token expires ${bundle.expiresAt}. Keep the original files and ledger when restarting.\n`, { mode: 0o600, flag: "wx" });
335
+ renameSync(staging, directory);
336
+ staging = undefined;
337
+ console.log(`\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.`);
338
+ }
339
+ finally {
340
+ prompt.close();
341
+ if (staging)
342
+ rmSync(staging, { recursive: true, force: true });
343
+ }
344
+ }
@@ -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;