@envarai/envarpay 0.2.0-alpha.9 → 2026.1025.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +5 -15
  5. package/dist/commerce/alerts.js +21 -77
  6. package/dist/commerce/card.js +1 -1
  7. package/dist/commerce/cli.js +74 -137
  8. package/dist/commerce/config.d.ts +1 -4
  9. package/dist/commerce/config.js +5 -81
  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 +1 -0
  17. package/dist/commerce/envar-runtime.js +7 -0
  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/managed.d.ts +3 -0
  31. package/dist/commerce/managed.js +143 -0
  32. package/dist/commerce/native-inventory.d.ts +9 -0
  33. package/dist/commerce/native-inventory.js +75 -0
  34. package/dist/commerce/native-skills.d.ts +8 -0
  35. package/dist/commerce/native-skills.js +11 -8
  36. package/dist/commerce/openclaw-setup.d.ts +28 -0
  37. package/dist/commerce/openclaw-setup.js +43 -0
  38. package/dist/commerce/operations.d.ts +1 -2
  39. package/dist/commerce/operations.js +17 -20
  40. package/dist/commerce/pricing.d.ts +0 -1
  41. package/dist/commerce/pricing.js +1 -3
  42. package/dist/commerce/runtime.d.ts +2 -2
  43. package/dist/commerce/runtime.js +2 -2
  44. package/dist/commerce/server.d.ts +0 -1
  45. package/dist/commerce/server.js +2 -3
  46. package/dist/commerce/setup.d.ts +25 -0
  47. package/dist/commerce/setup.js +379 -0
  48. package/dist/commerce/store.d.ts +11 -1
  49. package/dist/commerce/store.js +36 -2
  50. package/dist/commerce/types.d.ts +1 -56
  51. package/examples/envar-integration-policy.json +1 -9
  52. package/examples/seller.json +143 -26
  53. package/package.json +11 -20
  54. package/runtime/hermes-setup-probe.py +109 -0
  55. package/runtime/hermes-skill-inventory.py +108 -0
  56. package/schemas/envarpay.config.schema.json +7 -73
  57. package/dist/commerce/buyer-store.d.ts +0 -101
  58. package/dist/commerce/buyer-store.js +0 -168
  59. package/dist/commerce/buyer.d.ts +0 -138
  60. package/dist/commerce/buyer.js +0 -775
  61. package/dist/commerce/chain.d.ts +0 -24
  62. package/dist/commerce/chain.js +0 -146
  63. package/dist/commerce/client-runtime.d.ts +0 -8
  64. package/dist/commerce/client-runtime.js +0 -7
  65. package/dist/commerce/management.d.ts +0 -14
  66. package/dist/commerce/management.js +0 -140
  67. package/dist/commerce/mpp-client.d.ts +0 -70
  68. package/dist/commerce/mpp-client.js +0 -29
  69. package/dist/commerce/mpp-runtime.d.ts +0 -4
  70. package/dist/commerce/mpp-runtime.js +0 -4
  71. package/dist/commerce/mpp.d.ts +0 -53
  72. package/dist/commerce/mpp.js +0 -163
  73. package/dist/commerce/peer-proxy.d.ts +0 -44
  74. package/dist/commerce/peer-proxy.js +0 -673
  75. package/dist/commerce/stripe-issuer.d.ts +0 -41
  76. package/dist/commerce/stripe-issuer.js +0 -351
  77. package/dist/commerce/stripe-provider.d.ts +0 -11
  78. package/dist/commerce/stripe-provider.js +0 -132
  79. package/dist/commerce/x402.d.ts +0 -38
  80. package/dist/commerce/x402.js +0 -181
  81. package/dist/index.d.ts +0 -95
  82. package/dist/index.js +0 -163
  83. package/examples/buyer-policy.json +0 -18
  84. package/schemas/buyer-policy.schema.json +0 -253
  85. /package/{examples → runtime}/native-skill-runner.py +0 -0
@@ -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("../../runtime/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
+ }
@@ -31,6 +31,7 @@ export interface EnvarSellerRuntimeOptions {
31
31
  /** Local payment adapters/merchant credentials must already exist; config sync cannot create them. */
32
32
  validateConfig?: (config: CommerceConfig) => void;
33
33
  onError?: (code: string) => void;
34
+ beforeSync?: () => Promise<void>;
34
35
  }
35
36
  export declare class EnvarSellerRuntime {
36
37
  private readonly options;
@@ -231,6 +231,13 @@ export class EnvarSellerRuntime {
231
231
  return this.running;
232
232
  this.running = (async () => {
233
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
+ }
234
241
  for (const candidate of await this.options.integration.pullCandidates())
235
242
  await this.options.integration.applyCandidate(candidate, (c) => this.apply(c));
236
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: "unknown" | "not_started" | "queued" | "dispatching" | "working" | "input_required" | "auth_required" | "completed" | "failed";
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: "unknown" | "not_started" | "queued" | "dispatching" | "working" | "input_required" | "auth_required" | "completed" | "failed";
24
+ }>;
25
+ complete(taskId: string, claimId: string, text: string, failed?: boolean): Promise<unknown>;
26
+ }
@@ -0,0 +1,129 @@
1
+ /** User-operated external agents claim and complete the original, payment-gated commerce Task. */
2
+ import { randomUUID } from "node:crypto";
3
+ import { Task, TaskState } from "@a2a-js/sdk";
4
+ import { ServerCallContext } from "@a2a-js/sdk/server";
5
+ import { CommerceError } from "./types.js";
6
+ export class ExternalAgentExecutor {
7
+ store;
8
+ principal;
9
+ services;
10
+ execute;
11
+ constructor(store, principal, services) {
12
+ this.store = store;
13
+ this.principal = principal;
14
+ this.services = services;
15
+ if (!principal ||
16
+ !services.length ||
17
+ services.some((s) => !/^[a-z][a-z0-9-]{0,63}$/.test(s)))
18
+ throw new CommerceError("external_scope", "Configure one agent principal and explicit services");
19
+ const self = this;
20
+ this.execute = async function* (order, service) {
21
+ if (!self.services.includes(service.id) ||
22
+ service.execution.type !== "a2a")
23
+ throw new CommerceError("external_scope", "This external agent cannot execute that service");
24
+ self.store.prepareExternalTask(order.id, self.principal);
25
+ const original = await self.store.load(order.taskId, self.context(order));
26
+ if (!original)
27
+ throw new CommerceError("external_task", "Original commerce Task missing");
28
+ yield Task.fromJSON({
29
+ ...Task.toJSON(original),
30
+ status: {
31
+ state: "TASK_STATE_INPUT_REQUIRED",
32
+ message: {
33
+ messageId: randomUUID(),
34
+ role: "ROLE_AGENT",
35
+ parts: [
36
+ {
37
+ text: "Waiting for the connected external agent to claim this original Task.",
38
+ },
39
+ ],
40
+ },
41
+ },
42
+ metadata: { externalAgent: self.principal },
43
+ });
44
+ };
45
+ this.execute.recover = async (order) => {
46
+ const task = await self.store.load(order.taskId, self.context(order));
47
+ if (!task)
48
+ throw new CommerceError("external_task", "Original commerce Task missing");
49
+ return task;
50
+ };
51
+ }
52
+ context(order) {
53
+ return new ServerCallContext({
54
+ user: { isAuthenticated: true, userName: order.caller },
55
+ tenant: order.serviceRevision,
56
+ requestedVersion: "1.0",
57
+ });
58
+ }
59
+ list() {
60
+ return this.store.externalTasks(this.principal, this.services).map((v) => ({
61
+ taskId: v.order.taskId,
62
+ serviceId: v.order.serviceRevision.split(":")[0],
63
+ claimId: v.claimId,
64
+ state: v.order.executionState,
65
+ input: v.order.input,
66
+ paymentState: v.order.paymentState,
67
+ }));
68
+ }
69
+ async claim(taskId) {
70
+ const value = this.store.claimExternalTask(taskId, this.principal, this.services);
71
+ const order = value.order;
72
+ const original = await this.store.load(taskId, this.context(order));
73
+ if (!original)
74
+ throw new CommerceError("external_task", "Original Task missing");
75
+ if (![TaskState.TASK_STATE_COMPLETED, TaskState.TASK_STATE_FAILED].includes(original.status?.state ?? 0))
76
+ await this.store.save(Task.fromJSON({
77
+ ...Task.toJSON(original),
78
+ status: { state: "TASK_STATE_WORKING" },
79
+ metadata: { ...original.metadata, externalAgent: this.principal },
80
+ }), this.context(order));
81
+ return {
82
+ taskId,
83
+ claimId: value.claimId,
84
+ input: order.input,
85
+ paymentState: order.paymentState,
86
+ state: order.executionState,
87
+ };
88
+ }
89
+ async complete(taskId, claimId, text, failed = false) {
90
+ if (typeof text !== "string" ||
91
+ !text.trim() ||
92
+ Buffer.byteLength(text) > 32000)
93
+ throw new CommerceError("external_result", "Return one bounded nonempty result");
94
+ const order = this.store.externalClaim(taskId, this.principal, claimId, this.services);
95
+ const original = await this.store.load(taskId, this.context(order));
96
+ if (!original)
97
+ throw new CommerceError("external_task", "Original Task missing");
98
+ const state = failed
99
+ ? TaskState.TASK_STATE_FAILED
100
+ : TaskState.TASK_STATE_COMPLETED;
101
+ if ([TaskState.TASK_STATE_COMPLETED, TaskState.TASK_STATE_FAILED].includes(original.status?.state ?? 0)) {
102
+ const previous = original.artifacts
103
+ .flatMap((a) => a.parts)
104
+ .map((p) => (p.content?.$case === "text" ? p.content.value : ""))
105
+ .join("\n");
106
+ if (original.status?.state !== state || previous !== text)
107
+ throw new CommerceError("external_result_conflict", "A completed original result cannot be replaced");
108
+ return Task.toJSON(original);
109
+ }
110
+ const result = Task.fromJSON({
111
+ ...Task.toJSON(original),
112
+ status: { state: failed ? "TASK_STATE_FAILED" : "TASK_STATE_COMPLETED" },
113
+ artifacts: [
114
+ {
115
+ artifactId: "external-result",
116
+ name: "external-agent-result.txt",
117
+ parts: [{ text, mediaType: "text/plain" }],
118
+ },
119
+ ],
120
+ metadata: {
121
+ ...original.metadata,
122
+ externalAgent: this.principal,
123
+ claimId,
124
+ },
125
+ });
126
+ await this.store.save(result, this.context(order), original);
127
+ return Task.toJSON(result);
128
+ }
129
+ }