@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.
- package/README.md +2 -0
- package/dist/commerce/alerts.d.ts +37 -0
- package/dist/commerce/alerts.js +127 -0
- package/dist/commerce/buyer-store.d.ts +8 -0
- package/dist/commerce/buyer.d.ts +12 -3
- package/dist/commerce/buyer.js +151 -7
- package/dist/commerce/card.js +2 -2
- package/dist/commerce/cli.js +74 -9
- package/dist/commerce/client-runtime.d.ts +1 -1
- package/dist/commerce/config.d.ts +5 -1
- package/dist/commerce/config.js +37 -1
- package/dist/commerce/configure.d.ts +3 -0
- package/dist/commerce/configure.js +158 -0
- package/dist/commerce/discovery.d.ts +23 -0
- package/dist/commerce/discovery.js +162 -0
- package/dist/commerce/envar-runtime.d.ts +3 -1
- package/dist/commerce/envar-runtime.js +28 -1
- package/dist/commerce/envar.d.ts +5 -0
- package/dist/commerce/envar.js +13 -0
- package/dist/commerce/index.d.ts +1 -0
- package/dist/commerce/index.js +1 -0
- package/dist/commerce/management.js +3 -1
- package/dist/commerce/mpp-client.d.ts +11 -0
- package/dist/commerce/mpp-client.js +5 -0
- package/dist/commerce/mpp-runtime.d.ts +1 -0
- package/dist/commerce/mpp-runtime.js +1 -0
- package/dist/commerce/native-skills.d.ts +31 -0
- package/dist/commerce/native-skills.js +174 -0
- package/dist/commerce/operations.d.ts +22 -0
- package/dist/commerce/operations.js +39 -0
- package/dist/commerce/peer-proxy.js +11 -5
- package/dist/commerce/pricing.js +2 -2
- package/dist/commerce/runtime.d.ts +2 -0
- package/dist/commerce/runtime.js +2 -0
- package/dist/commerce/server.d.ts +4 -1
- package/dist/commerce/server.js +40 -9
- package/dist/commerce/setup.d.ts +18 -0
- package/dist/commerce/setup.js +344 -0
- package/dist/commerce/skill-gate.d.ts +54 -0
- package/dist/commerce/skill-gate.js +153 -0
- package/dist/commerce/store.d.ts +2 -0
- package/dist/commerce/store.js +7 -0
- package/dist/commerce/stripe-issuer.d.ts +41 -0
- package/dist/commerce/stripe-issuer.js +351 -0
- package/dist/commerce/stripe-provider.d.ts +2 -0
- package/dist/commerce/stripe-provider.js +57 -22
- package/dist/commerce/types.d.ts +27 -2
- package/dist/commerce/x402.d.ts +1 -0
- package/dist/commerce/x402.js +8 -1
- package/examples/hermes-setup-probe.py +105 -0
- package/examples/hermes-skill-inventory.py +107 -0
- package/examples/native-skill-runner.py +197 -0
- package/package.json +1 -1
- package/schemas/buyer-policy.schema.json +105 -0
- 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
|
}
|
package/dist/commerce/envar.d.ts
CHANGED
|
@@ -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[];
|
package/dist/commerce/envar.js
CHANGED
|
@@ -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");
|
package/dist/commerce/index.d.ts
CHANGED
package/dist/commerce/index.js
CHANGED
|
@@ -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 })] });
|
|
@@ -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
|
-
:
|
|
255
|
-
? "
|
|
256
|
-
:
|
|
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"
|
package/dist/commerce/pricing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
3
3
|
import addFormatsImport from 'ajv-formats';
|
|
4
|
-
import { atomic, currencyOf, digest } from './config.js';
|
|
4
|
+
import { atomic, currencyOf, digest, serviceTermsDigest } from './config.js';
|
|
5
5
|
import { CommerceError } from './types.js';
|
|
6
6
|
const addFormats = addFormatsImport;
|
|
7
7
|
export function buildQuote(config, args) {
|
|
@@ -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:
|
|
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,
|
package/dist/commerce/runtime.js
CHANGED
|
@@ -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;
|