@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
@@ -50,15 +50,17 @@ export class PinnedUpstreams {
50
50
  inputEncoding;
51
51
  fetchImpl;
52
52
  signal;
53
+ skills;
53
54
  bindings = {};
54
55
  executors = new Map();
55
56
  execute;
56
- constructor(path, credentials, inputEncoding = {}, fetchImpl = fetch, signal) {
57
+ constructor(path, credentials, inputEncoding = {}, fetchImpl = fetch, signal, skills) {
57
58
  this.path = path;
58
59
  this.credentials = credentials;
59
60
  this.inputEncoding = inputEncoding;
60
61
  this.fetchImpl = fetchImpl;
61
62
  this.signal = signal;
63
+ this.skills = skills;
62
64
  if (existsSync(path)) {
63
65
  const value = privateJson(path);
64
66
  if (value.version !== 1 ||
@@ -92,10 +94,22 @@ export class PinnedUpstreams {
92
94
  }
93
95
  return undefined;
94
96
  };
97
+ if (skills) {
98
+ this.execute.assertSkill = (service) => skills.assertSkill(service);
99
+ this.execute.skillCard = skills.skillCard;
100
+ }
95
101
  }
96
102
  prepare(config, history = []) {
97
103
  const next = structuredClone(this.bindings), current = new Set(config.services.map((s) => `${s.id}:${s.revision}`));
98
104
  for (const service of [...history, ...config.services]) {
105
+ if (service.execution.type === 'skill') {
106
+ if (!this.skills)
107
+ throw new CommerceError('skill_gate_required', 'Configure the native skill runtime');
108
+ // Retired versions stay readable even after their skill package is upgraded.
109
+ if (current.has(`${service.id}:${service.revision}`))
110
+ this.skills.assertSkill(service);
111
+ continue;
112
+ }
99
113
  const key = `${service.id}:${service.revision}`, saved = next[key], explicit = this.credentials[key];
100
114
  if (saved) {
101
115
  if (saved.cardUrl !== service.execution.cardUrl ||
@@ -120,6 +134,11 @@ export class PinnedUpstreams {
120
134
  this.bindings = next;
121
135
  }
122
136
  executor(service) {
137
+ if (service.execution.type === 'skill') {
138
+ if (!this.skills)
139
+ throw new CommerceError('skill_gate_required', 'Configure the native skill runtime');
140
+ return this.skills;
141
+ }
123
142
  const key = `${service.id}:${service.revision}`, binding = this.bindings[key];
124
143
  if (!binding || binding.cardUrl !== service.execution.cardUrl)
125
144
  throw new CommerceError("upstream_binding_missing", "Original runtime credential binding is unavailable");
@@ -147,6 +166,7 @@ export function mergeEnvarService(base, candidate) {
147
166
  const profiles = { ...base.paymentProfiles, ...incoming.paymentProfiles }, services = [...retained, service], used = new Set(services.flatMap((s) => s.offers.map((o) => o.paymentProfile).filter((p) => !!p)));
148
167
  return loadCommerceConfig({
149
168
  ...base,
169
+ configVersion: Math.max(base.configVersion, incoming.configVersion),
150
170
  paymentProfiles: Object.fromEntries(Object.entries(profiles).filter(([key]) => used.has(key))),
151
171
  services,
152
172
  });
@@ -211,6 +231,13 @@ export class EnvarSellerRuntime {
211
231
  return this.running;
212
232
  this.running = (async () => {
213
233
  try {
234
+ try {
235
+ await this.options.beforeSync?.();
236
+ }
237
+ catch {
238
+ // A discovery failure must not stall existing service updates or delivery reports.
239
+ this.options.onError?.("skill_discovery");
240
+ }
214
241
  for (const candidate of await this.options.integration.pullCandidates())
215
242
  await this.options.integration.applyCandidate(candidate, (c) => this.apply(c));
216
243
  }
@@ -81,6 +81,11 @@ export declare class EnvarIntegration {
81
81
  close(): void;
82
82
  private open;
83
83
  private request;
84
+ allowInstalledService(name: string): void;
85
+ reportRuntime(value: unknown): Promise<{
86
+ name: string;
87
+ digest: string;
88
+ }[]>;
84
89
  private candidate;
85
90
  /** Latest locally acknowledged revisions; startup recovery never requires platform connectivity. */
86
91
  appliedCandidates(): EnvarCandidate[];
@@ -218,6 +218,7 @@ export class EnvarIntegration {
218
218
  "/api/v1/agent-config/candidates",
219
219
  "/api/v1/agent-config/acknowledgments",
220
220
  "/api/v1/commerce-reports",
221
+ "/api/v1/agent-config/runtime",
221
222
  ].includes(path))
222
223
  fail("envar_path", "Unsupported platform integration operation");
223
224
  const token = await this.options.token();
@@ -297,6 +298,18 @@ export class EnvarIntegration {
297
298
  fail("envar_response_json", "Platform response must be an object");
298
299
  return value;
299
300
  }
301
+ allowInstalledService(name) {
302
+ if (!ID.test(name))
303
+ fail('envar_policy', 'Invalid installed Skill name');
304
+ if (!this.policy.allowedServices.includes(name))
305
+ this.policy.allowedServices.push(name);
306
+ }
307
+ async reportRuntime(value) {
308
+ const response = await this.request('/api/v1/agent-config/runtime', value, randomUUID());
309
+ if (response.agent_id !== this.policy.agentId || !Array.isArray(response.enable) || response.enable.length > 128)
310
+ fail('envar_inventory', 'Invalid runtime acknowledgment');
311
+ return response.enable.filter((x) => x && ID.test(x.name) && /^[0-9a-f]{64}$/.test(x.digest));
312
+ }
300
313
  candidate(value) {
301
314
  if (!this.policy.acceptUpdates)
302
315
  fail("envar_updates_disabled", "Local owner has not enabled platform configuration updates");
@@ -2,3 +2,4 @@ export * from './types.js';
2
2
  export * from './config.js';
3
3
  export * from './pricing.js';
4
4
  export * from './card.js';
5
+ export * from './skill-gate.js';
@@ -2,3 +2,4 @@ export * from './types.js';
2
2
  export * from './config.js';
3
3
  export * from './pricing.js';
4
4
  export * from './card.js';
5
+ export * from './skill-gate.js';
@@ -77,9 +77,11 @@ export class BuyerManagement {
77
77
  throw new CommerceError('invalid_request', 'Confirmation identifiers must be strings');
78
78
  return Response.json(await this.options.buyer.confirm(caller, body), { headers });
79
79
  }
80
- const match = /^\/management\/v1\/purchases\/([0-9a-f-]{36})(\/(?:recover|continue))?$/.exec(url.pathname);
80
+ const match = /^\/management\/v1\/purchases\/([0-9a-f-]{36})(\/(?:recover|continue|authentication))?$/.exec(url.pathname);
81
81
  if (match && request.method === 'GET' && !match[2])
82
82
  return Response.json(this.options.buyer.get(caller, match[1]), { headers });
83
+ if (match && request.method === 'GET' && match[2] === '/authentication')
84
+ return Response.json(await this.options.buyer.authentication(caller, match[1]), { headers });
83
85
  if (match && request.method === 'POST' && match[2] === '/continue') {
84
86
  const body = await jsonBody(request);
85
87
  exactKeys(body, ['messageId', 'input']);
@@ -28,16 +28,27 @@ export interface MppVerificationContext {
28
28
  mode: 'test' | 'live';
29
29
  operationId: string;
30
30
  }
31
+ export interface MppAuthenticationAction {
32
+ type: 'use_stripe_sdk';
33
+ hashedValue: string;
34
+ publishableKey: string;
35
+ }
36
+ /** Explicit official sandbox aliases, never accepted for live funding. */
37
+ export declare const STRIPE_TEST_PAYMENT_METHODS: readonly ["pm_card_visa", "pm_card_visa_chargeDeclined", "pm_card_authenticationRequired"];
38
+ export declare function isStripeTestPaymentMethod(value: string): boolean;
31
39
  export interface MppBuyerOptions {
32
40
  /** Stable private buyer/PSP identity; never inferred from seller metadata. */
33
41
  payer: string;
34
42
  mode: 'test' | 'live';
35
43
  paymentMethod: string;
44
+ issuance?: 'issued-token' | 'test-helper';
36
45
  createToken: (operation: MppTokenOperation) => Promise<string>;
37
46
  /** Read the original operation only; never create another SPT on absence. */
38
47
  recoverToken?: (operation: MppTokenOperation) => Promise<string | undefined>;
39
48
  /** Independently read provider state and match account/mode/merchant/amount/order. */
40
49
  verifyReceipt: (receipt: MppReceipt, context: MppVerificationContext) => Promise<boolean>;
50
+ /** Read only, for the owner of this exact original purchase. Never completes payment. */
51
+ getAuthentication?: (context: MppVerificationContext) => Promise<MppAuthenticationAction | undefined>;
41
52
  }
42
53
  export interface SavedMppAuthorization {
43
54
  protocol: 'mpp';
@@ -3,6 +3,11 @@ import { Mppx } from 'mppx/client';
3
3
  import { charge } from 'mppx/stripe/client';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { CommerceError } from './types.js';
6
+ /** Explicit official sandbox aliases, never accepted for live funding. */
7
+ export const STRIPE_TEST_PAYMENT_METHODS = ['pm_card_visa', 'pm_card_visa_chargeDeclined', 'pm_card_authenticationRequired'];
8
+ export function isStripeTestPaymentMethod(value) {
9
+ return STRIPE_TEST_PAYMENT_METHODS.includes(value);
10
+ }
6
11
  /** Native SDK preparation only; no fetch polyfill or automatic payment retry. */
7
12
  export async function prepareMppCredential(challenge, paymentMethod, createToken) {
8
13
  const client = Mppx.create({ polyfill: false, maxPaymentRetries: 0, methods: [charge({ paymentMethod, createToken })] });
@@ -1,3 +1,4 @@
1
1
  /** Opt-in MPP server adapter. Account eligibility is checked at startup. */
2
2
  export * from './mpp.js';
3
3
  export * from './stripe-provider.js';
4
+ export * from './stripe-issuer.js';
@@ -1,3 +1,4 @@
1
1
  /** Opt-in MPP server adapter. Account eligibility is checked at startup. */
2
2
  export * from './mpp.js';
3
3
  export * from './stripe-provider.js';
4
+ export * from './stripe-issuer.js';
@@ -0,0 +1,31 @@
1
+ import { type SkillDescriptor } from './skill-gate.js';
2
+ import type { ExecuteOrder } from './server.js';
3
+ export interface NativeSkillSettings {
4
+ framework: 'hermes' | 'openclaw';
5
+ skillsDirectory: string;
6
+ freeSkills: string[];
7
+ stateDirectory: string;
8
+ python: string;
9
+ command?: string[];
10
+ model: string;
11
+ baseUrl: string;
12
+ apiKeyFile: string;
13
+ /** Host-side Docker CLI, never a Docker socket mounted into the Agent. */
14
+ dockerContainer?: string;
15
+ dockerStateDirectory?: string;
16
+ discovery?: {
17
+ hermesHome: string;
18
+ enabledFromWeb: true;
19
+ };
20
+ }
21
+ interface InstalledSkill {
22
+ descriptor: SkillDescriptor;
23
+ instructions: string;
24
+ }
25
+ /** Hash every resource; reject symlinks, unbounded packages and executable skill requirements. */
26
+ export declare function readInstalledSkills(root: string, freeSkills: readonly string[], cardUrl: string): InstalledSkill[];
27
+ export declare function createNativeSkillExecutor(settings: NativeSkillSettings, origin: string, agentName: string): {
28
+ execute: ExecuteOrder;
29
+ close(): Promise<void>;
30
+ };
31
+ export {};
@@ -0,0 +1,174 @@
1
+ /** Owner-operated, instruction-only skill runtime. No model file, shell or network tools. */
2
+ import { AgentCard, Task } from '@a2a-js/sdk';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { lstatSync, readFileSync, readdirSync, mkdirSync } from 'node:fs';
5
+ import { resolve, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { spawn } from 'node:child_process';
8
+ import { DatabaseSync } from 'node:sqlite';
9
+ import { digest } from './config.js';
10
+ import { CommerceError } from './types.js';
11
+ import { skillScopedExecutor } from './skill-gate.js';
12
+ const namePattern = /^[a-z][a-z0-9-]{0,63}$/;
13
+ /** Hash every resource; reject symlinks, unbounded packages and executable skill requirements. */
14
+ export function readInstalledSkills(root, freeSkills, cardUrl) {
15
+ const base = resolve(root), rootStat = lstatSync(base);
16
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
17
+ throw new CommerceError('skill_directory', 'Use a real installed skill directory');
18
+ const output = [];
19
+ for (const entry of readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
20
+ if (entry.name.startsWith('.'))
21
+ continue;
22
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !namePattern.test(entry.name))
23
+ throw new CommerceError('skill_package', 'Use one real directory per exact skill name');
24
+ const files = [];
25
+ let total = 0;
26
+ function walk(folder, prefix = '') {
27
+ if (prefix.split('/').length > 12)
28
+ throw new CommerceError('skill_size', 'Skill package nesting exceeds the supported limit');
29
+ for (const item of readdirSync(folder, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
30
+ const path = join(folder, item.name), relative = prefix + item.name;
31
+ if (item.isSymbolicLink())
32
+ throw new CommerceError('skill_symlink', 'Skill packages cannot contain symlinks');
33
+ if (item.isDirectory()) {
34
+ walk(path, relative + '/');
35
+ continue;
36
+ }
37
+ const stat = lstatSync(path);
38
+ if (!stat.isFile() || stat.size > 512 * 1024 || files.length >= 128 || (total += stat.size) > 1024 * 1024)
39
+ throw new CommerceError('skill_size', 'Skill package exceeds supported size');
40
+ files.push({ path: relative, sha256: createHash('sha256').update(readFileSync(path)).digest('hex') });
41
+ }
42
+ }
43
+ walk(join(base, entry.name));
44
+ const instructions = readFileSync(join(base, entry.name, 'SKILL.md'), 'utf8');
45
+ const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(instructions)?.[1];
46
+ const name = /^name:\s*["']?([a-z][a-z0-9-]*)["']?\s*$/m.exec(front ?? '')?.[1];
47
+ if (name !== entry.name || instructions.length > 48000)
48
+ throw new CommerceError('skill_name_mismatch', 'SKILL.md name must match its installed directory');
49
+ // This adapter deliberately supports only text-in/text-out workflows. Owners must use
50
+ // a separately reviewed adapter for skills needing code, browsing, external APIs or files.
51
+ if (!/^envar-runtime:\s*instruction-only\s*$/m.test(front ?? ''))
52
+ throw new CommerceError('skill_runtime_required', 'Review and mark each compatible skill envar-runtime: instruction-only before publication');
53
+ const description = /^description:\s*(.+)$/m.exec(front ?? '')?.[1]?.replace(/^["']|["']$/g, '') ?? name;
54
+ const references = files.filter(file => file.path !== 'SKILL.md' && file.path.endsWith('.md')).map(file => `\n\n<skill-reference path="${file.path}">\n${readFileSync(join(base, entry.name, file.path), 'utf8')}\n</skill-reference>`).join('');
55
+ if (instructions.length + references.length > 120000)
56
+ throw new CommerceError('skill_size', 'Instruction resources exceed the supported context size');
57
+ output.push({ descriptor: { name, digest: digest(files), cardUrl, access: freeSkills.includes(name) ? 'free' : 'paid', description }, instructions: instructions + references });
58
+ }
59
+ if (output.length > 32 || freeSkills.some(name => !output.some(s => s.descriptor.name === name)))
60
+ throw new CommerceError('skill_inventory', 'Install at most 32 skills and declare only installed free skills');
61
+ // Free helpers are executable dependencies of every purchase, so their versions
62
+ // and access-policy changes must invalidate the quote's selected skill digest too.
63
+ const helpers = output.filter(s => s.descriptor.access === 'free').map(s => ({ name: s.descriptor.name, digest: s.descriptor.digest }));
64
+ for (const skill of output)
65
+ skill.descriptor.digest = digest({ package: skill.descriptor.digest, helpers });
66
+ return output;
67
+ }
68
+ export function createNativeSkillExecutor(settings, origin, agentName) {
69
+ if (!['hermes', 'openclaw'].includes(settings.framework) || !Array.isArray(settings.freeSkills) || !settings.python || !settings.model || !settings.baseUrl)
70
+ throw new CommerceError('skill_settings', 'Configure a supported native skill runtime');
71
+ const cardUrl = new URL('/.well-known/agent-card.json', origin).href;
72
+ const installed = () => readInstalledSkills(settings.skillsDirectory, settings.freeSkills, cardUrl);
73
+ installed();
74
+ mkdirSync(settings.stateDirectory, { recursive: true, mode: 0o700 });
75
+ const stateStat = lstatSync(settings.stateDirectory);
76
+ if (!stateStat.isDirectory() || stateStat.isSymbolicLink() || (stateStat.mode & 0o077) !== 0)
77
+ throw new CommerceError('skill_state', 'Skill state must be owner-only');
78
+ const db = new DatabaseSync(join(settings.stateDirectory, 'skills.sqlite3'));
79
+ db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; CREATE TABLE IF NOT EXISTS skill_tasks (id TEXT PRIMARY KEY, order_id TEXT UNIQUE NOT NULL, binding TEXT NOT NULL, task_json TEXT NOT NULL)');
80
+ for (const row of db.prepare('SELECT id,task_json FROM skill_tasks').all()) {
81
+ const task = JSON.parse(String(row.task_json));
82
+ if (task.status?.state === 'TASK_STATE_WORKING') {
83
+ task.status = { state: 'TASK_STATE_FAILED', message: { role: 'ROLE_AGENT', messageId: randomUUID(), parts: [{ text: 'Native execution interrupted; inspect this original order' }] } };
84
+ db.prepare('UPDATE skill_tasks SET task_json=? WHERE id=?').run(JSON.stringify(task), row.id);
85
+ }
86
+ }
87
+ const inFlight = new Map();
88
+ const runner = fileURLToPath(new URL('../../examples/native-skill-runner.py', import.meta.url));
89
+ function readTask(id, invocation) {
90
+ const row = db.prepare('SELECT order_id,task_json FROM skill_tasks WHERE id=?').get(id);
91
+ if (!row || row.order_id !== invocation.orderId)
92
+ throw new CommerceError('skill_task_not_found', 'Original skill Task not found');
93
+ return Task.fromJSON(JSON.parse(String(row.task_json)));
94
+ }
95
+ function executeProcess(payload) {
96
+ return new Promise((accept, reject) => {
97
+ const docker = settings.dockerContainer;
98
+ if (docker && !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(docker))
99
+ throw new CommerceError('skill_container', 'Invalid Docker container name');
100
+ const child = docker
101
+ ? spawn('docker', ['exec', '-i', docker, settings.python, '-c', readFileSync(runner, 'utf8')], { stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: process.env.PATH, HOME: process.env.HOME, DOCKER_CONFIG: process.env.DOCKER_CONFIG, DOCKER_HOST: process.env.DOCKER_HOST } })
102
+ : spawn(settings.python, [runner], { stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: process.env.PATH, LANG: 'C.UTF-8', PYTHONUNBUFFERED: '1' } });
103
+ const chunks = [];
104
+ let size = 0;
105
+ const timeout = setTimeout(() => child.kill('SIGTERM'), 240000);
106
+ child.stdout.on('data', (chunk) => { size += chunk.length; if (size > 1024 * 1024)
107
+ child.kill('SIGTERM');
108
+ else
109
+ chunks.push(chunk); });
110
+ child.stderr.resume();
111
+ child.on('error', () => { clearTimeout(timeout); reject(new CommerceError('skill_runtime_start', 'Native skill runtime could not start')); });
112
+ child.on('close', code => {
113
+ clearTimeout(timeout);
114
+ try {
115
+ if (code !== 0 || size > 1024 * 1024)
116
+ throw Error();
117
+ const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
118
+ if (typeof result.output !== 'string' || !result.output.trim() || result.framework !== settings.framework || !Array.isArray(result.tools) || result.tools.length)
119
+ throw Error();
120
+ accept(result);
121
+ }
122
+ catch {
123
+ reject(new CommerceError('skill_runtime_failed', 'Native skill execution failed; inspect the original Task without resubmitting'));
124
+ }
125
+ });
126
+ child.stdin.on('error', () => { });
127
+ child.stdin.end(JSON.stringify(payload));
128
+ });
129
+ }
130
+ const runtime = {
131
+ list: () => installed().map(s => s.descriptor),
132
+ async send(name, input, invocation) {
133
+ const skills = installed(), selected = skills.find(s => s.descriptor.name === name);
134
+ if (!selected || selected.descriptor.digest !== invocation.grant.skillDigest)
135
+ throw new CommerceError('skill_changed', 'The purchased skill version is no longer installed');
136
+ invocation.grant.require(name);
137
+ const allowed = skills.filter(s => s.descriptor.name === name || s.descriptor.access === 'free');
138
+ allowed.forEach(s => invocation.grant.require(s.descriptor.name));
139
+ const binding = digest({ name, version: selected.descriptor.digest, caller: invocation.grant.caller, input, allowed: allowed.map(s => s.descriptor) });
140
+ const prior = db.prepare('SELECT id,binding FROM skill_tasks WHERE order_id=?').get(invocation.orderId);
141
+ if (prior) {
142
+ if (prior.binding !== binding)
143
+ throw new CommerceError('skill_task_conflict', 'An order already identifies a different skill input');
144
+ return readTask(String(prior.id), invocation);
145
+ }
146
+ const id = randomUUID();
147
+ // A crash cannot cause a second native dispatch. The durable Task remains failed until owner inspection.
148
+ const pending = Task.fromJSON({ id, status: { state: 'TASK_STATE_WORKING' } });
149
+ db.prepare('INSERT INTO skill_tasks VALUES (?,?,?,?)').run(id, invocation.orderId, binding, JSON.stringify(Task.toJSON(pending)));
150
+ const operation = (async () => {
151
+ let task;
152
+ try {
153
+ const result = await executeProcess({ settings: { ...settings, stateDirectory: join(settings.dockerStateDirectory ?? settings.stateDirectory, id) }, skillName: name, skills: allowed.map(s => ({ name: s.descriptor.name, instructions: s.instructions })), input });
154
+ task = Task.fromJSON({ id, status: { state: 'TASK_STATE_COMPLETED' }, artifacts: [{ artifactId: 'result', name: name + '.md', parts: [{ text: result.output, mediaType: 'text/markdown' }] }], metadata: { skillName: name, skillDigest: selected.descriptor.digest, allowedSkills: allowed.map(s => s.descriptor.name), framework: settings.framework, tools: result.tools, sessionId: result.sessionId ?? id } });
155
+ }
156
+ catch (error) {
157
+ task = Task.fromJSON({ id, status: { state: 'TASK_STATE_FAILED' }, metadata: { errorCode: error instanceof CommerceError ? error.code : 'skill_runtime_failed' } });
158
+ }
159
+ db.prepare('UPDATE skill_tasks SET task_json=? WHERE id=?').run(JSON.stringify(Task.toJSON(task)), id);
160
+ return task;
161
+ })();
162
+ inFlight.set(id, operation);
163
+ void operation.finally(() => inFlight.delete(id));
164
+ return pending;
165
+ },
166
+ async getTask(_name, id, invocation) { return readTask(id, invocation); },
167
+ };
168
+ const execute = skillScopedExecutor(runtime);
169
+ execute.skillCard = () => {
170
+ const skills = installed();
171
+ return AgentCard.fromJSON({ name: agentName, description: 'Installed Agent Skills', version: '1.0', supportedInterfaces: [{ url: new URL('/a2a', origin).href, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }], capabilities: { extensions: [{ uri: 'urn:envarpay:skill-gate:1', required: false, params: { runtime: 'instruction-only-v1', skills: skills.map(s => ({ name: s.descriptor.name, digest: s.descriptor.digest, access: s.descriptor.access })) } }] }, defaultInputModes: ['application/json'], defaultOutputModes: ['text/markdown'], skills: skills.map(s => ({ id: s.descriptor.name, name: s.descriptor.name, description: s.descriptor.description, tags: [s.descriptor.name] })) });
172
+ };
173
+ return { execute, close: async () => { await Promise.allSettled(inFlight.values()); db.close(); } };
174
+ }
@@ -0,0 +1,22 @@
1
+ export { inspectLocalAlerts, type LocalAlertOptions } from './alerts.js';
2
+ export type InspectionView = 'attention' | 'refund-review';
3
+ /** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
4
+ export declare function inspectLedger(path: string, after?: number, limit?: number, view?: InspectionView): {
5
+ role: string;
6
+ view: InspectionView;
7
+ total: number;
8
+ attentionCount: number;
9
+ refundReviewCount: number;
10
+ items: {
11
+ reason?: "confirmed_payment_failed_execution" | undefined;
12
+ cursor: number;
13
+ id: string;
14
+ paymentState: string;
15
+ executionState: string;
16
+ taskId: string | null;
17
+ }[];
18
+ nextCursor: number;
19
+ hasMore: boolean;
20
+ paymentPerformed: boolean;
21
+ refundPerformed: boolean;
22
+ };
@@ -0,0 +1,39 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { lstatSync } from 'node:fs';
3
+ import { CommerceError } from './types.js';
4
+ export { inspectLocalAlerts } from './alerts.js';
5
+ /** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
6
+ export function inspectLedger(path, after = 0, limit = 100, view = 'attention') {
7
+ const stat = lstatSync(path);
8
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
9
+ throw new CommerceError('private_file', 'Inspect an existing ledger owned only by the current user');
10
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
11
+ throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit from1 to100');
12
+ if (!['attention', 'refund-review'].includes(view))
13
+ throw new CommerceError('inspection_view', 'Use attention or refund-review inspection');
14
+ const db = new DatabaseSync(path, { readOnly: true });
15
+ try {
16
+ db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000');
17
+ const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
18
+ const role = names.has('buyer_purchases') ? 'buyer' : names.has('commerce_orders') ? 'seller' : null;
19
+ if (!role)
20
+ throw new CommerceError('store_version', 'Unrecognized commerce ledger');
21
+ const meta = role === 'buyer' ? 'buyer_meta' : 'commerce_meta', version = role === 'buyer' ? 1 : 2;
22
+ if (db.prepare(`SELECT version FROM ${meta}`).get()?.version !== version)
23
+ throw new CommerceError('store_version', 'Unsupported commerce ledger version');
24
+ const table = role === 'buyer' ? 'buyer_purchases' : 'commerce_orders';
25
+ const total = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
26
+ const attention = role === 'buyer' ? "json_extract(record_json,'$.state') IN ('signing','submitted','unknown') OR json_extract(record_json,'$.paymentState') IN ('reserved','unknown') OR json_extract(record_json,'$.executionState')='unknown'" : "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
27
+ const refundReview = role === 'buyer' ? "json_extract(record_json,'$.paymentState')='confirmed' AND json_extract(record_json,'$.executionState')='failed'" : "payment_state='confirmed' AND execution_state='failed'";
28
+ const filter = view === 'refund-review' ? refundReview : attention;
29
+ const selected = role === 'buyer' ? "rowid AS cursor,id,json_extract(record_json,'$.paymentState') AS payment_state,json_extract(record_json,'$.executionState') AS execution_state,json_extract(record_json,'$.task.id') AS task_id" : "rowid AS cursor,id,payment_state,execution_state,task_id";
30
+ const rows = db.prepare(`SELECT ${selected} FROM ${table} WHERE rowid>? AND (${filter}) ORDER BY rowid LIMIT ?`).all(after, limit + 1);
31
+ const page = rows.slice(0, limit).map(row => ({ cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state), executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null, ...(view === 'refund-review' ? { reason: 'confirmed_payment_failed_execution' } : {}) }));
32
+ const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${attention}`).get().n);
33
+ const refundReviewCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${refundReview}`).get().n);
34
+ return { role, view, total, attentionCount, refundReviewCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false, refundPerformed: false };
35
+ }
36
+ finally {
37
+ db.close();
38
+ }
39
+ }
@@ -230,7 +230,7 @@ export class A2APeerProxy {
230
230
  let raw = snapshot?.task
231
231
  ? structuredClone(snapshot.task)
232
232
  : {};
233
- if (!code && ["failed", "unknown"].includes(String(row.phase))) {
233
+ if (!code && ["created", "attention", "failed", "unknown"].includes(String(row.phase))) {
234
234
  try {
235
235
  const previous = JSON.parse(String(row.task_json));
236
236
  if (typeof previous.metadata?.errorCode === "string")
@@ -251,9 +251,11 @@ export class A2APeerProxy {
251
251
  status: {
252
252
  state: needsApproval
253
253
  ? "TASK_STATE_AUTH_REQUIRED"
254
- : failed
255
- ? "TASK_STATE_FAILED"
256
- : "TASK_STATE_WORKING",
254
+ : phase === "attention"
255
+ ? "TASK_STATE_INPUT_REQUIRED"
256
+ : failed
257
+ ? "TASK_STATE_FAILED"
258
+ : "TASK_STATE_WORKING",
257
259
  },
258
260
  };
259
261
  }
@@ -458,9 +460,13 @@ export class A2APeerProxy {
458
460
  "peer_not_allowed",
459
461
  "invalid_input",
460
462
  "purchase_limit",
463
+ "paid_quote_required",
464
+ "offer_metadata_required",
465
+ "quote_mismatch",
466
+ "retired_revision",
461
467
  ].includes(error.code);
462
468
  this.phase(id, definitelyBlocked
463
- ? "failed"
469
+ ? state === "previewing" ? "attention" : "failed"
464
470
  : state === "confirming"
465
471
  ? "unknown"
466
472
  : state === "previewing"
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { Ajv2020 } from 'ajv/dist/2020.js';
3
3
  import addFormatsImport from 'ajv-formats';
4
- import { atomic, currencyOf, digest } from './config.js';
4
+ import { atomic, currencyOf, digest, serviceTermsDigest } from './config.js';
5
5
  import { CommerceError } from './types.js';
6
6
  const addFormats = addFormatsImport;
7
7
  export function buildQuote(config, args) {
@@ -43,7 +43,7 @@ export function buildQuote(config, args) {
43
43
  const quote = {
44
44
  quoteVersion: 1, quoteId: randomUUID(), caller: args.caller, serviceId: service.id,
45
45
  serviceRevision: service.revision, offerId: offer.id, messageId: args.messageId,
46
- inputDigest: digest(args.input), termsDigest: digest({ contract: service.contract, offer, profile }),
46
+ inputDigest: digest(args.input), termsDigest: serviceTermsDigest(config.configVersion, service, offer, profile),
47
47
  issuedAt: now.toISOString(), expiresAt: new Date(now.getTime() + ttl * 1000).toISOString(),
48
48
  amount: amount.toString(), currency: profile ? currencyOf(profile) : null,
49
49
  recipient: recipient ?? null, paymentProfile: profile ? structuredClone(profile) : null,
@@ -2,6 +2,8 @@
2
2
  export * from './store.js';
3
3
  export * from './server.js';
4
4
  export * from './upstream.js';
5
+ export * from './skill-gate.js';
6
+ export * from './native-skills.js';
5
7
  export * from './http.js';
6
8
  export * from './x402.js';
7
9
  export * from './vault.js';
@@ -2,6 +2,8 @@
2
2
  export * from './store.js';
3
3
  export * from './server.js';
4
4
  export * from './upstream.js';
5
+ export * from './skill-gate.js';
6
+ export * from './native-skills.js';
5
7
  export * from './http.js';
6
8
  export * from './x402.js';
7
9
  export * from './vault.js';
@@ -1,9 +1,11 @@
1
- import { Task, SendMessageRequest } from '@a2a-js/sdk';
1
+ import { AgentCard, Task, SendMessageRequest } from '@a2a-js/sdk';
2
2
  import { ServerCallContext } from '@a2a-js/sdk/server';
3
3
  import { CommerceStore, type OrderRecord, type ContinuationRecord, type RemoteTaskReference } from './store.js';
4
4
  import { type CommerceConfig, type Input, type Service } from './types.js';
5
5
  export interface ExecuteOrder {
6
6
  (order: OrderRecord, service: Service): AsyncIterable<Task>;
7
+ assertSkill?: (service: Service) => unknown;
8
+ skillCard?: () => AgentCard;
7
9
  continue?: (order: OrderRecord, service: Service, continuation: ContinuationRecord, remote: RemoteTaskReference) => AsyncIterable<Task>;
8
10
  recover?: (order: OrderRecord, service: Service, remote: RemoteTaskReference) => Promise<Task>;
9
11
  remoteInterface?: (task: Task) => string | undefined;
@@ -36,6 +38,7 @@ export declare class CommerceServer {
36
38
  private running;
37
39
  private kickPending;
38
40
  private stopped;
41
+ private workerFailed;
39
42
  private waiters;
40
43
  private activeOrders;
41
44
  private recoveries;