@envarai/envarpay 0.2.0-alpha.2 → 0.2.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +18 -56
  2. package/dist/commerce/agent-setup.d.ts +19 -0
  3. package/dist/commerce/agent-setup.js +210 -0
  4. package/dist/commerce/alerts.d.ts +27 -0
  5. package/dist/commerce/alerts.js +71 -0
  6. package/dist/commerce/card.js +3 -3
  7. package/dist/commerce/cli.js +91 -119
  8. package/dist/commerce/config.d.ts +3 -2
  9. package/dist/commerce/config.js +19 -77
  10. package/dist/commerce/configure.d.ts +2 -0
  11. package/dist/commerce/configure.js +78 -0
  12. package/dist/commerce/discovery.d.ts +20 -0
  13. package/dist/commerce/discovery.js +162 -0
  14. package/dist/commerce/envar-balance.d.ts +25 -0
  15. package/dist/commerce/envar-balance.js +65 -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 +13 -31
  19. package/dist/commerce/envar.js +35 -83
  20. package/dist/commerce/external-agent.d.ts +26 -0
  21. package/dist/commerce/external-agent.js +129 -0
  22. package/dist/commerce/external-events.d.ts +76 -0
  23. package/dist/commerce/external-events.js +273 -0
  24. package/dist/commerce/external-mcp.d.ts +10 -0
  25. package/dist/commerce/external-mcp.js +199 -0
  26. package/dist/commerce/external-oauth.d.ts +12 -0
  27. package/dist/commerce/external-oauth.js +230 -0
  28. package/dist/commerce/external-runtime.d.ts +25 -0
  29. package/dist/commerce/external-runtime.js +244 -0
  30. package/dist/commerce/index.d.ts +1 -0
  31. package/dist/commerce/index.js +1 -0
  32. package/dist/commerce/managed.d.ts +3 -0
  33. package/dist/commerce/managed.js +143 -0
  34. package/dist/commerce/native-inventory.d.ts +9 -0
  35. package/dist/commerce/native-inventory.js +75 -0
  36. package/dist/commerce/native-skills.d.ts +32 -0
  37. package/dist/commerce/native-skills.js +174 -0
  38. package/dist/commerce/openclaw-setup.d.ts +28 -0
  39. package/dist/commerce/openclaw-setup.js +43 -0
  40. package/dist/commerce/operations.d.ts +8 -3
  41. package/dist/commerce/operations.js +21 -18
  42. package/dist/commerce/pricing.d.ts +0 -1
  43. package/dist/commerce/pricing.js +3 -5
  44. package/dist/commerce/runtime.d.ts +4 -2
  45. package/dist/commerce/runtime.js +4 -2
  46. package/dist/commerce/server.d.ts +3 -2
  47. package/dist/commerce/server.js +26 -10
  48. package/dist/commerce/setup.d.ts +25 -0
  49. package/dist/commerce/setup.js +379 -0
  50. package/dist/commerce/skill-gate.d.ts +54 -0
  51. package/dist/commerce/skill-gate.js +153 -0
  52. package/dist/commerce/store.d.ts +11 -1
  53. package/dist/commerce/store.js +36 -2
  54. package/dist/commerce/types.d.ts +6 -57
  55. package/examples/envar-integration-policy.json +1 -9
  56. package/examples/hermes-setup-probe.py +109 -0
  57. package/examples/hermes-skill-inventory.py +108 -0
  58. package/examples/native-skill-runner.py +197 -0
  59. package/examples/seller.json +143 -26
  60. package/package.json +5 -16
  61. package/schemas/envarpay.config.schema.json +28 -87
  62. package/dist/commerce/buyer-store.d.ts +0 -101
  63. package/dist/commerce/buyer-store.js +0 -168
  64. package/dist/commerce/buyer.d.ts +0 -132
  65. package/dist/commerce/buyer.js +0 -755
  66. package/dist/commerce/chain.d.ts +0 -24
  67. package/dist/commerce/chain.js +0 -146
  68. package/dist/commerce/client-runtime.d.ts +0 -8
  69. package/dist/commerce/client-runtime.js +0 -7
  70. package/dist/commerce/management.d.ts +0 -14
  71. package/dist/commerce/management.js +0 -138
  72. package/dist/commerce/mpp-client.d.ts +0 -59
  73. package/dist/commerce/mpp-client.js +0 -24
  74. package/dist/commerce/mpp-runtime.d.ts +0 -3
  75. package/dist/commerce/mpp-runtime.js +0 -3
  76. package/dist/commerce/mpp.d.ts +0 -53
  77. package/dist/commerce/mpp.js +0 -163
  78. package/dist/commerce/peer-proxy.d.ts +0 -44
  79. package/dist/commerce/peer-proxy.js +0 -673
  80. package/dist/commerce/stripe-provider.d.ts +0 -9
  81. package/dist/commerce/stripe-provider.js +0 -97
  82. package/dist/commerce/x402.d.ts +0 -37
  83. package/dist/commerce/x402.js +0 -174
  84. package/examples/buyer-policy.json +0 -18
  85. package/schemas/buyer-policy.schema.json +0 -253
@@ -0,0 +1,162 @@
1
+ import { readSkillInventory } from "./native-inventory.js";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { readFileSync, writeFileSync, existsSync, mkdtempSync, mkdirSync, renameSync, rmSync, readdirSync, lstatSync, } from "node:fs";
5
+ import { createHash } from 'node:crypto';
6
+ import { join } from "node:path";
7
+ import { createRequire } from "node:module";
8
+ import { prepareTextSkill } from "./setup.js";
9
+ import { readInstalledSkills } from "./native-skills.js";
10
+ const exec = promisify(execFile);
11
+ function packageDigest(root) {
12
+ const files = [];
13
+ let size = 0;
14
+ function walk(folder, prefix = '') {
15
+ if (prefix.split('/').length > 12)
16
+ throw Error('skill_size');
17
+ for (const name of readdirSync(folder).sort()) {
18
+ const path = join(folder, name), stat = lstatSync(path);
19
+ if (stat.isSymbolicLink())
20
+ throw Error('skill_symlink');
21
+ if (stat.isDirectory()) {
22
+ walk(path, prefix + name + '/');
23
+ continue;
24
+ }
25
+ if (!stat.isFile() || stat.size > 512 * 1024 || (size += stat.size) > 1024 * 1024 || files.length >= 128)
26
+ throw Error('skill_size');
27
+ files.push([prefix + name, createHash('sha256').update(readFileSync(path)).digest('hex')]);
28
+ }
29
+ }
30
+ walk(root);
31
+ files.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
32
+ const json = JSON.stringify(files).replace(/[\u007f-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
33
+ return createHash('sha256').update(json).digest('hex');
34
+ }
35
+ export class SkillDiscovery {
36
+ settings;
37
+ discovery;
38
+ origin;
39
+ enabled;
40
+ cached = [];
41
+ nextScan = 0;
42
+ state = "ready";
43
+ path;
44
+ constructor(settings, discovery, origin) {
45
+ this.settings = settings;
46
+ this.discovery = discovery;
47
+ this.origin = origin;
48
+ this.path = join(settings.skillsDirectory, ".source-digests.json");
49
+ this.enabled = existsSync(this.path)
50
+ ? JSON.parse(readFileSync(this.path, "utf8"))
51
+ : {};
52
+ }
53
+ async scan() {
54
+ if (this.discovery.roots && !this.settings.dockerContainer)
55
+ return readSkillInventory(this.discovery.roots);
56
+ const code = readFileSync(new URL("../../examples/hermes-skill-inventory.py", import.meta.url), "utf8");
57
+ const args = ["-c", code, this.discovery.hermesHome];
58
+ const { stdout } = this.settings.dockerContainer
59
+ ? await exec("docker", [
60
+ "exec",
61
+ this.settings.dockerContainer,
62
+ this.settings.python,
63
+ ...args,
64
+ ], { timeout: 30000, maxBuffer: 1024 * 1024 })
65
+ : await exec(this.settings.python, args, {
66
+ timeout: 30000,
67
+ maxBuffer: 1024 * 1024,
68
+ });
69
+ const list = JSON.parse(stdout);
70
+ if (!Array.isArray(list) ||
71
+ list.length > 128 ||
72
+ list.some((s) => !/^[a-z][a-z0-9-]{0,63}$/.test(s.name) ||
73
+ !/^[0-9a-f]{64}$/.test(s.digest) ||
74
+ typeof s.path !== "string"))
75
+ throw Error("inventory");
76
+ return list;
77
+ }
78
+ async sync(integration) {
79
+ if (Date.now() >= this.nextScan) {
80
+ this.nextScan = Date.now() + 15000;
81
+ try {
82
+ this.cached = await this.scan();
83
+ this.state = "ready";
84
+ }
85
+ catch {
86
+ this.state = this.settings.dockerContainer
87
+ ? "agent_unavailable"
88
+ : "scan_failed";
89
+ }
90
+ }
91
+ const active = new Set(readdirSync(this.settings.skillsDirectory, { withFileTypes: true })
92
+ .filter((x) => x.isDirectory() && !x.name.startsWith("."))
93
+ .map((x) => x.name));
94
+ for (const skill of this.cached)
95
+ if (active.has(skill.name) && !this.enabled[skill.name])
96
+ this.enabled[skill.name] = skill.digest;
97
+ if (this.state === "ready")
98
+ writeFileSync(this.path, JSON.stringify(this.enabled), { mode: 0o600 });
99
+ const skills = this.cached.map((s) => ({
100
+ name: s.name,
101
+ digest: s.digest,
102
+ description: s.description,
103
+ status: !s.supported
104
+ ? "requires_adapter"
105
+ : active.has(s.name)
106
+ ? this.enabled[s.name] && this.enabled[s.name] !== s.digest
107
+ ? "updated"
108
+ : "enabled"
109
+ : "available",
110
+ }));
111
+ for (const name of active)
112
+ if (!skills.some((s) => s.name === name))
113
+ skills.push({
114
+ name,
115
+ digest: this.enabled[name] ?? "0".repeat(64),
116
+ description: "",
117
+ status: "removed",
118
+ });
119
+ const result = await integration.reportRuntime({
120
+ version: createRequire(import.meta.url)("../../package.json").version,
121
+ card_url: this.origin + "/.well-known/agent-card.json",
122
+ state: this.state,
123
+ skills: skills.slice(0, 128),
124
+ });
125
+ for (const request of result) {
126
+ const skill = this.cached.find((s) => s.name === request.name && s.digest === request.digest && s.supported);
127
+ if (!skill || active.has(skill.name) || this.state !== "ready")
128
+ continue;
129
+ // Check source again after the owner's request. Never overwrite a live package.
130
+ const current = (await this.scan()).find((s) => s.name === skill.name && s.digest === skill.digest && s.supported);
131
+ if (!current)
132
+ continue;
133
+ const stage = mkdtempSync(join(this.settings.skillsDirectory, ".prepare-"));
134
+ try {
135
+ let source = skill.path;
136
+ if (this.settings.dockerContainer) {
137
+ source = join(stage, "source");
138
+ await exec("docker", ["cp", `${this.settings.dockerContainer}:${skill.path}`, source], { timeout: 30000, maxBuffer: 1024 * 1024 });
139
+ }
140
+ if (packageDigest(source) !== skill.digest)
141
+ continue;
142
+ const prepared = join(stage, "prepared");
143
+ mkdirSync(prepared);
144
+ prepareTextSkill(source, join(prepared, skill.name), skill.name);
145
+ readInstalledSkills(prepared, [], this.origin + "/.well-known/agent-card.json");
146
+ const after = (await this.scan()).find((s) => s.name === skill.name);
147
+ if (after?.digest !== skill.digest)
148
+ continue;
149
+ renameSync(join(prepared, skill.name), join(this.settings.skillsDirectory, skill.name));
150
+ this.enabled[skill.name] = skill.digest;
151
+ writeFileSync(this.path, JSON.stringify(this.enabled), { mode: 0o600 });
152
+ // This grant includes only the exact installed package explicitly selected in the owner UI.
153
+ integration.allowInstalledService(skill.name);
154
+ }
155
+ finally {
156
+ rmSync(stage, { recursive: true, force: true });
157
+ }
158
+ }
159
+ for (const name of active)
160
+ integration.allowInstalledService(name);
161
+ }
162
+ }
@@ -0,0 +1,25 @@
1
+ import { type PriceQuote } from './types.js';
2
+ import { CommerceStore, type OrderRecord } from './store.js';
3
+ import { CredentialVault } from './vault.js';
4
+ export interface BalanceClaimArgs {
5
+ platformOrderId: string;
6
+ claimToken: string;
7
+ taskId: string;
8
+ quote: PriceQuote;
9
+ }
10
+ export interface EnvarBalanceGateOptions {
11
+ store: CommerceStore;
12
+ vault: CredentialVault;
13
+ claim: (args: BalanceClaimArgs) => Promise<Record<string, unknown>>;
14
+ }
15
+ export declare class EnvarBalanceGate {
16
+ private readonly options;
17
+ constructor(options: EnvarBalanceGateOptions);
18
+ private assertReceipt;
19
+ private settle;
20
+ handle(request: Request, order: OrderRecord): Promise<{
21
+ response: Response;
22
+ } | {
23
+ headers: Record<string, string>;
24
+ }>;
25
+ }
@@ -0,0 +1,65 @@
1
+ /** Seller gate for one captured Envar account-balance claim. */
2
+ import { digest } from './config.js';
3
+ import { CommerceError } from './types.js';
4
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5
+ export class EnvarBalanceGate {
6
+ options;
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ assertReceipt(order, platformOrderId, receipt) {
11
+ const quote = order.quote, profile = quote.paymentProfile;
12
+ if (profile?.adapter !== 'envar_balance')
13
+ throw new CommerceError('balance_profile', 'Original quote is not an Envar balance offer');
14
+ if (receipt.state !== 'captured' || receipt.order_id !== platformOrderId ||
15
+ receipt.amount !== quote.amount || receipt.currency !== 'usd' || receipt.recipient !== profile?.accountRef ||
16
+ receipt.quote_id !== quote.quoteId || receipt.message_id !== quote.messageId ||
17
+ receipt.task_id !== order.taskId ||
18
+ receipt.service_id !== quote.serviceId || receipt.service_revision !== quote.serviceRevision ||
19
+ receipt.offer_id !== quote.offerId || receipt.input_digest !== quote.inputDigest ||
20
+ receipt.terms_digest !== quote.termsDigest ||
21
+ typeof receipt.economic_id !== 'string' || !/^ledger:[0-9a-f-]{36}$/i.test(receipt.economic_id))
22
+ throw new CommerceError('balance_receipt_mismatch', 'Platform capture differs from the frozen service order');
23
+ }
24
+ async settle(order, attemptId, saved) {
25
+ if (saved.quoteId !== order.quote.quoteId)
26
+ throw new CommerceError('balance_recovery_conflict', 'Original balance claim belongs to another quote');
27
+ let receipt;
28
+ try {
29
+ receipt = await this.options.claim({ platformOrderId: saved.platformOrderId, claimToken: saved.claimToken, taskId: order.taskId, quote: order.quote });
30
+ this.assertReceipt(order, saved.platformOrderId, receipt);
31
+ }
32
+ catch {
33
+ this.options.store.recordSettlement(attemptId, 'unknown', {});
34
+ return { response: Response.json({ error: 'balance_claim_unknown', taskId: order.taskId }, { status: 503, headers: { 'Cache-Control': 'no-store' } }) };
35
+ }
36
+ this.options.store.recordSettlement(attemptId, 'confirmed', receipt);
37
+ return { headers: { 'ENVAR-BALANCE-RECEIPT': String(receipt.economic_id) } };
38
+ }
39
+ async handle(request, order) {
40
+ const quote = order.quote, profile = quote.paymentProfile;
41
+ if (profile?.adapter !== 'envar_balance' || quote.currency !== 'usd' || quote.recipient !== profile.accountRef)
42
+ throw new CommerceError('balance_profile', 'This offer does not accept Envar account balance');
43
+ const prior = this.options.store.paymentForOrder(order.id);
44
+ if (order.paymentState === 'confirmed' && prior?.receipt)
45
+ return { headers: { 'ENVAR-BALANCE-RECEIPT': String(prior.receipt.economic_id) } };
46
+ if (['settling', 'unknown'].includes(order.paymentState)) {
47
+ if (!prior)
48
+ throw new CommerceError('balance_recovery_conflict', 'Original balance attempt is missing');
49
+ const saved = this.options.vault.get(prior.credentialRef);
50
+ return this.settle(order, prior.id, saved);
51
+ }
52
+ if (Date.now() >= Date.parse(quote.expiresAt))
53
+ return { response: Response.json({ error: 'quote_expired' }, { status: 409, headers: { 'Cache-Control': 'no-store' } }) };
54
+ const platformOrderId = request.headers.get('Envar-Balance-Order') ?? '';
55
+ const claimToken = request.headers.get('Envar-Balance-Claim') ?? '';
56
+ if (!platformOrderId && !claimToken)
57
+ return { response: Response.json({ error: 'envar_balance_required', taskId: order.taskId, quote: { quoteId: quote.quoteId, messageId: quote.messageId, serviceId: quote.serviceId, serviceRevision: quote.serviceRevision, offerId: quote.offerId, inputDigest: quote.inputDigest, termsDigest: quote.termsDigest, amount: quote.amount, currency: quote.currency, recipient: quote.recipient, expiresAt: quote.expiresAt } }, { status: 402, headers: { 'Cache-Control': 'no-store' } }) };
58
+ if (!UUID.test(platformOrderId) || claimToken.length < 32 || claimToken.length > 4096 || /\s/.test(claimToken))
59
+ throw new CommerceError('balance_claim', 'Use the original bounded platform balance claim');
60
+ const credentialRef = this.options.vault.put({ platformOrderId, claimToken, quoteId: quote.quoteId });
61
+ const economicKey = digest({ adapter: 'envar_balance', platformOrderId });
62
+ const attempt = this.options.store.reservePayment(order.id, order.caller, economicKey, credentialRef);
63
+ return this.settle(order, attempt, { platformOrderId, claimToken, quoteId: quote.quoteId });
64
+ }
65
+ }
@@ -8,10 +8,11 @@ export declare class PinnedUpstreams {
8
8
  private readonly inputEncoding;
9
9
  private readonly fetchImpl;
10
10
  private readonly signal?;
11
+ private readonly skills?;
11
12
  private bindings;
12
13
  private readonly executors;
13
14
  readonly execute: ExecuteOrder;
14
- constructor(path: string, credentials: Readonly<Record<string, string>>, inputEncoding?: Readonly<Record<string, "data" | "json-text">>, fetchImpl?: typeof fetch, signal?: AbortSignal | undefined);
15
+ constructor(path: string, credentials: Readonly<Record<string, string>>, inputEncoding?: Readonly<Record<string, "data" | "json-text">>, fetchImpl?: typeof fetch, signal?: AbortSignal | undefined, skills?: ExecuteOrder | undefined);
15
16
  prepare(config: CommerceConfig, history?: readonly Service[]): void;
16
17
  private executor;
17
18
  }
@@ -30,6 +31,7 @@ export interface EnvarSellerRuntimeOptions {
30
31
  /** Local payment adapters/merchant credentials must already exist; config sync cannot create them. */
31
32
  validateConfig?: (config: CommerceConfig) => void;
32
33
  onError?: (code: string) => void;
34
+ beforeSync?: () => Promise<void>;
33
35
  }
34
36
  export declare class EnvarSellerRuntime {
35
37
  private readonly options;
@@ -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
  }
@@ -9,13 +9,8 @@ export interface EnvarPolicy {
9
9
  acceptUpdates: boolean;
10
10
  allowedServices: string[];
11
11
  allowedUpstreamOrigins: string[];
12
- allowedX402: {
13
- network: string;
14
- asset: string;
15
- payTo: string;
16
- facilitatorOrigin: string;
17
- }[];
18
- allowedMppAccounts: string[];
12
+ /** Explicit seller account references allowed to accept platform balance. */
13
+ allowedBalanceAccounts: string[];
19
14
  }
20
15
  export interface EnvarCandidate {
21
16
  id: string;
@@ -47,20 +42,6 @@ export interface EnvarIntegrationOptions {
47
42
  responseLimitBytes?: number;
48
43
  now?: () => number;
49
44
  }
50
- export interface EnvarX402Evidence {
51
- network: string;
52
- asset: string;
53
- payer: string;
54
- recipient: string;
55
- amount: string;
56
- nonce: string;
57
- transaction: string;
58
- }
59
- export interface EnvarMppEvidence {
60
- reference: string;
61
- amount: string;
62
- currency: string;
63
- }
64
45
  /** Same frozen fingerprint as CommerceStore.registerCatalog; callers must read actual persisted state. */
65
46
  export declare function envarServiceProofs(config: CommerceConfig): EnvarApplicationProof["services"];
66
47
  export declare class EnvarIntegration {
@@ -81,6 +62,17 @@ export declare class EnvarIntegration {
81
62
  close(): void;
82
63
  private open;
83
64
  private request;
65
+ claimBalancePayment(args: {
66
+ platformOrderId: string;
67
+ claimToken: string;
68
+ taskId: string;
69
+ quote: PriceQuote;
70
+ }): Promise<Record<string, unknown>>;
71
+ allowInstalledService(name: string): void;
72
+ reportRuntime(value: unknown): Promise<{
73
+ name: string;
74
+ digest: string;
75
+ }[]>;
84
76
  private candidate;
85
77
  /** Latest locally acknowledged revisions; startup recovery never requires platform connectivity. */
86
78
  appliedCandidates(): EnvarCandidate[];
@@ -97,16 +89,6 @@ export declare class EnvarIntegration {
97
89
  orderId: string;
98
90
  kind: EnvarStatusKind;
99
91
  }): string;
100
- enqueueX402Payment(dedupeKey: string, report: {
101
- orderId: string;
102
- quote: PriceQuote;
103
- payment: EnvarX402Evidence;
104
- }): string;
105
- enqueueMppPayment(dedupeKey: string, report: {
106
- orderId: string;
107
- quote: PriceQuote;
108
- payment: EnvarMppEvidence;
109
- }): string;
110
92
  queueStatus(): {
111
93
  pending: number;
112
94
  sent: number;
@@ -3,9 +3,9 @@ import { randomUUID } from "node:crypto";
3
3
  import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { DatabaseSync } from "node:sqlite";
6
- import { atomic, currencyOf, digest, loadCommerceConfig } from "./config.js";
6
+ import { digest, loadCommerceConfig } from "./config.js";
7
7
  import { CommerceError, } from "./types.js";
8
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, HASH = /^0x[0-9a-f]{64}$/i, ADDRESS = /^0x[0-9a-f]{40}$/i, ID = /^[a-z][a-z0-9-]{0,63}$/;
8
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, ID = /^[a-z][a-z0-9-]{0,63}$/;
9
9
  const STATUS_KINDS = [
10
10
  "seller_started",
11
11
  "seller_completed",
@@ -122,20 +122,9 @@ export class EnvarIntegration {
122
122
  this.policy.allowedServices.some((id) => !ID.test(id)))
123
123
  fail("envar_policy", "Configure valid local Agent and service IDs");
124
124
  this.policy.allowedUpstreamOrigins = this.policy.allowedUpstreamOrigins.map((url) => origin(url, true));
125
- this.policy.allowedX402 = this.policy.allowedX402.map((entry) => {
126
- if (!/^eip155:[1-9][0-9]*$/.test(entry.network) ||
127
- !ADDRESS.test(entry.asset) ||
128
- !ADDRESS.test(entry.payTo))
129
- fail("envar_policy", "Configure explicit receiving identities");
130
- return {
131
- ...entry,
132
- asset: entry.asset.toLowerCase(),
133
- payTo: entry.payTo.toLowerCase(),
134
- facilitatorOrigin: origin(entry.facilitatorOrigin),
135
- };
136
- });
137
- if (this.policy.allowedMppAccounts.some((id) => !ID.test(id)))
138
- fail("envar_policy", "Configure explicit merchant account references");
125
+ this.policy.allowedBalanceAccounts ??= [];
126
+ if (this.policy.allowedBalanceAccounts.some((id) => !ID.test(id)))
127
+ fail("envar_policy", "Configure explicit balance account references");
139
128
  this.timeoutMs = options.timeoutMs ?? 10000;
140
129
  this.limitBytes = options.responseLimitBytes ?? 1048576;
141
130
  if (!Number.isInteger(this.timeoutMs) ||
@@ -214,11 +203,12 @@ export class EnvarIntegration {
214
203
  }
215
204
  async request(path, payload, eventId) {
216
205
  this.open();
217
- if (![
206
+ if (!([
218
207
  "/api/v1/agent-config/candidates",
219
208
  "/api/v1/agent-config/acknowledgments",
220
209
  "/api/v1/commerce-reports",
221
- ].includes(path))
210
+ "/api/v1/agent-config/runtime",
211
+ ].includes(path) || /^\/api\/v1\/commerce\/payment-claims\/[0-9a-f-]{36}\/claim$/i.test(path)))
222
212
  fail("envar_path", "Unsupported platform integration operation");
223
213
  const token = await this.options.token();
224
214
  if (typeof token !== "string" ||
@@ -297,6 +287,31 @@ export class EnvarIntegration {
297
287
  fail("envar_response_json", "Platform response must be an object");
298
288
  return value;
299
289
  }
290
+ async claimBalancePayment(args) {
291
+ uuid(args.platformOrderId);
292
+ if (typeof args.claimToken !== 'string' || args.claimToken.length < 32 || args.claimToken.length > 4096 || /\s/.test(args.claimToken))
293
+ fail('envar_balance_claim', 'Use the original bounded platform claim');
294
+ return this.request(`/api/v1/commerce/payment-claims/${args.platformOrderId}/claim`, {
295
+ claim_token: args.claimToken, task_id: args.taskId,
296
+ quote_id: args.quote.quoteId, message_id: args.quote.messageId,
297
+ service_id: args.quote.serviceId, service_revision: args.quote.serviceRevision,
298
+ offer_id: args.quote.offerId, input_digest: args.quote.inputDigest,
299
+ terms_digest: args.quote.termsDigest, amount: args.quote.amount,
300
+ currency: args.quote.currency, recipient: args.quote.recipient,
301
+ }, args.platformOrderId);
302
+ }
303
+ allowInstalledService(name) {
304
+ if (!ID.test(name))
305
+ fail('envar_policy', 'Invalid installed Skill name');
306
+ if (!this.policy.allowedServices.includes(name))
307
+ this.policy.allowedServices.push(name);
308
+ }
309
+ async reportRuntime(value) {
310
+ const response = await this.request('/api/v1/agent-config/runtime', value, randomUUID());
311
+ if (response.agent_id !== this.policy.agentId || !Array.isArray(response.enable) || response.enable.length > 128)
312
+ fail('envar_inventory', 'Invalid runtime acknowledgment');
313
+ return response.enable.filter((x) => x && ID.test(x.name) && /^[0-9a-f]{64}$/.test(x.digest));
314
+ }
300
315
  candidate(value) {
301
316
  if (!this.policy.acceptUpdates)
302
317
  fail("envar_updates_disabled", "Local owner has not enabled platform configuration updates");
@@ -328,15 +343,8 @@ export class EnvarIntegration {
328
343
  if (!this.policy.allowedUpstreamOrigins.includes(new URL(service.execution.cardUrl).origin))
329
344
  fail("envar_upstream_denied", "Upstream origin is outside the local update grant");
330
345
  for (const profile of Object.values(config.paymentProfiles)) {
331
- if (profile.adapter === "x402") {
332
- if (!this.policy.allowedX402.some((a) => a.network === profile.network &&
333
- a.asset === profile.asset.toLowerCase() &&
334
- a.payTo === profile.payTo.toLowerCase() &&
335
- a.facilitatorOrigin === new URL(profile.facilitatorUrl).origin))
336
- fail("envar_recipient_denied", "Receiving identity or facilitator is outside the local update grant");
337
- }
338
- else if (!this.policy.allowedMppAccounts.includes(profile.accountRef))
339
- fail("envar_merchant_denied", "Merchant account is outside the local update grant");
346
+ if (!this.policy.allowedBalanceAccounts.includes(profile.accountRef))
347
+ fail("envar_balance_denied", "Balance receiving account is outside the local update grant");
340
348
  }
341
349
  const prior = this.db
342
350
  .prepare("SELECT * FROM revisions WHERE id=? OR (local_service=? AND revision=?)")
@@ -526,62 +534,6 @@ export class EnvarIntegration {
526
534
  payload: {},
527
535
  });
528
536
  }
529
- enqueueX402Payment(dedupeKey, report) {
530
- uuid(report.orderId);
531
- const { quote, payment: p } = report, profile = quote.paymentProfile;
532
- if (!profile ||
533
- profile.adapter !== "x402" ||
534
- p.network !== profile.network ||
535
- p.asset.toLowerCase() !== profile.asset.toLowerCase() ||
536
- p.recipient.toLowerCase() !== profile.payTo.toLowerCase() ||
537
- p.amount !== quote.amount ||
538
- quote.currency !== currencyOf(profile) ||
539
- quote.recipient?.toLowerCase() !== profile.payTo.toLowerCase() ||
540
- !ADDRESS.test(p.payer) ||
541
- !HASH.test(p.nonce) ||
542
- !HASH.test(p.transaction))
543
- fail("envar_payment_binding", "Report the original x402 transaction matching the frozen quote");
544
- atomic(p.amount);
545
- return this.enqueue(`report:${dedupeKey}`, "report", {
546
- order_id: report.orderId,
547
- kind: "payment_observed",
548
- payload: {
549
- protocol: "x402",
550
- payment: {
551
- network: p.network,
552
- asset: p.asset.toLowerCase(),
553
- payer: p.payer.toLowerCase(),
554
- recipient: p.recipient.toLowerCase(),
555
- amount: p.amount,
556
- nonce: p.nonce.toLowerCase(),
557
- transaction: p.transaction.toLowerCase(),
558
- },
559
- },
560
- });
561
- }
562
- enqueueMppPayment(dedupeKey, report) {
563
- uuid(report.orderId);
564
- const { quote, payment: p } = report;
565
- if (quote.paymentProfile?.adapter !== "mpp" ||
566
- p.amount !== quote.amount ||
567
- p.currency !== quote.currency ||
568
- p.currency !== "usd" ||
569
- !/^pi_[A-Za-z0-9]{1,240}$/.test(p.reference))
570
- fail("envar_payment_binding", "Report the original Stripe PaymentIntent matching the frozen quote");
571
- atomic(p.amount);
572
- return this.enqueue(`report:${dedupeKey}`, "report", {
573
- order_id: report.orderId,
574
- kind: "payment_observed",
575
- payload: {
576
- protocol: "mpp",
577
- payment: {
578
- reference: p.reference,
579
- amount: p.amount,
580
- currency: p.currency,
581
- },
582
- },
583
- });
584
- }
585
537
  queueStatus() {
586
538
  this.open();
587
539
  const count = (state) => Number(this.db
@@ -0,0 +1,26 @@
1
+ import { CommerceStore } from "./store.js";
2
+ import type { ExecuteOrder } from "./server.js";
3
+ export declare class ExternalAgentExecutor {
4
+ readonly store: CommerceStore;
5
+ readonly principal: string;
6
+ readonly services: readonly string[];
7
+ readonly execute: ExecuteOrder;
8
+ constructor(store: CommerceStore, principal: string, services: readonly string[]);
9
+ private context;
10
+ list(): {
11
+ taskId: string;
12
+ serviceId: string;
13
+ claimId: string;
14
+ state: "working" | "input_required" | "completed" | "failed" | "unknown" | "not_started" | "queued" | "dispatching" | "auth_required";
15
+ input: import("./types.js").Input;
16
+ paymentState: "not_required" | "quoted" | "settling" | "confirmed" | "rejected" | "unknown";
17
+ }[];
18
+ claim(taskId: string): Promise<{
19
+ taskId: string;
20
+ claimId: string;
21
+ input: import("./types.js").Input;
22
+ paymentState: "not_required" | "quoted" | "settling" | "confirmed" | "rejected" | "unknown";
23
+ state: "working" | "input_required" | "completed" | "failed" | "unknown" | "not_started" | "queued" | "dispatching" | "auth_required";
24
+ }>;
25
+ complete(taskId: string, claimId: string, text: string, failed?: boolean): Promise<unknown>;
26
+ }