@ouro.bot/cli 0.1.0-alpha.652 → 0.1.0-alpha.654
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 +119 -4
- package/dist/heart/daemon/cli-help.js +14 -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,755 @@
|
|
|
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
|
+
exports.commerceConfirmationMessage = commerceConfirmationMessage;
|
|
37
|
+
exports.commerceAuthorityToken = commerceAuthorityToken;
|
|
38
|
+
exports.createCommercePreview = createCommercePreview;
|
|
39
|
+
exports.readCommerceRecord = readCommerceRecord;
|
|
40
|
+
exports.confirmCommercePreview = confirmCommercePreview;
|
|
41
|
+
exports.validateCommerceAuthorityToken = validateCommerceAuthorityToken;
|
|
42
|
+
exports.validateCommerceAuthority = validateCommerceAuthority;
|
|
43
|
+
exports.reserveCommerceAuthority = reserveCommerceAuthority;
|
|
44
|
+
exports.consumeReservedCommerceAuthority = consumeReservedCommerceAuthority;
|
|
45
|
+
exports.markReservedCommerceAuthorityAttempted = markReservedCommerceAuthorityAttempted;
|
|
46
|
+
exports.releaseReservedCommerceAuthority = releaseReservedCommerceAuthority;
|
|
47
|
+
exports.consumeCommerceAuthorityToken = consumeCommerceAuthorityToken;
|
|
48
|
+
exports.readCommerceAccessLog = readCommerceAccessLog;
|
|
49
|
+
exports.confirmationPhrase = confirmationPhrase;
|
|
50
|
+
const fs = __importStar(require("node:fs"));
|
|
51
|
+
const path = __importStar(require("node:path"));
|
|
52
|
+
const node_crypto_1 = require("node:crypto");
|
|
53
|
+
const runtime_1 = require("../nerves/runtime");
|
|
54
|
+
const DEFAULT_EXPIRES_MINUTES = 30;
|
|
55
|
+
const CONFIRMATION_PHRASE = "CONFIRM_PURCHASE";
|
|
56
|
+
function commerceRoot(agentRoot) {
|
|
57
|
+
return path.join(agentRoot, "state", "commerce");
|
|
58
|
+
}
|
|
59
|
+
function recordsDir(agentRoot) {
|
|
60
|
+
return path.join(commerceRoot(agentRoot), "checkouts");
|
|
61
|
+
}
|
|
62
|
+
function accessLogPath(agentRoot) {
|
|
63
|
+
return path.join(commerceRoot(agentRoot), "access-log.jsonl");
|
|
64
|
+
}
|
|
65
|
+
function recordPath(agentRoot, checkoutId) {
|
|
66
|
+
return path.join(recordsDir(agentRoot), `${checkoutId}.json`);
|
|
67
|
+
}
|
|
68
|
+
function recordLockPath(agentRoot, checkoutId) {
|
|
69
|
+
return `${recordPath(agentRoot, checkoutId)}.lock`;
|
|
70
|
+
}
|
|
71
|
+
function canonicalMandatePayload(input) {
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
friendId: input.friendId,
|
|
74
|
+
merchant: input.merchant.trim(),
|
|
75
|
+
items: input.items.map((item) => ({
|
|
76
|
+
name: item.name.trim(),
|
|
77
|
+
...(item.quantity !== undefined ? { quantity: item.quantity } : {}),
|
|
78
|
+
...(item.amount !== undefined ? { amount: item.amount } : {}),
|
|
79
|
+
})),
|
|
80
|
+
amount: input.amount,
|
|
81
|
+
currency: input.currency.trim().toLowerCase(),
|
|
82
|
+
allowedTools: [...input.allowedTools].sort(),
|
|
83
|
+
constraints: Object.fromEntries(Object.entries(input.constraints).sort(([a], [b]) => a.localeCompare(b))),
|
|
84
|
+
reason: input.reason.trim(),
|
|
85
|
+
expiresAt: input.expiresAt,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function digestFor(input) {
|
|
89
|
+
return (0, node_crypto_1.createHash)("sha256").update(canonicalMandatePayload(input)).digest("hex");
|
|
90
|
+
}
|
|
91
|
+
function digestForRecord(record) {
|
|
92
|
+
return digestFor({
|
|
93
|
+
friendId: record.friendId,
|
|
94
|
+
merchant: record.merchant,
|
|
95
|
+
items: record.items,
|
|
96
|
+
amount: record.amount,
|
|
97
|
+
currency: record.currency,
|
|
98
|
+
/* v8 ignore next -- legacy records missing allowedTools are rejected before validation digest checks; fallback keeps digesting total @preserve */
|
|
99
|
+
allowedTools: record.allowedTools ?? [],
|
|
100
|
+
/* v8 ignore next -- legacy records missing constraints are rejected before validation digest checks; fallback keeps digesting total @preserve */
|
|
101
|
+
constraints: record.constraints ?? {},
|
|
102
|
+
reason: record.reason,
|
|
103
|
+
expiresAt: record.expiresAt,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function consentSummary(record) {
|
|
107
|
+
const tools = [...(record.allowedTools ?? [])].sort().join(",");
|
|
108
|
+
const constraints = JSON.stringify(Object.fromEntries(Object.entries(record.constraints ?? {}).sort(([a], [b]) => a.localeCompare(b))));
|
|
109
|
+
return `${record.merchant} ${record.amount} ${record.currency} via ${tools} constraints ${constraints}`;
|
|
110
|
+
}
|
|
111
|
+
function expectedConfirmationMessage(record) {
|
|
112
|
+
return `${CONFIRMATION_PHRASE} checkout ${record.id} digest ${record.digest} for ${consentSummary(record)}`;
|
|
113
|
+
}
|
|
114
|
+
function commerceConfirmationMessage(record) {
|
|
115
|
+
(0, runtime_1.emitNervesEvent)({
|
|
116
|
+
component: "repertoire",
|
|
117
|
+
event: "repertoire.commerce_confirmation_message_built",
|
|
118
|
+
message: "built commerce confirmation message",
|
|
119
|
+
meta: { checkoutId: record.id },
|
|
120
|
+
});
|
|
121
|
+
return expectedConfirmationMessage(record);
|
|
122
|
+
}
|
|
123
|
+
function parseAmount(value) {
|
|
124
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
125
|
+
throw new Error("commerce amount must be a positive number");
|
|
126
|
+
}
|
|
127
|
+
return Math.round(value * 100) / 100;
|
|
128
|
+
}
|
|
129
|
+
function normalizeItems(items, merchant, amount) {
|
|
130
|
+
if (!items || items.length === 0)
|
|
131
|
+
return [{ name: merchant, quantity: 1, amount }];
|
|
132
|
+
return items.map((item) => ({
|
|
133
|
+
name: item.name.trim(),
|
|
134
|
+
...(item.quantity !== undefined ? { quantity: item.quantity } : {}),
|
|
135
|
+
...(item.amount !== undefined ? { amount: parseAmount(item.amount) } : {}),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function normalizeToolName(toolName) {
|
|
139
|
+
const normalized = toolName.trim();
|
|
140
|
+
if (!normalized)
|
|
141
|
+
throw new Error("commerce allowed tool is required");
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
function normalizeAllowedTools(allowedTools) {
|
|
145
|
+
const tools = (allowedTools ?? []).map(normalizeToolName);
|
|
146
|
+
if (tools.length === 0)
|
|
147
|
+
throw new Error("commerce preview must name at least one allowed tool");
|
|
148
|
+
return [...new Set(tools)].sort();
|
|
149
|
+
}
|
|
150
|
+
function normalizeConstraints(constraints) {
|
|
151
|
+
const normalized = {};
|
|
152
|
+
for (const [key, value] of Object.entries(constraints ?? {})) {
|
|
153
|
+
const cleanKey = key.trim();
|
|
154
|
+
const cleanValue = String(value).trim();
|
|
155
|
+
if (!cleanKey || !cleanValue)
|
|
156
|
+
continue;
|
|
157
|
+
normalized[cleanKey] = cleanValue;
|
|
158
|
+
}
|
|
159
|
+
return Object.fromEntries(Object.entries(normalized).sort(([a], [b]) => a.localeCompare(b)));
|
|
160
|
+
}
|
|
161
|
+
function appendAccessLog(agentRoot, entry) {
|
|
162
|
+
fs.mkdirSync(commerceRoot(agentRoot), { recursive: true });
|
|
163
|
+
fs.appendFileSync(accessLogPath(agentRoot), `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
function timestampMillis(value) {
|
|
166
|
+
const parsed = Date.parse(value);
|
|
167
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
168
|
+
}
|
|
169
|
+
function writeRecord(agentRoot, record) {
|
|
170
|
+
fs.mkdirSync(recordsDir(agentRoot), { recursive: true });
|
|
171
|
+
const { authorityToken: _authorityToken, ...persisted } = record;
|
|
172
|
+
fs.writeFileSync(recordPath(agentRoot, record.id), `${JSON.stringify(persisted, null, 2)}\n`, "utf-8");
|
|
173
|
+
}
|
|
174
|
+
function sleepSync(ms) {
|
|
175
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
176
|
+
}
|
|
177
|
+
function withRecordLock(agentRoot, checkoutId, fn) {
|
|
178
|
+
fs.mkdirSync(recordsDir(agentRoot), { recursive: true });
|
|
179
|
+
const lockPath = recordLockPath(agentRoot, checkoutId);
|
|
180
|
+
const deadline = Date.now() + 5_000;
|
|
181
|
+
let fd = null;
|
|
182
|
+
while (fd === null) {
|
|
183
|
+
try {
|
|
184
|
+
fd = fs.openSync(lockPath, "wx");
|
|
185
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error.code !== "EEXIST")
|
|
189
|
+
throw error;
|
|
190
|
+
try {
|
|
191
|
+
const ageMs = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
192
|
+
if (ageMs > 30_000) {
|
|
193
|
+
fs.unlinkSync(lockPath);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* v8 ignore next -- another process can remove a stale lock between failed open and stat; the next loop rechecks from scratch @preserve */
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (Date.now() >= deadline)
|
|
202
|
+
throw new Error("commerce_authority lock timed out");
|
|
203
|
+
sleepSync(25);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return fn();
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
fs.closeSync(fd);
|
|
211
|
+
try {
|
|
212
|
+
fs.unlinkSync(lockPath);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* v8 ignore next -- lock cleanup is best-effort after successful close; stale-lock reap handles rare removal races @preserve */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function tokenHash(token) {
|
|
220
|
+
return (0, node_crypto_1.createHash)("sha256").update(token).digest("hex");
|
|
221
|
+
}
|
|
222
|
+
function reservationToken() {
|
|
223
|
+
return (0, node_crypto_1.randomUUID)();
|
|
224
|
+
}
|
|
225
|
+
function createCommerceAuthorityToken(record) {
|
|
226
|
+
return `commerce:${record.id}:${record.digest}:${(0, node_crypto_1.randomUUID)()}`;
|
|
227
|
+
}
|
|
228
|
+
function commerceAuthorityToken(record) {
|
|
229
|
+
if (!record.authorityToken)
|
|
230
|
+
throw new Error("commerce authority token is only available after confirmation");
|
|
231
|
+
const token = record.authorityToken;
|
|
232
|
+
(0, runtime_1.emitNervesEvent)({
|
|
233
|
+
component: "repertoire",
|
|
234
|
+
event: "repertoire.commerce_authority_token_built",
|
|
235
|
+
message: "built commerce authority token",
|
|
236
|
+
meta: { checkoutId: record.id },
|
|
237
|
+
});
|
|
238
|
+
return token;
|
|
239
|
+
}
|
|
240
|
+
function createCommercePreview(input) {
|
|
241
|
+
const now = new Date();
|
|
242
|
+
const amount = parseAmount(input.amount);
|
|
243
|
+
const currency = input.currency.trim().toLowerCase();
|
|
244
|
+
if (!currency)
|
|
245
|
+
throw new Error("commerce currency is required");
|
|
246
|
+
const merchant = input.merchant.trim();
|
|
247
|
+
if (!merchant)
|
|
248
|
+
throw new Error("commerce merchant is required");
|
|
249
|
+
const reason = input.reason.trim();
|
|
250
|
+
if (!reason)
|
|
251
|
+
throw new Error("commerce reason is required");
|
|
252
|
+
const allowedTools = normalizeAllowedTools(input.allowedTools);
|
|
253
|
+
const constraints = normalizeConstraints(input.constraints);
|
|
254
|
+
const expiresAt = new Date(now.getTime() + (input.expiresInMinutes ?? DEFAULT_EXPIRES_MINUTES) * 60_000).toISOString();
|
|
255
|
+
const items = normalizeItems(input.items, merchant, amount);
|
|
256
|
+
const digest = digestFor({
|
|
257
|
+
friendId: input.friendId,
|
|
258
|
+
merchant,
|
|
259
|
+
items,
|
|
260
|
+
amount,
|
|
261
|
+
currency,
|
|
262
|
+
allowedTools,
|
|
263
|
+
constraints,
|
|
264
|
+
reason,
|
|
265
|
+
expiresAt,
|
|
266
|
+
});
|
|
267
|
+
const record = {
|
|
268
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
269
|
+
status: "previewed",
|
|
270
|
+
friendId: input.friendId,
|
|
271
|
+
merchant,
|
|
272
|
+
items,
|
|
273
|
+
amount,
|
|
274
|
+
currency,
|
|
275
|
+
allowedTools,
|
|
276
|
+
constraints,
|
|
277
|
+
reason,
|
|
278
|
+
digest,
|
|
279
|
+
createdAt: now.toISOString(),
|
|
280
|
+
updatedAt: now.toISOString(),
|
|
281
|
+
expiresAt,
|
|
282
|
+
};
|
|
283
|
+
writeRecord(input.agentRoot, record);
|
|
284
|
+
appendAccessLog(input.agentRoot, { checkoutId: record.id, action: "preview", friendId: input.friendId, ok: true });
|
|
285
|
+
(0, runtime_1.emitNervesEvent)({
|
|
286
|
+
component: "repertoire",
|
|
287
|
+
event: "repertoire.commerce_preview_created",
|
|
288
|
+
message: "created commerce checkout preview",
|
|
289
|
+
meta: { checkoutId: record.id, merchant, amount, currency },
|
|
290
|
+
});
|
|
291
|
+
return record;
|
|
292
|
+
}
|
|
293
|
+
function readCommerceRecord(agentRoot, checkoutId) {
|
|
294
|
+
try {
|
|
295
|
+
const raw = fs.readFileSync(recordPath(agentRoot, checkoutId), "utf-8");
|
|
296
|
+
const record = JSON.parse(raw);
|
|
297
|
+
appendAccessLog(agentRoot, { checkoutId, action: "read", ok: true });
|
|
298
|
+
(0, runtime_1.emitNervesEvent)({
|
|
299
|
+
component: "repertoire",
|
|
300
|
+
event: "repertoire.commerce_record_read",
|
|
301
|
+
message: "read commerce checkout record",
|
|
302
|
+
meta: { checkoutId },
|
|
303
|
+
});
|
|
304
|
+
return record;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
appendAccessLog(agentRoot, { checkoutId, action: "read", ok: false, reason: "not_found" });
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function confirmCommercePreview(input) {
|
|
312
|
+
const record = readCommerceRecord(input.agentRoot, input.checkoutId);
|
|
313
|
+
if (!record)
|
|
314
|
+
throw new Error(`commerce checkout not found: ${input.checkoutId}`);
|
|
315
|
+
if (record.friendId !== input.friendId)
|
|
316
|
+
throw new Error("commerce checkout belongs to a different friend");
|
|
317
|
+
if (record.status !== "previewed")
|
|
318
|
+
throw new Error(`commerce checkout is ${record.status}, not previewed`);
|
|
319
|
+
const expiresAtMs = timestampMillis(record.expiresAt);
|
|
320
|
+
if (expiresAtMs === null)
|
|
321
|
+
throw new Error("commerce checkout preview has invalid expiry");
|
|
322
|
+
if (expiresAtMs <= Date.now())
|
|
323
|
+
throw new Error("commerce checkout preview has expired");
|
|
324
|
+
if (record.digest !== input.digest)
|
|
325
|
+
throw new Error("commerce digest mismatch");
|
|
326
|
+
if (digestForRecord(record) !== record.digest)
|
|
327
|
+
throw new Error("commerce record digest mismatch");
|
|
328
|
+
if (input.confirmation.trim() !== CONFIRMATION_PHRASE)
|
|
329
|
+
throw new Error(`confirmation must be ${CONFIRMATION_PHRASE}`);
|
|
330
|
+
const currentUserMessage = input.currentUserMessage?.trim() ?? "";
|
|
331
|
+
if (currentUserMessage !== expectedConfirmationMessage(record)) {
|
|
332
|
+
throw new Error(`current human message must exactly equal: ${expectedConfirmationMessage(record)}`);
|
|
333
|
+
}
|
|
334
|
+
const now = new Date().toISOString();
|
|
335
|
+
const authorityToken = createCommerceAuthorityToken(record);
|
|
336
|
+
const confirmed = {
|
|
337
|
+
...record,
|
|
338
|
+
status: "confirmed",
|
|
339
|
+
confirmedAt: now,
|
|
340
|
+
updatedAt: now,
|
|
341
|
+
confirmation: CONFIRMATION_PHRASE,
|
|
342
|
+
confirmedByMessage: currentUserMessage,
|
|
343
|
+
authorityToken,
|
|
344
|
+
authorityTokenHash: tokenHash(authorityToken),
|
|
345
|
+
};
|
|
346
|
+
writeRecord(input.agentRoot, confirmed);
|
|
347
|
+
appendAccessLog(input.agentRoot, { checkoutId: record.id, action: "confirm", friendId: input.friendId, ok: true });
|
|
348
|
+
(0, runtime_1.emitNervesEvent)({
|
|
349
|
+
component: "repertoire",
|
|
350
|
+
event: "repertoire.commerce_preview_confirmed",
|
|
351
|
+
message: "confirmed commerce checkout preview",
|
|
352
|
+
meta: { checkoutId: record.id },
|
|
353
|
+
});
|
|
354
|
+
return confirmed;
|
|
355
|
+
}
|
|
356
|
+
function amountFromArgs(args) {
|
|
357
|
+
if (!args)
|
|
358
|
+
return null;
|
|
359
|
+
const raw = args.amount ?? args.spend_limit;
|
|
360
|
+
if (!raw)
|
|
361
|
+
return null;
|
|
362
|
+
if (!/^\d+(?:\.\d{1,2})?$/.test(raw.trim()))
|
|
363
|
+
return Number.NaN;
|
|
364
|
+
const parsed = Number(raw);
|
|
365
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
366
|
+
}
|
|
367
|
+
function currencyFromArgs(args) {
|
|
368
|
+
const raw = args?.currency;
|
|
369
|
+
return raw ? raw.trim().toLowerCase() : null;
|
|
370
|
+
}
|
|
371
|
+
function requiredAmountForTool(toolName) {
|
|
372
|
+
if (toolName === "stripe_create_card")
|
|
373
|
+
return "spend_limit";
|
|
374
|
+
if (toolName === "flight_hold" || toolName === "flight_book")
|
|
375
|
+
return "amount";
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
function requiredConstraintsForTool(toolName) {
|
|
379
|
+
if (toolName === "stripe_create_card")
|
|
380
|
+
return ["type", "merchant_categories"];
|
|
381
|
+
if (toolName === "flight_hold" || toolName === "flight_book")
|
|
382
|
+
return ["offer_id"];
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
function authorityRecordValidationReason(record, input, options = {}) {
|
|
386
|
+
const expiresAtMs = timestampMillis(record.expiresAt);
|
|
387
|
+
if (!Array.isArray(record.allowedTools) || record.allowedTools.length === 0)
|
|
388
|
+
return "commerce_authority is missing allowed tools";
|
|
389
|
+
if (!record.constraints || typeof record.constraints !== "object" || Array.isArray(record.constraints)) {
|
|
390
|
+
return "commerce_authority constraints are invalid";
|
|
391
|
+
}
|
|
392
|
+
if (expiresAtMs === null)
|
|
393
|
+
return "commerce_authority has invalid expiry";
|
|
394
|
+
if (record.digest !== digestForRecord(record))
|
|
395
|
+
return "commerce_authority record digest mismatch";
|
|
396
|
+
if (record.status !== "confirmed")
|
|
397
|
+
return `commerce checkout is ${record.status}, not confirmed`;
|
|
398
|
+
if (timestampMillis(record.confirmedAt ?? "") === null)
|
|
399
|
+
return "commerce_authority confirmation state is invalid";
|
|
400
|
+
if (record.confirmation !== CONFIRMATION_PHRASE)
|
|
401
|
+
return "commerce_authority confirmation state is invalid";
|
|
402
|
+
if (record.confirmedByMessage !== expectedConfirmationMessage(record))
|
|
403
|
+
return "commerce_authority confirmation state is invalid";
|
|
404
|
+
if (typeof record.authorityTokenHash !== "string" || !/^[a-f0-9]{64}$/.test(record.authorityTokenHash)) {
|
|
405
|
+
return "commerce_authority confirmation state is invalid";
|
|
406
|
+
}
|
|
407
|
+
if (options.token !== undefined && record.authorityTokenHash !== tokenHash(options.token))
|
|
408
|
+
return "commerce_authority token mismatch";
|
|
409
|
+
if (input.friendId && record.friendId !== input.friendId)
|
|
410
|
+
return "commerce_authority belongs to a different friend";
|
|
411
|
+
if (options.digest !== undefined && record.digest !== options.digest)
|
|
412
|
+
return "commerce_authority digest mismatch";
|
|
413
|
+
if (expiresAtMs <= Date.now())
|
|
414
|
+
return "commerce_authority expired";
|
|
415
|
+
if (!record.allowedTools.includes(input.toolName))
|
|
416
|
+
return "tool is not allowed by commerce_authority";
|
|
417
|
+
const requiredAmountKey = requiredAmountForTool(input.toolName);
|
|
418
|
+
const amount = amountFromArgs(input.args);
|
|
419
|
+
if (requiredAmountKey && amount === null)
|
|
420
|
+
return `tool ${requiredAmountKey} is required for commerce_authority validation`;
|
|
421
|
+
if (amount !== null && (!Number.isFinite(amount) || amount !== record.amount))
|
|
422
|
+
return "tool amount does not match commerce_authority amount";
|
|
423
|
+
const currency = currencyFromArgs(input.args);
|
|
424
|
+
if (requiredAmountKey && !currency)
|
|
425
|
+
return "tool currency is required for commerce_authority validation";
|
|
426
|
+
if (currency && currency !== record.currency)
|
|
427
|
+
return "tool currency does not match commerce_authority";
|
|
428
|
+
for (const key of requiredConstraintsForTool(input.toolName)) {
|
|
429
|
+
if (!record.constraints[key])
|
|
430
|
+
return `commerce_authority is missing required ${key} constraint`;
|
|
431
|
+
}
|
|
432
|
+
for (const [key, expected] of Object.entries(record.constraints)) {
|
|
433
|
+
const actual = input.args?.[key]?.trim();
|
|
434
|
+
if (actual !== expected)
|
|
435
|
+
return `tool ${key} does not match commerce_authority`;
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
function validateCommerceAuthorityToken(input) {
|
|
440
|
+
const token = input.token?.trim();
|
|
441
|
+
if (!token)
|
|
442
|
+
return { ok: false, reason: "missing commerce_authority token" };
|
|
443
|
+
const match = /^commerce:([^:]+):([a-f0-9]{64}):([0-9a-f-]{36})$/.exec(token);
|
|
444
|
+
if (!match)
|
|
445
|
+
return { ok: false, reason: "invalid commerce_authority token format" };
|
|
446
|
+
const checkoutId = match[1];
|
|
447
|
+
const digest = match[2];
|
|
448
|
+
const record = readCommerceRecord(input.agentRoot, checkoutId);
|
|
449
|
+
if (!record)
|
|
450
|
+
return { ok: false, reason: "commerce checkout not found" };
|
|
451
|
+
const reason = authorityRecordValidationReason(record, input, { token, digest });
|
|
452
|
+
const ok = !reason;
|
|
453
|
+
appendAccessLog(input.agentRoot, {
|
|
454
|
+
checkoutId,
|
|
455
|
+
action: "validate",
|
|
456
|
+
toolName: input.toolName,
|
|
457
|
+
ok,
|
|
458
|
+
...(reason ? { reason } : {}),
|
|
459
|
+
});
|
|
460
|
+
(0, runtime_1.emitNervesEvent)({
|
|
461
|
+
component: "repertoire",
|
|
462
|
+
event: "repertoire.commerce_authority_validated",
|
|
463
|
+
message: "validated commerce authority token",
|
|
464
|
+
meta: { checkoutId, toolName: input.toolName, ok, reason: reason ?? "" },
|
|
465
|
+
});
|
|
466
|
+
return ok ? { ok: true, record } : { ok: false, reason: reason };
|
|
467
|
+
}
|
|
468
|
+
function readAllCommerceRecords(agentRoot) {
|
|
469
|
+
try {
|
|
470
|
+
return fs.readdirSync(recordsDir(agentRoot))
|
|
471
|
+
.filter((file) => file.endsWith(".json"))
|
|
472
|
+
.flatMap((file) => {
|
|
473
|
+
try {
|
|
474
|
+
return [JSON.parse(fs.readFileSync(path.join(recordsDir(agentRoot), file), "utf-8"))];
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function matchingCommerceRecords(input) {
|
|
486
|
+
return readAllCommerceRecords(input.agentRoot)
|
|
487
|
+
.filter((record) => authorityRecordValidationReason(record, input) === null);
|
|
488
|
+
}
|
|
489
|
+
function noMatchingAuthorityReason(records, input) {
|
|
490
|
+
const reasons = records
|
|
491
|
+
.map((record) => authorityRecordValidationReason(record, input))
|
|
492
|
+
.filter((reason) => typeof reason === "string");
|
|
493
|
+
const uniqueReasons = [...new Set(reasons)];
|
|
494
|
+
return uniqueReasons.length === 1 ? uniqueReasons[0] : "no matching confirmed commerce_authority";
|
|
495
|
+
}
|
|
496
|
+
function validateMatchingCommerceAuthority(input) {
|
|
497
|
+
const records = readAllCommerceRecords(input.agentRoot);
|
|
498
|
+
const matching = records.filter((record) => authorityRecordValidationReason(record, input) === null);
|
|
499
|
+
if (matching.length === 0)
|
|
500
|
+
return { ok: false, reason: noMatchingAuthorityReason(records, input) };
|
|
501
|
+
if (matching.length > 1)
|
|
502
|
+
return { ok: false, reason: "multiple matching confirmed commerce_authority records" };
|
|
503
|
+
return { ok: true, record: matching[0] };
|
|
504
|
+
}
|
|
505
|
+
function validateCommerceAuthority(input) {
|
|
506
|
+
return input.token?.trim() ? validateCommerceAuthorityToken(input) : validateMatchingCommerceAuthority(input);
|
|
507
|
+
}
|
|
508
|
+
function consumeValidatedCommerceRecord(agentRoot, record, input) {
|
|
509
|
+
const now = new Date().toISOString();
|
|
510
|
+
const consumed = {
|
|
511
|
+
...record,
|
|
512
|
+
status: "consumed",
|
|
513
|
+
reservedAt: undefined,
|
|
514
|
+
reservedByTool: undefined,
|
|
515
|
+
reservationTokenHash: undefined,
|
|
516
|
+
attemptedAt: undefined,
|
|
517
|
+
attemptedByTool: undefined,
|
|
518
|
+
consumedAt: now,
|
|
519
|
+
consumedByTool: input.toolName,
|
|
520
|
+
updatedAt: now,
|
|
521
|
+
};
|
|
522
|
+
writeRecord(agentRoot, consumed);
|
|
523
|
+
appendAccessLog(agentRoot, {
|
|
524
|
+
checkoutId: record.id,
|
|
525
|
+
action: "consume",
|
|
526
|
+
toolName: input.toolName,
|
|
527
|
+
friendId: input.friendId,
|
|
528
|
+
ok: true,
|
|
529
|
+
});
|
|
530
|
+
(0, runtime_1.emitNervesEvent)({
|
|
531
|
+
component: "repertoire",
|
|
532
|
+
event: "repertoire.commerce_authority_consumed",
|
|
533
|
+
message: "consumed commerce authority token",
|
|
534
|
+
meta: { checkoutId: record.id, toolName: input.toolName },
|
|
535
|
+
});
|
|
536
|
+
return { ok: true, record: consumed };
|
|
537
|
+
}
|
|
538
|
+
function consumeMatchingCommerceAuthority(input) {
|
|
539
|
+
const matching = matchingCommerceRecords(input);
|
|
540
|
+
if (matching.length === 0)
|
|
541
|
+
return { ok: false, reason: "no matching confirmed commerce_authority" };
|
|
542
|
+
if (matching.length > 1)
|
|
543
|
+
return { ok: false, reason: "multiple matching confirmed commerce_authority records" };
|
|
544
|
+
const record = matching[0];
|
|
545
|
+
return withRecordLock(input.agentRoot, record.id, () => {
|
|
546
|
+
const fresh = readCommerceRecord(input.agentRoot, record.id);
|
|
547
|
+
/* v8 ignore next -- race-defense: matching records can disappear between directory scan and checkout lock @preserve */
|
|
548
|
+
if (!fresh)
|
|
549
|
+
return { ok: false, reason: "commerce checkout not found" };
|
|
550
|
+
const reason = authorityRecordValidationReason(fresh, input);
|
|
551
|
+
/* v8 ignore next -- race-defense: matching records can change between directory scan and checkout lock @preserve */
|
|
552
|
+
if (reason)
|
|
553
|
+
return { ok: false, reason };
|
|
554
|
+
return consumeValidatedCommerceRecord(input.agentRoot, fresh, input);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function checkoutIdFromAuthorityToken(token) {
|
|
558
|
+
return token ? /^commerce:([^:]+):[a-f0-9]{64}:[0-9a-f-]{36}$/.exec(token)?.[1] : undefined;
|
|
559
|
+
}
|
|
560
|
+
function reserveValidatedCommerceRecord(agentRoot, record, input) {
|
|
561
|
+
const now = new Date().toISOString();
|
|
562
|
+
const token = reservationToken();
|
|
563
|
+
const reserved = {
|
|
564
|
+
...record,
|
|
565
|
+
status: "reserved",
|
|
566
|
+
reservedAt: now,
|
|
567
|
+
reservedByTool: input.toolName,
|
|
568
|
+
reservationTokenHash: tokenHash(token),
|
|
569
|
+
updatedAt: now,
|
|
570
|
+
};
|
|
571
|
+
writeRecord(agentRoot, reserved);
|
|
572
|
+
appendAccessLog(agentRoot, {
|
|
573
|
+
checkoutId: record.id,
|
|
574
|
+
action: "reserve",
|
|
575
|
+
toolName: input.toolName,
|
|
576
|
+
friendId: input.friendId,
|
|
577
|
+
ok: true,
|
|
578
|
+
});
|
|
579
|
+
(0, runtime_1.emitNervesEvent)({
|
|
580
|
+
component: "repertoire",
|
|
581
|
+
event: "repertoire.commerce_authority_reserved",
|
|
582
|
+
message: "reserved commerce authority for tool execution",
|
|
583
|
+
meta: { checkoutId: record.id, toolName: input.toolName },
|
|
584
|
+
});
|
|
585
|
+
return { ok: true, checkoutId: record.id, reservationToken: token, record: reserved };
|
|
586
|
+
}
|
|
587
|
+
function reserveMatchingCommerceAuthority(input) {
|
|
588
|
+
const records = readAllCommerceRecords(input.agentRoot);
|
|
589
|
+
const matching = records.filter((record) => authorityRecordValidationReason(record, input) === null);
|
|
590
|
+
if (matching.length === 0)
|
|
591
|
+
return { ok: false, reason: noMatchingAuthorityReason(records, input) };
|
|
592
|
+
if (matching.length > 1)
|
|
593
|
+
return { ok: false, reason: "multiple matching confirmed commerce_authority records" };
|
|
594
|
+
const record = matching[0];
|
|
595
|
+
return withRecordLock(input.agentRoot, record.id, () => {
|
|
596
|
+
const fresh = readCommerceRecord(input.agentRoot, record.id);
|
|
597
|
+
/* v8 ignore next -- race-defense: matching records can disappear between directory scan and checkout lock @preserve */
|
|
598
|
+
if (!fresh)
|
|
599
|
+
return { ok: false, reason: "commerce checkout not found" };
|
|
600
|
+
const reason = authorityRecordValidationReason(fresh, input);
|
|
601
|
+
/* v8 ignore next -- race-defense: matching records can change between directory scan and checkout lock @preserve */
|
|
602
|
+
if (reason)
|
|
603
|
+
return { ok: false, reason };
|
|
604
|
+
return reserveValidatedCommerceRecord(input.agentRoot, fresh, input);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function reserveCommerceAuthority(input) {
|
|
608
|
+
const token = input.token?.trim();
|
|
609
|
+
const checkoutId = checkoutIdFromAuthorityToken(token);
|
|
610
|
+
if (!checkoutId) {
|
|
611
|
+
if (!token)
|
|
612
|
+
return reserveMatchingCommerceAuthority(input);
|
|
613
|
+
return { ok: false, reason: "invalid commerce_authority token format" };
|
|
614
|
+
}
|
|
615
|
+
return withRecordLock(input.agentRoot, checkoutId, () => {
|
|
616
|
+
const validation = validateCommerceAuthorityToken(input);
|
|
617
|
+
if (!validation.ok)
|
|
618
|
+
return validation;
|
|
619
|
+
return reserveValidatedCommerceRecord(input.agentRoot, validation.record, input);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function consumeReservedCommerceAuthority(input) {
|
|
623
|
+
return withRecordLock(input.agentRoot, input.checkoutId, () => {
|
|
624
|
+
const record = readCommerceRecord(input.agentRoot, input.checkoutId);
|
|
625
|
+
if (!record)
|
|
626
|
+
return { ok: false, reason: "commerce checkout not found" };
|
|
627
|
+
if (record.status !== "reserved" && record.status !== "attempted") {
|
|
628
|
+
return { ok: false, reason: `commerce checkout is ${record.status}, not reserved or attempted` };
|
|
629
|
+
}
|
|
630
|
+
if (record.reservedByTool !== input.toolName)
|
|
631
|
+
return { ok: false, reason: "commerce_authority reservation belongs to a different tool" };
|
|
632
|
+
if (input.friendId && record.friendId !== input.friendId)
|
|
633
|
+
return { ok: false, reason: "commerce_authority belongs to a different friend" };
|
|
634
|
+
if (record.reservationTokenHash !== tokenHash(input.reservationToken))
|
|
635
|
+
return { ok: false, reason: "commerce_authority reservation token mismatch" };
|
|
636
|
+
return consumeValidatedCommerceRecord(input.agentRoot, record, {
|
|
637
|
+
agentRoot: input.agentRoot,
|
|
638
|
+
token: undefined,
|
|
639
|
+
toolName: input.toolName,
|
|
640
|
+
friendId: input.friendId,
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function markReservedCommerceAuthorityAttempted(input) {
|
|
645
|
+
return withRecordLock(input.agentRoot, input.checkoutId, () => {
|
|
646
|
+
const record = readCommerceRecord(input.agentRoot, input.checkoutId);
|
|
647
|
+
if (!record)
|
|
648
|
+
return { ok: false, reason: "commerce checkout not found" };
|
|
649
|
+
if (record.status !== "reserved")
|
|
650
|
+
return { ok: false, reason: `commerce checkout is ${record.status}, not reserved` };
|
|
651
|
+
if (record.reservedByTool !== input.toolName)
|
|
652
|
+
return { ok: false, reason: "commerce_authority reservation belongs to a different tool" };
|
|
653
|
+
if (input.friendId && record.friendId !== input.friendId)
|
|
654
|
+
return { ok: false, reason: "commerce_authority belongs to a different friend" };
|
|
655
|
+
if (record.reservationTokenHash !== tokenHash(input.reservationToken))
|
|
656
|
+
return { ok: false, reason: "commerce_authority reservation token mismatch" };
|
|
657
|
+
const now = new Date().toISOString();
|
|
658
|
+
const attempted = {
|
|
659
|
+
...record,
|
|
660
|
+
status: "attempted",
|
|
661
|
+
attemptedAt: now,
|
|
662
|
+
attemptedByTool: input.toolName,
|
|
663
|
+
updatedAt: now,
|
|
664
|
+
};
|
|
665
|
+
writeRecord(input.agentRoot, attempted);
|
|
666
|
+
appendAccessLog(input.agentRoot, {
|
|
667
|
+
checkoutId: record.id,
|
|
668
|
+
action: "attempt",
|
|
669
|
+
toolName: input.toolName,
|
|
670
|
+
friendId: input.friendId,
|
|
671
|
+
ok: true,
|
|
672
|
+
});
|
|
673
|
+
(0, runtime_1.emitNervesEvent)({
|
|
674
|
+
component: "repertoire",
|
|
675
|
+
event: "repertoire.commerce_authority_attempted",
|
|
676
|
+
message: "marked commerce authority as externally attempted",
|
|
677
|
+
meta: { checkoutId: record.id, toolName: input.toolName },
|
|
678
|
+
});
|
|
679
|
+
return { ok: true, record: attempted };
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
function releaseReservedCommerceAuthority(input) {
|
|
683
|
+
return withRecordLock(input.agentRoot, input.checkoutId, () => {
|
|
684
|
+
const record = readCommerceRecord(input.agentRoot, input.checkoutId);
|
|
685
|
+
if (!record || record.status !== "reserved")
|
|
686
|
+
return { ok: false, reason: "commerce checkout is not reserved" };
|
|
687
|
+
if (record.reservedByTool !== input.toolName)
|
|
688
|
+
return { ok: false, reason: "commerce_authority reservation belongs to a different tool" };
|
|
689
|
+
if (input.friendId && record.friendId !== input.friendId)
|
|
690
|
+
return { ok: false, reason: "commerce_authority belongs to a different friend" };
|
|
691
|
+
if (record.reservationTokenHash !== tokenHash(input.reservationToken))
|
|
692
|
+
return { ok: false, reason: "commerce_authority reservation token mismatch" };
|
|
693
|
+
const now = new Date().toISOString();
|
|
694
|
+
const released = {
|
|
695
|
+
...record,
|
|
696
|
+
status: "confirmed",
|
|
697
|
+
reservedAt: undefined,
|
|
698
|
+
reservedByTool: undefined,
|
|
699
|
+
reservationTokenHash: undefined,
|
|
700
|
+
updatedAt: now,
|
|
701
|
+
};
|
|
702
|
+
writeRecord(input.agentRoot, released);
|
|
703
|
+
appendAccessLog(input.agentRoot, {
|
|
704
|
+
checkoutId: record.id,
|
|
705
|
+
action: "release",
|
|
706
|
+
toolName: input.toolName,
|
|
707
|
+
friendId: input.friendId,
|
|
708
|
+
ok: true,
|
|
709
|
+
});
|
|
710
|
+
(0, runtime_1.emitNervesEvent)({
|
|
711
|
+
component: "repertoire",
|
|
712
|
+
event: "repertoire.commerce_authority_released",
|
|
713
|
+
message: "released reserved commerce authority",
|
|
714
|
+
meta: { checkoutId: record.id, toolName: input.toolName },
|
|
715
|
+
});
|
|
716
|
+
return { ok: true, record: released };
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
function consumeCommerceAuthorityToken(input) {
|
|
720
|
+
const token = input.token?.trim();
|
|
721
|
+
const checkoutId = checkoutIdFromAuthorityToken(token);
|
|
722
|
+
if (!checkoutId)
|
|
723
|
+
return token ? validateCommerceAuthorityToken(input) : consumeMatchingCommerceAuthority(input);
|
|
724
|
+
return withRecordLock(input.agentRoot, checkoutId, () => {
|
|
725
|
+
const validation = validateCommerceAuthorityToken(input);
|
|
726
|
+
if (!validation.ok)
|
|
727
|
+
return validation;
|
|
728
|
+
return consumeValidatedCommerceRecord(input.agentRoot, validation.record, input);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function readCommerceAccessLog(agentRoot, limit = 20) {
|
|
732
|
+
try {
|
|
733
|
+
const lines = fs.readFileSync(accessLogPath(agentRoot), "utf-8").trim().split("\n").filter(Boolean);
|
|
734
|
+
const entries = lines.map((line) => JSON.parse(line));
|
|
735
|
+
(0, runtime_1.emitNervesEvent)({
|
|
736
|
+
component: "repertoire",
|
|
737
|
+
event: "repertoire.commerce_access_log_read",
|
|
738
|
+
message: "read commerce access log",
|
|
739
|
+
meta: { limit },
|
|
740
|
+
});
|
|
741
|
+
return entries.slice(-limit);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function confirmationPhrase() {
|
|
748
|
+
(0, runtime_1.emitNervesEvent)({
|
|
749
|
+
component: "repertoire",
|
|
750
|
+
event: "repertoire.commerce_confirmation_phrase_read",
|
|
751
|
+
message: "read commerce confirmation phrase",
|
|
752
|
+
meta: {},
|
|
753
|
+
});
|
|
754
|
+
return CONFIRMATION_PHRASE;
|
|
755
|
+
}
|