@envarai/envarpay 0.2.0-alpha.17 → 0.2.0-alpha.19

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 CHANGED
@@ -13,6 +13,11 @@ not evidence that its npm/OCI artifact is published.
13
13
 
14
14
  For Hermes onboarding, see [guided Skill setup](../../docs/hermes-skill-setup.md). The `setup --file ...` command reads the owner's local model configuration and creates a private seller with an empty catalog, ready for the first service to be configured in Envar.
15
15
 
16
+ The experimental `external-setup` and `external-serve` commands connect an existing
17
+ external Agent through scoped MCP tasks/events and a free A2A entry. See
18
+ [external Agent acceptance](../../docs/external-agents.md) for OAuth pairing,
19
+ Skill discovery, restart and the required real execution evidence.
20
+
16
21
  Node.js 22.14+ (Node 24 recommended). SQLite is single-instance. Contracts can be
17
22
  read without loading the seller or buyer runtime; Bun validation covers the
18
23
  contracts entry, not the SQLite server.
@@ -14,7 +14,7 @@ export function createOfferCard(config, serviceId, offerId, origin) {
14
14
  if (!service || !offer)
15
15
  throw new CommerceError('offer_not_found', 'Service offer not found');
16
16
  const profile = offer.paymentProfile ? config.paymentProfiles[offer.paymentProfile] : null;
17
- const payment = profile?.adapter === 'x402' ? { adapter: 'x402', network: profile.network, asset: profile.asset, payTo: profile.payTo } : profile ? { adapter: 'mpp', method: 'stripe', intent: 'charge', currency: profile.currency } : null;
17
+ const payment = profile?.adapter === 'x402' ? { adapter: 'x402', network: profile.network, asset: profile.asset, payTo: profile.payTo } : profile?.adapter === 'envar_balance' ? { adapter: 'envar_balance', currency: 'usd', accountRef: profile.accountRef } : profile ? { adapter: 'mpp', method: 'stripe', intent: 'charge', currency: profile.currency } : null;
18
18
  return AgentCard.fromJSON({
19
19
  name: service.name,
20
20
  description: `Deliverables: ${service.contract.deliverables.join(', ')}`,
@@ -17,6 +17,7 @@ import { EnvarSellerRuntime, PinnedUpstreams, restoreEnvarConfig } from './envar
17
17
  import { listenCommerce } from './http.js';
18
18
  import { CredentialVault } from './vault.js';
19
19
  import { X402Gate } from './x402.js';
20
+ import { EnvarBalanceGate } from './envar-balance.js';
20
21
  import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
21
22
  import { CommerceError } from './types.js';
22
23
  import { createNativeSkillExecutor } from './native-skills.js';
@@ -26,6 +27,7 @@ import { setupAgent } from './setup.js';
26
27
  import { SkillDiscovery } from './discovery.js';
27
28
  import { upgradeDiscovery, configurePayments } from './configure.js';
28
29
  import { agentSetup } from './agent-setup.js';
30
+ import { setupExternalAgent, serveExternalAgent } from './external-runtime.js';
29
31
  const { values, positionals } = parseArgs({ allowPositionals: true, options: {
30
32
  config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
31
33
  version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
@@ -33,7 +35,7 @@ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
33
35
  after: { type: 'string' }, limit: { type: 'string' }, 'refund-review': { type: 'boolean' }, alerts: { type: 'boolean' }, 'fail-on-alert': { type: 'boolean' }, 'integration-state': { type: 'string' }, 'no-integration': { type: 'boolean' },
34
36
  framework: { type: 'string' }, file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' }, check: { type: 'boolean' },
35
37
  } });
36
- const usage = 'envarpay onboard --file invitation.json [--framework hermes|openclaw] [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
38
+ const usage = 'envarpay external-setup --directory DIR --origin HTTPS_ORIGIN | external-serve --config FILE --credentials FILE --state DB --origin HTTPS_ORIGIN\nenvarpay onboard --file invitation.json [--framework hermes|openclaw] [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
37
39
  const closers = [];
38
40
  function privateFile(path) {
39
41
  const stat = lstatSync(path);
@@ -93,6 +95,22 @@ async function run() {
93
95
  return;
94
96
  }
95
97
  const command = positionals[0];
98
+ if (command === 'external-setup') {
99
+ if (!values.directory || !values.origin)
100
+ throw new CommerceError('external_setup', 'Provide directory and public HTTPS origin');
101
+ console.log(JSON.stringify(setupExternalAgent(values.directory, values.origin)));
102
+ return;
103
+ }
104
+ if (command === 'external-serve') {
105
+ if (!values.config || !values.credentials || !values.state || !values.origin)
106
+ throw new CommerceError('external_serve', 'Provide config, credentials, state and origin');
107
+ const runtime = serveExternalAgent({ configFile: values.config, credentialsFile: values.credentials, stateFile: values.state, origin: values.origin, host: values.host ?? '127.0.0.1', port: port(4020) });
108
+ closers.push(() => runtime.close());
109
+ await once(runtime.http, 'listening');
110
+ signals();
111
+ console.log(JSON.stringify({ listening: port(4020), role: 'external-agent', paymentsEnabled: false }));
112
+ return;
113
+ }
96
114
  if (values.framework && !['hermes', 'openclaw'].includes(values.framework))
97
115
  throw new CommerceError('setup_runtime', 'Use hermes or openclaw');
98
116
  if (command === 'managed-serve') {
@@ -294,6 +312,11 @@ async function run() {
294
312
  gates.mpp = gate;
295
313
  stripeRecipientFor = ref => gate.merchantRecipient(ref);
296
314
  }
315
+ if (protocols.has('envar_balance')) {
316
+ if (!integration)
317
+ throw new CommerceError('balance_configuration', 'Envar balance requires an authenticated Envar seller integration');
318
+ gates.envar_balance = new EnvarBalanceGate({ store, vault: vault, claim: args => integration.claimBalancePayment(args) });
319
+ }
297
320
  const nativeSkills = secrets.skills ? createNativeSkillExecutor(secrets.skills, values.origin, config.agent.name) : undefined;
298
321
  const discovery = secrets.skills?.discovery?.enabledFromWeb ? new SkillDiscovery(secrets.skills, secrets.skills.discovery, values.origin) : undefined;
299
322
  if (nativeSkills)
@@ -308,6 +331,8 @@ async function run() {
308
331
  throw new CommerceError('x402_configuration', 'Configure the required network RPC locally');
309
332
  if (profile.adapter === 'mpp' && !stripeRecipientFor?.(profile.accountRef))
310
333
  throw new CommerceError('mpp_configuration', 'Configure and verify the merchant account locally');
334
+ if (profile.adapter === 'envar_balance' && !syncSettings?.policy.allowedBalanceAccounts?.includes(profile.accountRef))
335
+ throw new CommerceError('balance_configuration', 'Allow this balance receiving account locally');
311
336
  }
312
337
  };
313
338
  validateAdapters(config);
@@ -135,6 +135,8 @@ export function loadCommerceConfig(value) {
135
135
  throw new CommerceError('invalid_amount', 'Maximum total exceeds supported integer range');
136
136
  if (profile.adapter === 'mpp' && low < 50n)
137
137
  throw new CommerceError('card_minimum', 'USD card orders must be at least 50 cents');
138
+ if (profile.adapter === 'envar_balance' && high > 2n ** 63n - 1n)
139
+ throw new CommerceError('balance_amount', 'Envar balance prices must fit signed micro-USD');
138
140
  }
139
141
  }
140
142
  return config;
@@ -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
+ /** Opt-in seller gate for a captured Envar account-balance claim. No x402 or MPP impersonation. */
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
+ }
@@ -16,6 +16,8 @@ export interface EnvarPolicy {
16
16
  facilitatorOrigin: string;
17
17
  }[];
18
18
  allowedMppAccounts: string[];
19
+ /** Explicit seller account references allowed to accept platform balance. */
20
+ allowedBalanceAccounts?: string[];
19
21
  }
20
22
  export interface EnvarCandidate {
21
23
  id: string;
@@ -81,6 +83,12 @@ export declare class EnvarIntegration {
81
83
  close(): void;
82
84
  private open;
83
85
  private request;
86
+ claimBalancePayment(args: {
87
+ platformOrderId: string;
88
+ claimToken: string;
89
+ taskId: string;
90
+ quote: PriceQuote;
91
+ }): Promise<Record<string, unknown>>;
84
92
  allowInstalledService(name: string): void;
85
93
  reportRuntime(value: unknown): Promise<{
86
94
  name: string;
@@ -136,6 +136,9 @@ export class EnvarIntegration {
136
136
  });
137
137
  if (this.policy.allowedMppAccounts.some((id) => !ID.test(id)))
138
138
  fail("envar_policy", "Configure explicit merchant account references");
139
+ this.policy.allowedBalanceAccounts ??= [];
140
+ if (this.policy.allowedBalanceAccounts.some((id) => !ID.test(id)))
141
+ fail("envar_policy", "Configure explicit balance account references");
139
142
  this.timeoutMs = options.timeoutMs ?? 10000;
140
143
  this.limitBytes = options.responseLimitBytes ?? 1048576;
141
144
  if (!Number.isInteger(this.timeoutMs) ||
@@ -214,12 +217,12 @@ export class EnvarIntegration {
214
217
  }
215
218
  async request(path, payload, eventId) {
216
219
  this.open();
217
- if (![
220
+ if (!([
218
221
  "/api/v1/agent-config/candidates",
219
222
  "/api/v1/agent-config/acknowledgments",
220
223
  "/api/v1/commerce-reports",
221
224
  "/api/v1/agent-config/runtime",
222
- ].includes(path))
225
+ ].includes(path) || /^\/api\/v1\/commerce\/payment-claims\/[0-9a-f-]{36}\/claim$/i.test(path)))
223
226
  fail("envar_path", "Unsupported platform integration operation");
224
227
  const token = await this.options.token();
225
228
  if (typeof token !== "string" ||
@@ -298,6 +301,19 @@ export class EnvarIntegration {
298
301
  fail("envar_response_json", "Platform response must be an object");
299
302
  return value;
300
303
  }
304
+ async claimBalancePayment(args) {
305
+ uuid(args.platformOrderId);
306
+ if (typeof args.claimToken !== 'string' || args.claimToken.length < 32 || args.claimToken.length > 4096 || /\s/.test(args.claimToken))
307
+ fail('envar_balance_claim', 'Use the original bounded platform claim');
308
+ return this.request(`/api/v1/commerce/payment-claims/${args.platformOrderId}/claim`, {
309
+ claim_token: args.claimToken, task_id: args.taskId,
310
+ quote_id: args.quote.quoteId, message_id: args.quote.messageId,
311
+ service_id: args.quote.serviceId, service_revision: args.quote.serviceRevision,
312
+ offer_id: args.quote.offerId, input_digest: args.quote.inputDigest,
313
+ terms_digest: args.quote.termsDigest, amount: args.quote.amount,
314
+ currency: args.quote.currency, recipient: args.quote.recipient,
315
+ }, args.platformOrderId);
316
+ }
301
317
  allowInstalledService(name) {
302
318
  if (!ID.test(name))
303
319
  fail('envar_policy', 'Invalid installed Skill name');
@@ -348,8 +364,10 @@ export class EnvarIntegration {
348
364
  a.facilitatorOrigin === new URL(profile.facilitatorUrl).origin))
349
365
  fail("envar_recipient_denied", "Receiving identity or facilitator is outside the local update grant");
350
366
  }
351
- else if (!this.policy.allowedMppAccounts.includes(profile.accountRef))
367
+ else if (profile.adapter === "mpp" && !this.policy.allowedMppAccounts.includes(profile.accountRef))
352
368
  fail("envar_merchant_denied", "Merchant account is outside the local update grant");
369
+ else if (profile.adapter === "envar_balance" && !this.policy.allowedBalanceAccounts.includes(profile.accountRef))
370
+ fail("envar_balance_denied", "Balance receiving account is outside the local update grant");
353
371
  }
354
372
  const prior = this.db
355
373
  .prepare("SELECT * FROM revisions WHERE id=? OR (local_service=? AND revision=?)")
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,76 @@
1
+ import { CredentialVault } from "./vault.js";
2
+ import { ExternalAgentExecutor } from "./external-agent.js";
3
+ import type { LookupFunction } from "node:net";
4
+ export type CallbackLookup = (hostname: string) => Promise<readonly {
5
+ address: string;
6
+ family: number;
7
+ }[]>;
8
+ export declare const pinnedCallbackLookup: (address: {
9
+ address: string;
10
+ family: number;
11
+ }) => LookupFunction;
12
+ export declare function publicWebhook(url: string, body: string, headers: Record<string, string>, resolve?: CallbackLookup): Promise<{
13
+ status: number;
14
+ body: string;
15
+ }>;
16
+ export declare class ExternalAgentEvents {
17
+ private readonly agent;
18
+ private readonly vault;
19
+ private readonly webhook;
20
+ private readonly db;
21
+ private timer?;
22
+ private running;
23
+ constructor(path: string, agent: ExternalAgentExecutor, vault: CredentialVault, webhook?: typeof publicWebhook);
24
+ definition(): {
25
+ events: {
26
+ name: string;
27
+ description: string;
28
+ delivery: string[];
29
+ inputSchema: {
30
+ type: string;
31
+ properties: {
32
+ agent_id: {
33
+ type: string;
34
+ const: string;
35
+ };
36
+ };
37
+ required: string[];
38
+ additionalProperties: boolean;
39
+ };
40
+ payloadSchema: {
41
+ type: string;
42
+ properties: {
43
+ agent_id: {
44
+ type: string;
45
+ };
46
+ task_id: {
47
+ type: string;
48
+ };
49
+ service_id: {
50
+ type: string;
51
+ };
52
+ };
53
+ required: string[];
54
+ additionalProperties: boolean;
55
+ };
56
+ }[];
57
+ };
58
+ private identity;
59
+ private send;
60
+ subscribe(params: Record<string, any>, principal: string): Promise<{
61
+ id: string;
62
+ refreshBefore: string;
63
+ cursor: null;
64
+ truncated: boolean;
65
+ }>;
66
+ unsubscribe(params: Record<string, any>, principal: string): {};
67
+ start(): void;
68
+ tick(): Promise<void>;
69
+ inspect(): {
70
+ subscriptions: {
71
+ active: boolean;
72
+ }[];
73
+ deliveries: Record<string, import("node:sqlite").SQLOutputValue>[];
74
+ };
75
+ close(): Promise<void>;
76
+ }