@ouro.bot/cli 0.1.0-alpha.653 → 0.1.0-alpha.655
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/changelog.json +13 -0
- package/dist/a2a/card.js +56 -0
- package/dist/a2a/client.js +143 -0
- package/dist/a2a/config.js +50 -0
- package/dist/a2a/onboarding.js +111 -0
- package/dist/a2a/server.js +498 -0
- package/dist/a2a/task-store.js +69 -0
- package/dist/a2a/types.js +3 -0
- package/dist/commerce/store.js +755 -0
- package/dist/commerce/types.js +3 -0
- package/dist/heart/daemon/cli-exec.js +118 -3
- package/dist/heart/daemon/cli-help.js +29 -2
- package/dist/heart/daemon/cli-parse.js +88 -4
- package/dist/heart/daemon/daemon.js +2 -1
- package/dist/heart/daemon/process-manager.js +2 -1
- package/dist/heart/daemon/runtime-logging.js +1 -1
- package/dist/heart/daemon/sense-manager.js +71 -15
- package/dist/heart/identity.js +4 -1
- package/dist/heart/sense-truth.js +2 -0
- package/dist/heart/turn-context.js +6 -0
- package/dist/mind/friends/channel.js +10 -1
- package/dist/mind/friends/resolver.js +13 -2
- package/dist/mind/friends/store-file.js +13 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/prompt.js +11 -0
- package/dist/repertoire/guardrails.js +25 -2
- package/dist/repertoire/tools-a2a.js +283 -0
- package/dist/repertoire/tools-base.js +4 -0
- package/dist/repertoire/tools-commerce.js +253 -0
- package/dist/repertoire/tools-flight.js +68 -5
- package/dist/repertoire/tools-stripe.js +49 -7
- package/dist/repertoire/tools.js +50 -2
- package/dist/senses/a2a-entry.js +78 -0
- package/dist/senses/pipeline.js +13 -0
- package/dist/senses/shared-turn.js +30 -5
- package/package.json +1 -1
- package/skills/agent-commerce.md +17 -10
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commerceToolDefinitions = void 0;
|
|
4
|
+
const identity_1 = require("../heart/identity");
|
|
5
|
+
const runtime_1 = require("../nerves/runtime");
|
|
6
|
+
const store_1 = require("../commerce/store");
|
|
7
|
+
function requireFamilyContext(ctx) {
|
|
8
|
+
if (!ctx?.context?.friend?.id)
|
|
9
|
+
return "no friend context — cannot use commerce tools.";
|
|
10
|
+
if (ctx.context.friend.trustLevel !== "family")
|
|
11
|
+
return "commerce tools require family trust level.";
|
|
12
|
+
/* v8 ignore next -- no-agentRoot fallback depends on process argv; normal tool calls inject agentRoot @preserve */
|
|
13
|
+
if (ctx.agentRoot)
|
|
14
|
+
return { friendId: ctx.context.friend.id, agentRoot: ctx.agentRoot };
|
|
15
|
+
return { friendId: ctx.context.friend.id, agentRoot: (0, identity_1.getAgentRoot)() };
|
|
16
|
+
}
|
|
17
|
+
function parseItems(raw) {
|
|
18
|
+
if (!raw?.trim())
|
|
19
|
+
return undefined;
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (!Array.isArray(parsed))
|
|
22
|
+
throw new Error("items_json must be a JSON array");
|
|
23
|
+
return parsed.map((item) => {
|
|
24
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
25
|
+
throw new Error("each item must be an object");
|
|
26
|
+
const record = item;
|
|
27
|
+
if (typeof record.name !== "string" || !record.name.trim())
|
|
28
|
+
throw new Error("each item needs a name");
|
|
29
|
+
return {
|
|
30
|
+
name: record.name,
|
|
31
|
+
...(typeof record.quantity === "number" ? { quantity: record.quantity } : {}),
|
|
32
|
+
...(typeof record.amount === "number" ? { amount: record.amount } : {}),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function parseExpiryMinutes(raw) {
|
|
37
|
+
if (!raw?.trim())
|
|
38
|
+
return undefined;
|
|
39
|
+
const value = Number.parseInt(raw, 10);
|
|
40
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
41
|
+
throw new Error("expires_minutes must be a positive integer");
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function parseExactAmount(raw) {
|
|
45
|
+
const value = raw?.trim();
|
|
46
|
+
if (!value || !/^\d+(?:\.\d{1,2})?$/.test(value))
|
|
47
|
+
throw new Error("amount must be an exact decimal with at most two places");
|
|
48
|
+
return Number(value);
|
|
49
|
+
}
|
|
50
|
+
function parseToolName(raw) {
|
|
51
|
+
const toolName = raw?.trim();
|
|
52
|
+
if (!toolName)
|
|
53
|
+
throw new Error("tool_name is required");
|
|
54
|
+
return toolName;
|
|
55
|
+
}
|
|
56
|
+
function parseConstraints(raw) {
|
|
57
|
+
if (!raw?.trim())
|
|
58
|
+
return undefined;
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
61
|
+
throw new Error("constraints_json must be a JSON object");
|
|
62
|
+
return Object.fromEntries(Object.entries(parsed).map(([key, value]) => [key, String(value)]));
|
|
63
|
+
}
|
|
64
|
+
function validateToolConstraints(toolName, constraints) {
|
|
65
|
+
if (toolName !== "stripe_create_card")
|
|
66
|
+
return;
|
|
67
|
+
if (!constraints?.type)
|
|
68
|
+
throw new Error("constraints_json.type is required for stripe_create_card");
|
|
69
|
+
if (!constraints.merchant_categories)
|
|
70
|
+
throw new Error("constraints_json.merchant_categories is required for stripe_create_card");
|
|
71
|
+
}
|
|
72
|
+
exports.commerceToolDefinitions = [
|
|
73
|
+
{
|
|
74
|
+
tool: {
|
|
75
|
+
type: "function",
|
|
76
|
+
function: {
|
|
77
|
+
name: "commerce_checkout_preview",
|
|
78
|
+
description: "Create an AP2-compatible local checkout preview before any payment or booking action. Requires family trust.",
|
|
79
|
+
parameters: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
merchant: { type: "string", description: "Merchant, provider, or counterparty name." },
|
|
83
|
+
amount: { type: "string", description: "Exact total amount." },
|
|
84
|
+
currency: { type: "string", description: "Currency code, e.g. usd." },
|
|
85
|
+
tool_name: { type: "string", description: "Exact tool this authority may be used with, e.g. stripe_create_card, flight_hold, or flight_book." },
|
|
86
|
+
constraints_json: { type: "string", description: "JSON object of exact tool-argument constraints; required for stripe_create_card type and merchant_categories, and recommended for flight offer_id." },
|
|
87
|
+
reason: { type: "string", description: "Why this purchase/booking is being made." },
|
|
88
|
+
items_json: { type: "string", description: "Optional JSON array of {name, quantity, amount} items." },
|
|
89
|
+
expires_minutes: { type: "string", description: "Optional expiry window in minutes; defaults to 30." },
|
|
90
|
+
},
|
|
91
|
+
required: ["merchant", "amount", "currency", "tool_name", "reason"],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
handler: async (args, ctx) => {
|
|
96
|
+
(0, runtime_1.emitNervesEvent)({
|
|
97
|
+
component: "repertoire",
|
|
98
|
+
event: "repertoire.tool_commerce_checkout_preview",
|
|
99
|
+
message: "commerce_checkout_preview invoked",
|
|
100
|
+
meta: { tool: "commerce_checkout_preview", merchant: args.merchant },
|
|
101
|
+
});
|
|
102
|
+
const guard = requireFamilyContext(ctx);
|
|
103
|
+
if (typeof guard === "string")
|
|
104
|
+
return guard;
|
|
105
|
+
try {
|
|
106
|
+
const expiresInMinutes = parseExpiryMinutes(args.expires_minutes);
|
|
107
|
+
const toolName = parseToolName(args.tool_name);
|
|
108
|
+
const constraints = parseConstraints(args.constraints_json);
|
|
109
|
+
const items = parseItems(args.items_json);
|
|
110
|
+
validateToolConstraints(toolName, constraints);
|
|
111
|
+
const record = (0, store_1.createCommercePreview)({
|
|
112
|
+
agentRoot: guard.agentRoot,
|
|
113
|
+
friendId: guard.friendId,
|
|
114
|
+
merchant: args.merchant,
|
|
115
|
+
amount: parseExactAmount(args.amount),
|
|
116
|
+
currency: args.currency,
|
|
117
|
+
allowedTools: [toolName],
|
|
118
|
+
constraints,
|
|
119
|
+
reason: args.reason,
|
|
120
|
+
items,
|
|
121
|
+
...(expiresInMinutes ? { expiresInMinutes } : {}),
|
|
122
|
+
});
|
|
123
|
+
const confirmationMessage = (0, store_1.commerceConfirmationMessage)(record);
|
|
124
|
+
return JSON.stringify({
|
|
125
|
+
checkoutId: record.id,
|
|
126
|
+
merchant: record.merchant,
|
|
127
|
+
amount: record.amount,
|
|
128
|
+
currency: record.currency,
|
|
129
|
+
allowedTools: record.allowedTools,
|
|
130
|
+
constraints: record.constraints,
|
|
131
|
+
expiresAt: record.expiresAt,
|
|
132
|
+
digest: record.digest,
|
|
133
|
+
confirmationMessage,
|
|
134
|
+
next: `Ask the family user to send the confirmationMessage exactly in a new turn after reviewing merchant, amount, currency, tool, and constraints; then call commerce_checkout_commit from that same turn.`,
|
|
135
|
+
}, null, 2);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return `commerce preview error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error parser failures */ String(error)}`;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
summaryKeys: ["merchant", "amount", "currency"],
|
|
142
|
+
riskProfile: { mutates: "durable_state_write", risk: "high", reason: "creates a purchase mandate preview" },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
tool: {
|
|
146
|
+
type: "function",
|
|
147
|
+
function: {
|
|
148
|
+
name: "commerce_checkout_commit",
|
|
149
|
+
description: "Confirm an exact checkout preview for the next matching payment/booking tool call. Requires family trust and confirmation=CONFIRM_PURCHASE.",
|
|
150
|
+
parameters: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
checkout_id: { type: "string", description: "Checkout preview ID." },
|
|
154
|
+
digest: { type: "string", description: "Digest returned by commerce_checkout_preview." },
|
|
155
|
+
confirmation: { type: "string", description: "Must be CONFIRM_PURCHASE." },
|
|
156
|
+
},
|
|
157
|
+
required: ["checkout_id", "digest", "confirmation"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
handler: async (args, ctx) => {
|
|
162
|
+
(0, runtime_1.emitNervesEvent)({
|
|
163
|
+
component: "repertoire",
|
|
164
|
+
event: "repertoire.tool_commerce_checkout_commit",
|
|
165
|
+
message: "commerce_checkout_commit invoked",
|
|
166
|
+
meta: { tool: "commerce_checkout_commit", checkoutId: args.checkout_id },
|
|
167
|
+
});
|
|
168
|
+
const guard = requireFamilyContext(ctx);
|
|
169
|
+
if (typeof guard === "string")
|
|
170
|
+
return guard;
|
|
171
|
+
try {
|
|
172
|
+
const record = (0, store_1.confirmCommercePreview)({
|
|
173
|
+
agentRoot: guard.agentRoot,
|
|
174
|
+
checkoutId: args.checkout_id,
|
|
175
|
+
digest: args.digest,
|
|
176
|
+
confirmation: args.confirmation,
|
|
177
|
+
friendId: guard.friendId,
|
|
178
|
+
currentUserMessage: ctx?.currentUserMessage,
|
|
179
|
+
});
|
|
180
|
+
return JSON.stringify({
|
|
181
|
+
checkoutId: record.id,
|
|
182
|
+
status: record.status,
|
|
183
|
+
expiresAt: record.expiresAt,
|
|
184
|
+
use: "Call the approved payment or booking tool with the exact merchant/tool/amount/currency/constraint arguments; Ouro will consume the matching authority without exposing a bearer token.",
|
|
185
|
+
}, null, 2);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return `commerce commit error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive non-Error store failures */ String(error)}`;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
summaryKeys: ["checkout_id"],
|
|
192
|
+
riskProfile: { mutates: "durable_state_write", risk: "high", reason: "confirms a purchase mandate" },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
tool: {
|
|
196
|
+
type: "function",
|
|
197
|
+
function: {
|
|
198
|
+
name: "commerce_receipt_get",
|
|
199
|
+
description: "Read a local commerce checkout/mandate receipt. Requires family trust.",
|
|
200
|
+
parameters: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
checkout_id: { type: "string", description: "Checkout ID." },
|
|
204
|
+
},
|
|
205
|
+
required: ["checkout_id"],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
handler: async (args, ctx) => {
|
|
210
|
+
(0, runtime_1.emitNervesEvent)({
|
|
211
|
+
component: "repertoire",
|
|
212
|
+
event: "repertoire.tool_commerce_receipt_get",
|
|
213
|
+
message: "commerce_receipt_get invoked",
|
|
214
|
+
meta: { tool: "commerce_receipt_get", checkoutId: args.checkout_id },
|
|
215
|
+
});
|
|
216
|
+
const guard = requireFamilyContext(ctx);
|
|
217
|
+
if (typeof guard === "string")
|
|
218
|
+
return guard;
|
|
219
|
+
const record = (0, store_1.readCommerceRecord)(guard.agentRoot, args.checkout_id);
|
|
220
|
+
return record ? JSON.stringify(record, null, 2) : `commerce checkout not found: ${args.checkout_id}`;
|
|
221
|
+
},
|
|
222
|
+
summaryKeys: ["checkout_id"],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
tool: {
|
|
226
|
+
type: "function",
|
|
227
|
+
function: {
|
|
228
|
+
name: "commerce_access_log",
|
|
229
|
+
description: "Read the commerce authority access log. Requires family trust.",
|
|
230
|
+
parameters: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {
|
|
233
|
+
limit: { type: "string", description: "Number of recent entries to return; defaults to 20." },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
handler: async (args, ctx) => {
|
|
239
|
+
(0, runtime_1.emitNervesEvent)({
|
|
240
|
+
component: "repertoire",
|
|
241
|
+
event: "repertoire.tool_commerce_access_log",
|
|
242
|
+
message: "commerce_access_log invoked",
|
|
243
|
+
meta: { tool: "commerce_access_log" },
|
|
244
|
+
});
|
|
245
|
+
const guard = requireFamilyContext(ctx);
|
|
246
|
+
if (typeof guard === "string")
|
|
247
|
+
return guard;
|
|
248
|
+
const limit = args.limit ? Number.parseInt(args.limit, 10) : 20;
|
|
249
|
+
return JSON.stringify((0, store_1.readCommerceAccessLog)(guard.agentRoot, Number.isInteger(limit) ? limit : 20), null, 2);
|
|
250
|
+
},
|
|
251
|
+
summaryKeys: ["limit"],
|
|
252
|
+
},
|
|
253
|
+
];
|
|
@@ -6,6 +6,7 @@ const duffel_client_1 = require("./duffel-client");
|
|
|
6
6
|
const user_profile_1 = require("./user-profile");
|
|
7
7
|
const credential_access_1 = require("./credential-access");
|
|
8
8
|
const runtime_1 = require("../nerves/runtime");
|
|
9
|
+
const store_1 = require("../commerce/store");
|
|
9
10
|
// Lazy-initialized Duffel client singleton
|
|
10
11
|
let _duffelClient = null;
|
|
11
12
|
async function getDuffelClient() {
|
|
@@ -29,6 +30,43 @@ function requireFriendContext(ctx) {
|
|
|
29
30
|
}
|
|
30
31
|
return { friendId: ctx.context.friend.id };
|
|
31
32
|
}
|
|
33
|
+
function parseExactAmount(raw) {
|
|
34
|
+
const value = raw.trim();
|
|
35
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(value))
|
|
36
|
+
throw new Error("amount must be an exact decimal with at most two places");
|
|
37
|
+
return Number(value);
|
|
38
|
+
}
|
|
39
|
+
function consumeReservedCommerce(ctx, toolName) {
|
|
40
|
+
if (!ctx?.commerceAuthority || !ctx.agentRoot)
|
|
41
|
+
return null;
|
|
42
|
+
const result = (0, store_1.consumeReservedCommerceAuthority)({
|
|
43
|
+
agentRoot: ctx.agentRoot,
|
|
44
|
+
checkoutId: ctx.commerceAuthority.checkoutId,
|
|
45
|
+
reservationToken: ctx.commerceAuthority.reservationToken,
|
|
46
|
+
toolName,
|
|
47
|
+
friendId: ctx.context?.friend?.id,
|
|
48
|
+
});
|
|
49
|
+
return result.ok ? null : `commerce authority consume error: ${result.reason}`;
|
|
50
|
+
}
|
|
51
|
+
function markCommerceAttempted(ctx, toolName) {
|
|
52
|
+
if (!ctx?.commerceAuthority || !ctx.agentRoot)
|
|
53
|
+
return null;
|
|
54
|
+
const result = (0, store_1.markReservedCommerceAuthorityAttempted)({
|
|
55
|
+
agentRoot: ctx.agentRoot,
|
|
56
|
+
checkoutId: ctx.commerceAuthority.checkoutId,
|
|
57
|
+
reservationToken: ctx.commerceAuthority.reservationToken,
|
|
58
|
+
toolName,
|
|
59
|
+
friendId: ctx.context?.friend?.id,
|
|
60
|
+
});
|
|
61
|
+
return result.ok ? null : `commerce authority attempt error: ${result.reason}`;
|
|
62
|
+
}
|
|
63
|
+
function normalizeCurrency(raw) {
|
|
64
|
+
return raw.trim().toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
function orderMatchesApprovedTotal(input, amount, currency) {
|
|
67
|
+
const orderAmount = parseExactAmount(input.totalAmount);
|
|
68
|
+
return orderAmount === amount && normalizeCurrency(input.totalCurrency) === normalizeCurrency(currency);
|
|
69
|
+
}
|
|
32
70
|
exports.flightToolDefinitions = [
|
|
33
71
|
{
|
|
34
72
|
tool: {
|
|
@@ -89,13 +127,16 @@ exports.flightToolDefinitions = [
|
|
|
89
127
|
type: "function",
|
|
90
128
|
function: {
|
|
91
129
|
name: "flight_hold",
|
|
92
|
-
description: "Hold a flight offer for a short period before committing to book. Not all airlines support holds.",
|
|
130
|
+
description: "Hold a flight offer for a short period before committing to book. Not all airlines support holds. Requires a matching confirmed commerce checkout.",
|
|
93
131
|
parameters: {
|
|
94
132
|
type: "object",
|
|
95
133
|
properties: {
|
|
96
134
|
offer_id: { type: "string", description: "The Duffel offer ID to hold" },
|
|
135
|
+
amount: { type: "string", description: "Exact hold amount from the approved commerce preview." },
|
|
136
|
+
currency: { type: "string", description: "Currency code from the approved commerce preview, e.g. usd." },
|
|
137
|
+
commerce_authority: { type: "string", description: "Optional explicit authority token for external/manual flows; normally omit so Ouro consumes the matching confirmed checkout." },
|
|
97
138
|
},
|
|
98
|
-
required: ["offer_id"],
|
|
139
|
+
required: ["offer_id", "amount", "currency"],
|
|
99
140
|
},
|
|
100
141
|
},
|
|
101
142
|
},
|
|
@@ -111,9 +152,19 @@ exports.flightToolDefinitions = [
|
|
|
111
152
|
return guard;
|
|
112
153
|
// Hold functionality would call Duffel's offer hold API.
|
|
113
154
|
// For pre-build, we return a structured acknowledgment.
|
|
155
|
+
const amount = parseExactAmount(args.amount);
|
|
156
|
+
const attemptError = markCommerceAttempted(ctx, "flight_hold");
|
|
157
|
+
if (attemptError)
|
|
158
|
+
return attemptError;
|
|
159
|
+
const consumeError = consumeReservedCommerce(ctx, "flight_hold");
|
|
160
|
+
/* v8 ignore next -- hold pre-build has no provider callback between attempt and consume; this branch is race-defense for file/process interference @preserve */
|
|
161
|
+
if (consumeError)
|
|
162
|
+
return consumeError;
|
|
114
163
|
return JSON.stringify({
|
|
115
164
|
status: "hold_requested",
|
|
116
165
|
offerId: args.offer_id,
|
|
166
|
+
amount,
|
|
167
|
+
currency: args.currency,
|
|
117
168
|
message: "Hold requested. Confirm or cancel before the hold expires.",
|
|
118
169
|
});
|
|
119
170
|
},
|
|
@@ -125,13 +176,14 @@ exports.flightToolDefinitions = [
|
|
|
125
176
|
type: "function",
|
|
126
177
|
function: {
|
|
127
178
|
name: "flight_book",
|
|
128
|
-
description: "Book a flight. Pulls passenger name/DOB/passport from the user's profile. Creates a virtual card, books the flight, then deactivates the card. Requires family trust level.",
|
|
179
|
+
description: "Book a flight. Pulls passenger name/DOB/passport from the user's profile. Creates a virtual card, books the flight, then deactivates the card. Requires family trust level and a matching confirmed commerce checkout.",
|
|
129
180
|
parameters: {
|
|
130
181
|
type: "object",
|
|
131
182
|
properties: {
|
|
132
183
|
offer_id: { type: "string", description: "The Duffel offer ID to book" },
|
|
133
184
|
amount: { type: "string", description: "Expected total amount in dollars" },
|
|
134
185
|
currency: { type: "string", description: "Currency code (e.g. 'usd')" },
|
|
186
|
+
commerce_authority: { type: "string", description: "Optional explicit authority token for external/manual flows; normally omit so Ouro consumes the matching confirmed checkout." },
|
|
135
187
|
},
|
|
136
188
|
required: ["offer_id", "amount", "currency"],
|
|
137
189
|
},
|
|
@@ -148,6 +200,8 @@ exports.flightToolDefinitions = [
|
|
|
148
200
|
if (typeof guard === "string")
|
|
149
201
|
return guard;
|
|
150
202
|
try {
|
|
203
|
+
const amount = parseExactAmount(args.amount);
|
|
204
|
+
const currency = normalizeCurrency(args.currency);
|
|
151
205
|
const store = (0, credential_access_1.getCredentialStore)();
|
|
152
206
|
// Get passenger data from profile
|
|
153
207
|
const legalName = await (0, user_profile_1.getUserProfileField)(guard.friendId, "legalName", store);
|
|
@@ -157,6 +211,9 @@ exports.flightToolDefinitions = [
|
|
|
157
211
|
const dateOfBirth = await (0, user_profile_1.getUserProfileField)(guard.friendId, "dateOfBirth", store);
|
|
158
212
|
const passport = await (0, user_profile_1.getUserProfileField)(guard.friendId, "passport", store);
|
|
159
213
|
const client = await getDuffelClient();
|
|
214
|
+
const attemptError = markCommerceAttempted(ctx, "flight_book");
|
|
215
|
+
if (attemptError)
|
|
216
|
+
return attemptError;
|
|
160
217
|
const result = await client.createOrder({
|
|
161
218
|
offerId: args.offer_id,
|
|
162
219
|
passengers: [{
|
|
@@ -169,9 +226,15 @@ exports.flightToolDefinitions = [
|
|
|
169
226
|
passportCountry: passport?.country,
|
|
170
227
|
passportExpiry: passport?.expiry,
|
|
171
228
|
}],
|
|
172
|
-
amount
|
|
173
|
-
currency
|
|
229
|
+
amount,
|
|
230
|
+
currency,
|
|
174
231
|
});
|
|
232
|
+
if (!orderMatchesApprovedTotal(result, amount, currency)) {
|
|
233
|
+
return `booking error: completed order total ${result.totalAmount} ${result.totalCurrency} does not match approved ${args.amount} ${args.currency}`;
|
|
234
|
+
}
|
|
235
|
+
const consumeError = consumeReservedCommerce(ctx, "flight_book");
|
|
236
|
+
if (consumeError)
|
|
237
|
+
return consumeError;
|
|
175
238
|
return JSON.stringify(result, null, 2);
|
|
176
239
|
}
|
|
177
240
|
catch (err) {
|
|
@@ -4,6 +4,7 @@ exports.stripeToolDefinitions = void 0;
|
|
|
4
4
|
exports.resetStripeClient = resetStripeClient;
|
|
5
5
|
const stripe_client_1 = require("./stripe-client");
|
|
6
6
|
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
const store_1 = require("../commerce/store");
|
|
7
8
|
// Lazy-initialized Stripe client singleton
|
|
8
9
|
let _stripeClient = null;
|
|
9
10
|
async function getStripeClient() {
|
|
@@ -21,13 +22,43 @@ function requireFamilyContext(ctx) {
|
|
|
21
22
|
}
|
|
22
23
|
return { friendId: ctx.context.friend.id };
|
|
23
24
|
}
|
|
25
|
+
function parseExactSpendLimit(raw) {
|
|
26
|
+
const value = raw.trim();
|
|
27
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(value))
|
|
28
|
+
throw new Error("spend_limit must be an exact decimal with at most two places");
|
|
29
|
+
return Number(value);
|
|
30
|
+
}
|
|
31
|
+
function consumeReservedCommerce(ctx, toolName) {
|
|
32
|
+
if (!ctx?.commerceAuthority || !ctx.agentRoot)
|
|
33
|
+
return null;
|
|
34
|
+
const result = (0, store_1.consumeReservedCommerceAuthority)({
|
|
35
|
+
agentRoot: ctx.agentRoot,
|
|
36
|
+
checkoutId: ctx.commerceAuthority.checkoutId,
|
|
37
|
+
reservationToken: ctx.commerceAuthority.reservationToken,
|
|
38
|
+
toolName,
|
|
39
|
+
friendId: ctx.context?.friend?.id,
|
|
40
|
+
});
|
|
41
|
+
return result.ok ? null : `commerce authority consume error: ${result.reason}`;
|
|
42
|
+
}
|
|
43
|
+
function markCommerceAttempted(ctx, toolName) {
|
|
44
|
+
if (!ctx?.commerceAuthority || !ctx.agentRoot)
|
|
45
|
+
return null;
|
|
46
|
+
const result = (0, store_1.markReservedCommerceAuthorityAttempted)({
|
|
47
|
+
agentRoot: ctx.agentRoot,
|
|
48
|
+
checkoutId: ctx.commerceAuthority.checkoutId,
|
|
49
|
+
reservationToken: ctx.commerceAuthority.reservationToken,
|
|
50
|
+
toolName,
|
|
51
|
+
friendId: ctx.context?.friend?.id,
|
|
52
|
+
});
|
|
53
|
+
return result.ok ? null : `commerce authority attempt error: ${result.reason}`;
|
|
54
|
+
}
|
|
24
55
|
exports.stripeToolDefinitions = [
|
|
25
56
|
{
|
|
26
57
|
tool: {
|
|
27
58
|
type: "function",
|
|
28
59
|
function: {
|
|
29
60
|
name: "stripe_create_card",
|
|
30
|
-
description: "Create a virtual card for
|
|
61
|
+
description: "Create a virtual card for an approved transaction. Returns card ID and last 4 digits (never the full card number). Requires family trust level and a matching confirmed commerce checkout.",
|
|
31
62
|
parameters: {
|
|
32
63
|
type: "object",
|
|
33
64
|
properties: {
|
|
@@ -46,10 +77,14 @@ exports.stripeToolDefinitions = [
|
|
|
46
77
|
},
|
|
47
78
|
merchant_categories: {
|
|
48
79
|
type: "string",
|
|
49
|
-
description: "Comma-separated allowed merchant categories
|
|
80
|
+
description: "Comma-separated allowed merchant categories from the confirmed commerce constraints.",
|
|
81
|
+
},
|
|
82
|
+
commerce_authority: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "Optional explicit authority token for external/manual flows; normally omit so Ouro consumes the matching confirmed checkout without exposing a bearer token.",
|
|
50
85
|
},
|
|
51
86
|
},
|
|
52
|
-
required: ["type", "spend_limit", "currency"],
|
|
87
|
+
required: ["type", "spend_limit", "currency", "merchant_categories"],
|
|
53
88
|
},
|
|
54
89
|
},
|
|
55
90
|
},
|
|
@@ -64,16 +99,23 @@ exports.stripeToolDefinitions = [
|
|
|
64
99
|
if (typeof guard === "string")
|
|
65
100
|
return guard;
|
|
66
101
|
try {
|
|
102
|
+
const categories = args.merchant_categories.split(",").map((c) => c.trim()).filter(Boolean);
|
|
103
|
+
if (categories.length === 0)
|
|
104
|
+
throw new Error("merchant_categories is required");
|
|
105
|
+
const spendLimit = parseExactSpendLimit(args.spend_limit);
|
|
106
|
+
const attemptError = markCommerceAttempted(ctx, "stripe_create_card");
|
|
107
|
+
if (attemptError)
|
|
108
|
+
return attemptError;
|
|
67
109
|
const client = await getStripeClient();
|
|
68
|
-
const categories = args.merchant_categories
|
|
69
|
-
? args.merchant_categories.split(",").map((c) => c.trim())
|
|
70
|
-
: undefined;
|
|
71
110
|
const card = await client.createVirtualCard({
|
|
72
111
|
type: args.type,
|
|
73
|
-
spendLimit
|
|
112
|
+
spendLimit,
|
|
74
113
|
currency: args.currency,
|
|
75
114
|
merchantCategories: categories,
|
|
76
115
|
});
|
|
116
|
+
const consumeError = consumeReservedCommerce(ctx, "stripe_create_card");
|
|
117
|
+
if (consumeError)
|
|
118
|
+
return consumeError;
|
|
77
119
|
return JSON.stringify({
|
|
78
120
|
cardId: card.cardId,
|
|
79
121
|
last4: card.last4,
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -15,6 +15,7 @@ const tools_bundle_1 = require("./tools-bundle");
|
|
|
15
15
|
const runtime_1 = require("../nerves/runtime");
|
|
16
16
|
const guardrails_1 = require("./guardrails");
|
|
17
17
|
const identity_1 = require("../heart/identity");
|
|
18
|
+
const store_1 = require("../commerce/store");
|
|
18
19
|
const tools_surface_1 = require("./tools-surface");
|
|
19
20
|
const mcp_tools_1 = require("./mcp-tools");
|
|
20
21
|
const tools_voice_1 = require("./tools-voice");
|
|
@@ -40,6 +41,7 @@ var tools_surface_2 = require("./tools-surface");
|
|
|
40
41
|
Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: function () { return tools_surface_2.surfaceToolDef; } });
|
|
41
42
|
// All tool definitions in a single registry
|
|
42
43
|
const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, tools_surface_1.surfaceToolDefinition];
|
|
44
|
+
const COMMERCE_AUTHORITY_TOOLS = new Set(["stripe_create_card", "flight_hold", "flight_book"]);
|
|
43
45
|
// MCP tool definitions — populated each time getToolsForChannel() is called with an mcpManager.
|
|
44
46
|
// Kept separate from allDefinitions so execTool can find them.
|
|
45
47
|
let mcpDefinitions = [];
|
|
@@ -251,7 +253,8 @@ async function execTool(name, args, ctx) {
|
|
|
251
253
|
const guardContext = {
|
|
252
254
|
readPaths: tools_base_1.editFileReadTracker,
|
|
253
255
|
trustLevel: ctx?.context?.friend?.trustLevel,
|
|
254
|
-
agentRoot: safeGetAgentRoot(),
|
|
256
|
+
agentRoot: ctx?.agentRoot ?? safeGetAgentRoot(),
|
|
257
|
+
friendId: ctx?.context?.friend?.id,
|
|
255
258
|
...(mcpDef?.mcpServer ? { mcpServerName: mcpDef.mcpServer } : {}),
|
|
256
259
|
...(ctx?.context?.isGroupChat !== undefined ? { isGroupChat: (ctx?.context).isGroupChat } : {}),
|
|
257
260
|
};
|
|
@@ -267,8 +270,37 @@ async function execTool(name, args, ctx) {
|
|
|
267
270
|
});
|
|
268
271
|
return guardResult.reason;
|
|
269
272
|
}
|
|
273
|
+
const commerceReservation = COMMERCE_AUTHORITY_TOOLS.has(name) && guardContext.agentRoot
|
|
274
|
+
? (0, store_1.reserveCommerceAuthority)({
|
|
275
|
+
agentRoot: guardContext.agentRoot,
|
|
276
|
+
token: guardArgs.commerce_authority,
|
|
277
|
+
toolName: name,
|
|
278
|
+
args: guardArgs,
|
|
279
|
+
friendId: guardContext.friendId,
|
|
280
|
+
})
|
|
281
|
+
: null;
|
|
282
|
+
if (commerceReservation && !commerceReservation.ok) {
|
|
283
|
+
(0, runtime_1.emitNervesEvent)({
|
|
284
|
+
level: "warn",
|
|
285
|
+
event: "tool.guardrail_block",
|
|
286
|
+
component: "tools",
|
|
287
|
+
message: "guardrail blocked tool execution",
|
|
288
|
+
meta: { name, reason: commerceReservation.reason },
|
|
289
|
+
});
|
|
290
|
+
return `commerce authority required: ${commerceReservation.reason}`;
|
|
291
|
+
}
|
|
292
|
+
const toolContext = commerceReservation?.ok
|
|
293
|
+
? {
|
|
294
|
+
...ctx,
|
|
295
|
+
agentRoot: guardContext.agentRoot,
|
|
296
|
+
commerceAuthority: {
|
|
297
|
+
checkoutId: commerceReservation.checkoutId,
|
|
298
|
+
reservationToken: commerceReservation.reservationToken,
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
: ctx;
|
|
270
302
|
try {
|
|
271
|
-
const result = await def.handler(args,
|
|
303
|
+
const result = await def.handler(args, toolContext);
|
|
272
304
|
(0, runtime_1.emitNervesEvent)({
|
|
273
305
|
event: "tool.end",
|
|
274
306
|
component: "tools",
|
|
@@ -287,6 +319,22 @@ async function execTool(name, args, ctx) {
|
|
|
287
319
|
});
|
|
288
320
|
throw error;
|
|
289
321
|
}
|
|
322
|
+
finally {
|
|
323
|
+
if (commerceReservation?.ok && guardContext.agentRoot) {
|
|
324
|
+
try {
|
|
325
|
+
(0, store_1.releaseReservedCommerceAuthority)({
|
|
326
|
+
agentRoot: guardContext.agentRoot,
|
|
327
|
+
checkoutId: commerceReservation.checkoutId,
|
|
328
|
+
reservationToken: commerceReservation.reservationToken,
|
|
329
|
+
toolName: name,
|
|
330
|
+
friendId: guardContext.friendId,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
/* v8 ignore next -- external tool result/error should not be masked by best-effort reservation cleanup @preserve */
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
290
338
|
}
|
|
291
339
|
function summarizeKeyValues(args, keys, maxValueLength = 60) {
|
|
292
340
|
const parts = [];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const agentArgIndex = process.argv.indexOf("--agent");
|
|
37
|
+
const agentName = agentArgIndex >= 0 ? process.argv[agentArgIndex + 1] : undefined;
|
|
38
|
+
if (!agentName) {
|
|
39
|
+
// eslint-disable-next-line no-console -- pre-boot guard: --agent check before imports
|
|
40
|
+
console.error("Missing required --agent <name> argument.\nUsage: node dist/senses/a2a-entry.js --agent ouroboros [--port 18920] [--base-url https://agent.example]");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
function argValue(name) {
|
|
44
|
+
const index = process.argv.indexOf(name);
|
|
45
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
46
|
+
}
|
|
47
|
+
const runtime_logging_1 = require("../heart/daemon/runtime-logging");
|
|
48
|
+
const runtime_1 = require("../nerves/runtime");
|
|
49
|
+
(0, runtime_logging_1.configureDaemonRuntimeLogger)("a2a");
|
|
50
|
+
(0, runtime_1.emitNervesEvent)({
|
|
51
|
+
component: "senses",
|
|
52
|
+
event: "senses.entry_boot",
|
|
53
|
+
message: "booting A2A entrypoint",
|
|
54
|
+
meta: { entry: "a2a", agentName },
|
|
55
|
+
});
|
|
56
|
+
Promise.resolve().then(() => __importStar(require("../a2a/server"))).then(async ({ startA2AServer }) => {
|
|
57
|
+
const rawPort = argValue("--port");
|
|
58
|
+
const port = rawPort ? Number.parseInt(rawPort, 10) : undefined;
|
|
59
|
+
await startA2AServer({
|
|
60
|
+
agentName,
|
|
61
|
+
...(argValue("--host") ? { host: argValue("--host") } : {}),
|
|
62
|
+
...(Number.isInteger(port) ? { port } : {}),
|
|
63
|
+
...(argValue("--base-url") ? { baseUrl: argValue("--base-url") } : {}),
|
|
64
|
+
...(argValue("--path") ? { path: argValue("--path") } : {}),
|
|
65
|
+
});
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
(0, runtime_1.emitNervesEvent)({
|
|
69
|
+
level: "error",
|
|
70
|
+
component: "senses",
|
|
71
|
+
event: "senses.entry_error",
|
|
72
|
+
message: "A2A entrypoint failed",
|
|
73
|
+
meta: { entry: "a2a", agentName, error: error instanceof Error ? error.message : String(error) },
|
|
74
|
+
});
|
|
75
|
+
// eslint-disable-next-line no-console -- fatal startup guard for sense process
|
|
76
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|