@ouro.bot/cli 0.1.0-alpha.236 → 0.1.0-alpha.238

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.
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.userProfileToolDefinitions = void 0;
4
+ const user_profile_1 = require("./user-profile");
5
+ const credential_access_1 = require("./credential-access");
6
+ const runtime_1 = require("../nerves/runtime");
7
+ function requireFamilyContext(ctx) {
8
+ if (!ctx?.context?.friend?.id) {
9
+ return "no friend context — cannot access user profile.";
10
+ }
11
+ if (ctx.context.friend.trustLevel !== "family") {
12
+ return "user profile access requires family trust level.";
13
+ }
14
+ return { friendId: ctx.context.friend.id };
15
+ }
16
+ exports.userProfileToolDefinitions = [
17
+ {
18
+ tool: {
19
+ type: "function",
20
+ function: {
21
+ name: "user_profile_store",
22
+ description: "Store or update user profile fields (legal name, DOB, passport, etc.) in the agent's vault. Fields are merged with any existing profile. Requires family trust level.",
23
+ parameters: {
24
+ type: "object",
25
+ properties: {
26
+ fields: {
27
+ type: "string",
28
+ description: 'JSON object with profile fields to store/update. Keys: legalName, dateOfBirth, gender, nationality, passport, driverLicense, email, phone, addresses, loyaltyPrograms, preferences, emergencyContact.',
29
+ },
30
+ },
31
+ required: ["fields"],
32
+ },
33
+ },
34
+ },
35
+ handler: async (args, ctx) => {
36
+ (0, runtime_1.emitNervesEvent)({
37
+ component: "repertoire",
38
+ event: "repertoire.tool_user_profile_store",
39
+ message: "user_profile_store invoked",
40
+ meta: { tool: "user_profile_store" },
41
+ });
42
+ const guard = requireFamilyContext(ctx);
43
+ if (typeof guard === "string")
44
+ return guard;
45
+ let fields;
46
+ try {
47
+ fields = JSON.parse(args.fields);
48
+ }
49
+ catch {
50
+ return "invalid JSON in fields parameter.";
51
+ }
52
+ try {
53
+ const store = (0, credential_access_1.getCredentialStore)();
54
+ await (0, user_profile_1.updateUserProfileFields)(guard.friendId, fields, store);
55
+ return `profile fields stored for ${guard.friendId}.`;
56
+ }
57
+ catch (err) {
58
+ /* v8 ignore next -- defensive: updateUserProfileFields errors are always Error instances @preserve */
59
+ return `failed to store profile: ${err instanceof Error ? err.message : String(err)}`;
60
+ }
61
+ },
62
+ summaryKeys: ["fields"],
63
+ },
64
+ {
65
+ tool: {
66
+ type: "function",
67
+ function: {
68
+ name: "user_profile_get",
69
+ description: "Retrieve a specific field from a user's profile. Only returns the requested field, never the full profile. Requires family trust level.",
70
+ parameters: {
71
+ type: "object",
72
+ properties: {
73
+ field: {
74
+ type: "string",
75
+ description: "The profile field to retrieve: legalName, dateOfBirth, gender, nationality, passport, driverLicense, email, phone, addresses, loyaltyPrograms, preferences, emergencyContact.",
76
+ },
77
+ },
78
+ required: ["field"],
79
+ },
80
+ },
81
+ },
82
+ handler: async (args, ctx) => {
83
+ (0, runtime_1.emitNervesEvent)({
84
+ component: "repertoire",
85
+ event: "repertoire.tool_user_profile_get",
86
+ message: "user_profile_get invoked",
87
+ meta: { tool: "user_profile_get", field: args.field },
88
+ });
89
+ const guard = requireFamilyContext(ctx);
90
+ if (typeof guard === "string")
91
+ return guard;
92
+ try {
93
+ const store = (0, credential_access_1.getCredentialStore)();
94
+ const value = await (0, user_profile_1.getUserProfileField)(guard.friendId, args.field, store);
95
+ if (value === undefined) {
96
+ return `field "${args.field}" is not set on the profile.`;
97
+ }
98
+ /* v8 ignore next -- platform-dependent v8 branch counting on ternary @preserve */
99
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
100
+ }
101
+ catch (err) {
102
+ /* v8 ignore next -- defensive: getUserProfileField errors are always Error instances @preserve */
103
+ return `failed to get profile field: ${err instanceof Error ? err.message : String(err)}`;
104
+ }
105
+ },
106
+ summaryKeys: ["field"],
107
+ },
108
+ {
109
+ tool: {
110
+ type: "function",
111
+ function: {
112
+ name: "user_profile_delete",
113
+ description: "Delete a user's entire profile from the vault. This is irreversible. Requires family trust level.",
114
+ parameters: {
115
+ type: "object",
116
+ properties: {},
117
+ },
118
+ },
119
+ },
120
+ handler: async (_args, ctx) => {
121
+ (0, runtime_1.emitNervesEvent)({
122
+ component: "repertoire",
123
+ event: "repertoire.tool_user_profile_delete",
124
+ message: "user_profile_delete invoked",
125
+ meta: { tool: "user_profile_delete" },
126
+ });
127
+ const guard = requireFamilyContext(ctx);
128
+ if (typeof guard === "string")
129
+ return guard;
130
+ try {
131
+ const store = (0, credential_access_1.getCredentialStore)();
132
+ const deleted = await (0, user_profile_1.deleteUserProfile)(guard.friendId, store);
133
+ return deleted
134
+ ? `profile deleted for ${guard.friendId}.`
135
+ : `no profile found for ${guard.friendId}.`;
136
+ }
137
+ catch (err) {
138
+ /* v8 ignore next -- defensive: deleteUserProfile errors are always Error instances @preserve */
139
+ return `failed to delete profile: ${err instanceof Error ? err.message : String(err)}`;
140
+ }
141
+ },
142
+ summaryKeys: [],
143
+ },
144
+ ];
@@ -3,8 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.surfaceToolDef = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
4
4
  exports.resetMcpDefinitions = resetMcpDefinitions;
5
5
  exports.getToolsForChannel = getToolsForChannel;
6
- exports.isConfirmationRequired = isConfirmationRequired;
7
- exports.isConfirmationAlwaysRequired = isConfirmationAlwaysRequired;
8
6
  exports.execTool = execTool;
9
7
  exports.summarizeArgs = summarizeArgs;
10
8
  exports.buildToolResultSummary = buildToolResultSummary;
@@ -39,7 +37,7 @@ Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: functi
39
37
  // All tool definitions in a single registry
40
38
  const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, tools_surface_1.surfaceToolDefinition];
41
39
  // MCP tool definitions — populated each time getToolsForChannel() is called with an mcpManager.
42
- // Kept separate from allDefinitions so execTool/isConfirmation* can find them.
40
+ // Kept separate from allDefinitions so execTool can find them.
43
41
  let mcpDefinitions = [];
44
42
  /** Exported for testing — reset the MCP definitions cache. */
45
43
  function resetMcpDefinitions() {
@@ -120,18 +118,6 @@ function findDefinition(toolName) {
120
118
  return allDefinitions.find((d) => d.tool.function.name === toolName)
121
119
  ?? mcpDefinitions.find((d) => d.tool.function.name === toolName);
122
120
  }
123
- // Check whether a tool requires user confirmation before execution.
124
- // Reads from ToolDefinition.confirmationRequired instead of a separate Set.
125
- function isConfirmationRequired(toolName) {
126
- const def = findDefinition(toolName);
127
- return def?.confirmationRequired === true;
128
- }
129
- // Check whether a tool's confirmation is non-bypassable (cannot be skipped by skipConfirmation).
130
- // Used for T2 config proposals that must always require operator approval.
131
- function isConfirmationAlwaysRequired(toolName) {
132
- const def = findDefinition(toolName);
133
- return def?.confirmationAlwaysRequired === true;
134
- }
135
121
  function normalizeGuardArgs(_name, args) {
136
122
  return args;
137
123
  }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * User profile model and vault storage.
4
+ *
5
+ * Profiles are stored as secure notes in the agent's Vaultwarden vault,
6
+ * keyed by friend ID: `user-profile/{friendId}`.
7
+ *
8
+ * The storage layer uses the existing CredentialStore interface — the profile
9
+ * JSON is stored in the `password` field of a login item (the vault's most
10
+ * reliable field for arbitrary data). Field-level access ensures the full
11
+ * profile is never dumped to model context unnecessarily.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.storeUserProfile = storeUserProfile;
15
+ exports.getUserProfile = getUserProfile;
16
+ exports.getUserProfileField = getUserProfileField;
17
+ exports.deleteUserProfile = deleteUserProfile;
18
+ exports.updateUserProfileFields = updateUserProfileFields;
19
+ const runtime_1 = require("../nerves/runtime");
20
+ // ---------------------------------------------------------------------------
21
+ // Storage key
22
+ // ---------------------------------------------------------------------------
23
+ function profileKey(friendId) {
24
+ return `user-profile/${friendId}`;
25
+ }
26
+ // ---------------------------------------------------------------------------
27
+ // CRUD operations
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * Store a complete user profile in the vault.
31
+ * Overwrites any existing profile for the given friend ID.
32
+ */
33
+ async function storeUserProfile(friendId, profile, store) {
34
+ (0, runtime_1.emitNervesEvent)({
35
+ event: "repertoire.user_profile_store",
36
+ component: "repertoire",
37
+ message: `storing user profile for ${friendId}`,
38
+ meta: { friendId },
39
+ });
40
+ const key = profileKey(friendId);
41
+ /* v8 ignore start -- platform-dependent v8 branch counting on await @preserve */
42
+ await store.store(key, {
43
+ password: JSON.stringify(profile),
44
+ notes: "user-profile",
45
+ });
46
+ /* v8 ignore stop */
47
+ }
48
+ /**
49
+ * Retrieve the full user profile for a friend.
50
+ * Returns null if no profile exists or if the stored data is invalid.
51
+ */
52
+ async function getUserProfile(friendId, store) {
53
+ (0, runtime_1.emitNervesEvent)({
54
+ event: "repertoire.user_profile_get",
55
+ component: "repertoire",
56
+ message: `getting user profile for ${friendId}`,
57
+ meta: { friendId },
58
+ });
59
+ try {
60
+ const raw = await store.getRawSecret(profileKey(friendId), "password");
61
+ const parsed = JSON.parse(raw);
62
+ return parsed;
63
+ /* v8 ignore next 2 -- platform-dependent v8 branch counting on catch @preserve */
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ /**
70
+ * Retrieve a specific field from a user profile.
71
+ * Returns undefined if the profile doesn't exist or the field is not set.
72
+ */
73
+ async function getUserProfileField(friendId, field, store) {
74
+ const profile = await getUserProfile(friendId, store);
75
+ if (!profile)
76
+ return undefined;
77
+ return profile[field];
78
+ }
79
+ /**
80
+ * Delete a user profile from the vault.
81
+ * Returns true if the profile was deleted, false if it didn't exist.
82
+ */
83
+ async function deleteUserProfile(friendId, store) {
84
+ (0, runtime_1.emitNervesEvent)({
85
+ event: "repertoire.user_profile_delete",
86
+ component: "repertoire",
87
+ message: `deleting user profile for ${friendId}`,
88
+ meta: { friendId },
89
+ });
90
+ return store.delete(profileKey(friendId));
91
+ }
92
+ /**
93
+ * Update specific fields on a user profile, merging with existing data.
94
+ * Creates the profile if it doesn't exist.
95
+ * Preferences are merged (not replaced) at the key level.
96
+ */
97
+ async function updateUserProfileFields(friendId, fields, store) {
98
+ (0, runtime_1.emitNervesEvent)({
99
+ event: "repertoire.user_profile_update",
100
+ component: "repertoire",
101
+ message: `updating user profile fields for ${friendId}`,
102
+ meta: { friendId, fieldCount: Object.keys(fields).length },
103
+ });
104
+ const existing = await getUserProfile(friendId, store);
105
+ let merged;
106
+ if (existing) {
107
+ // Merge preferences at key level
108
+ const mergedPreferences = {
109
+ ...existing.preferences,
110
+ ...(fields.preferences ?? {}),
111
+ };
112
+ merged = { ...existing, ...fields, preferences: mergedPreferences };
113
+ }
114
+ else {
115
+ merged = fields;
116
+ }
117
+ await storeUserProfile(friendId, merged, store);
118
+ }
@@ -693,7 +693,6 @@ async function runInnerDialogTurn(options) {
693
693
  runAgentOptions: {
694
694
  traceId,
695
695
  toolChoiceRequired: true,
696
- skipConfirmation: true,
697
696
  mcpManager,
698
697
  toolContext: {
699
698
  signin: async () => undefined,
@@ -41,7 +41,6 @@ exports.sanitizeFeedbackComment = sanitizeFeedbackComment;
41
41
  exports.buildFeedbackSyntheticText = buildFeedbackSyntheticText;
42
42
  exports.buildWelcomeCard = buildWelcomeCard;
43
43
  exports.createTeamsCallbacks = createTeamsCallbacks;
44
- exports.resolvePendingConfirmation = resolvePendingConfirmation;
45
44
  exports.withConversationLock = withConversationLock;
46
45
  exports.handleTeamsMessage = handleTeamsMessage;
47
46
  exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
@@ -479,24 +478,6 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
479
478
  safeSend(msg);
480
479
  }
481
480
  },
482
- onConfirmAction: options?.conversationId
483
- ? async (name, args) => {
484
- const convId = options.conversationId;
485
- const argsDesc = Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(", ");
486
- safeUpdate(`Confirm action: ${name} (${argsDesc}) -- reply "yes" to confirm or "no" to cancel`);
487
- return new Promise((resolve) => {
488
- _pendingConfirmations.set(convId, resolve);
489
- // Auto-deny after 2 minutes to prevent indefinite blocking
490
- // (e.g. when the stream dies and the user never sees the prompt).
491
- setTimeout(() => {
492
- if (_pendingConfirmations.has(convId)) {
493
- _pendingConfirmations.delete(convId);
494
- resolve("denied");
495
- }
496
- }, 120_000);
497
- });
498
- }
499
- : undefined,
500
481
  flush: async () => {
501
482
  stopFlushTimer();
502
483
  stopPhraseRotation();
@@ -534,26 +515,6 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
534
515
  },
535
516
  };
536
517
  }
537
- // Per-conversation pending confirmation resolvers.
538
- // When a mutate tool needs confirmation, the resolver is stored here.
539
- // The next message from the same conversation resolves it.
540
- const _pendingConfirmations = new Map();
541
- // Confirmation response words (case-insensitive)
542
- const CONFIRM_WORDS = new Set(["yes", "confirm", "go", "y", "ok", "approve", "proceed"]);
543
- function resolvePendingConfirmation(convId, text) {
544
- const resolver = _pendingConfirmations.get(convId);
545
- if (!resolver)
546
- return false;
547
- _pendingConfirmations.delete(convId);
548
- const word = text.trim().toLowerCase();
549
- if (CONFIRM_WORDS.has(word)) {
550
- resolver("confirmed");
551
- }
552
- else {
553
- resolver("denied");
554
- }
555
- return true;
556
- }
557
518
  const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
558
519
  function teamsTurnKey(conversationId) {
559
520
  return `teams:${conversationId}`;
@@ -659,8 +620,6 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
659
620
  },
660
621
  ...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
661
622
  };
662
- if (channelConfig.skipConfirmation)
663
- agentOptions.skipConfirmation = true;
664
623
  // ── Call shared pipeline ──────────────────────────────────────────
665
624
  // Capture terminal errors — failover message replaces the error card if it triggers
666
625
  let capturedTerminalError = null;
@@ -961,13 +920,6 @@ function registerBotHandlers(app, label) {
961
920
  const userId = activity.from?.id || "";
962
921
  const channelId = activity.channelId || "msteams";
963
922
  (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
964
- // Resolve pending confirmations IMMEDIATELY — before token fetches or
965
- // the conversation lock. The original message holds the lock while
966
- // awaiting confirmation, so acquiring it here would deadlock. Token
967
- // fetches are also unnecessary (and slow) for a simple yes/no reply.
968
- if (resolvePendingConfirmation(convId, text)) {
969
- return;
970
- }
971
923
  const commandRegistry = createTeamsCommandRegistry();
972
924
  const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
973
925
  if (parsedSlashCommand) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.236",
3
+ "version": "0.1.0-alpha.238",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -42,7 +42,8 @@
42
42
  "ink": "^3.2.0",
43
43
  "openai": "^6.27.0",
44
44
  "react": "^17.0.2",
45
- "semver": "^7.7.4"
45
+ "semver": "^7.7.4",
46
+ "stripe": "^22.0.0"
46
47
  },
47
48
  "repository": {
48
49
  "type": "git",
@@ -0,0 +1,106 @@
1
+ # Agent Commerce Skill
2
+
3
+ How to book, purchase, and pay for things on behalf of humans.
4
+
5
+ ## Three Patterns
6
+
7
+ ### Pattern A: API (Structured, Preferred)
8
+
9
+ For services with direct API access: Duffel flights, LiteAPI hotels.
10
+
11
+ 1. Search using the API tool (`flight_search`, LiteAPI MCP)
12
+ 2. Present options to the human with prices and details
13
+ 3. Human approves a specific option and price
14
+ 4. Book using the API tool with passenger data from `user_profile_get`
15
+ 5. Create a single-use virtual card via `stripe_create_card`
16
+ 6. Complete payment through the API
17
+ 7. Deactivate the card via `stripe_deactivate_card`
18
+ 8. Confirm booking to the human
19
+
20
+ **Key tools**: `flight_search`, `flight_book`, `flight_cancel`, `user_profile_get`, `user_profile_store`, `stripe_create_card`, `stripe_deactivate_card`, `stripe_list_cards`
21
+
22
+ ### Pattern B: Browser (Best-Effort)
23
+
24
+ For sites without API access, use browser automation via Playwright MCP.
25
+
26
+ 1. Navigate to the booking site
27
+ 2. Search for the requested service
28
+ 3. Fill forms using data from `user_profile_get`
29
+ 4. Use a virtual card from `stripe_create_card` for payment
30
+ 5. If blocked by anti-bot measures, fall back to Pattern C
31
+ 6. Complete and confirm the booking
32
+
33
+ **Limitations**: Browser automation is fragile. Sites may block, layouts change, CAPTCHAs appear. Always have Pattern C as fallback.
34
+
35
+ ### Pattern C: Link-Only (Primary for Hostile Sites)
36
+
37
+ For sites that block automation or require complex human interaction.
38
+
39
+ 1. Research the best options using browser tools or API tools
40
+ 2. Prepare a curated link with pre-filled parameters where possible
41
+ 3. Send the link to the human with a summary of what to book
42
+ 4. Human completes the booking in their own browser
43
+
44
+ **When to use**: Always use Pattern C as the primary approach for sites known to block automation (most airline direct sites, hotel chains, rental car sites). Pattern B is best-effort, not reliable.
45
+
46
+ ## Payment autonomy Levels
47
+
48
+ - **Level 0**: No autonomous payments. Agent researches, human pays manually.
49
+ - **Level 1**: Agent creates virtual cards, human approves each transaction explicitly.
50
+ - **Level 2**: Agent can book pre-approved items (within budget, approved categories) without per-transaction approval.
51
+ - **Level 3**: Full delegation with spending limits. Agent manages a budget and books as needed.
52
+
53
+ Default is Level 1. Level changes require explicit human approval.
54
+
55
+ ## Error Handling
56
+
57
+ ### Price Change Guard
58
+ Before completing a booking, verify the final price matches the approved price within 5%. If the price changed more than 5%, stop and report to the human. Never pay a price the human didn't approve.
59
+
60
+ ### Partial Failure Reporting
61
+ When booking involves multiple services (e.g., flight + hotel), each service may succeed or fail independently — this is a partial failure scenario. Report the status of each service separately. **Never auto-cancel a successful booking because a related booking failed.** Let the human decide.
62
+
63
+ Example: "Flight SFO-JFK booked (confirmation: ABC123). Hotel booking failed: no availability for those dates. Would you like me to search for alternative hotels?"
64
+
65
+ ### Refund Flow
66
+ If a booking fails after card creation:
67
+ 1. Deactivate the virtual card immediately
68
+ 2. Report the failure to the human
69
+ 3. If a charge was made, note it for the human to follow up with the provider
70
+
71
+ ## CAPTCHA Handling
72
+
73
+ When a CAPTCHA appears during browser automation (Pattern B):
74
+ 1. Take a screenshot and send it to the human
75
+ 2. Explain what page you're on and what you were trying to do
76
+ 3. Ask the human to solve the CAPTCHA in their own browser
77
+ 4. Switch to Pattern C (link-only) for this transaction
78
+
79
+ Never attempt to solve CAPTCHAs programmatically.
80
+
81
+ ## Card Number Isolation
82
+
83
+ Card numbers must NEVER appear in:
84
+ - Tool return values shown to the model
85
+ - Nerves events or logs
86
+ - Chat messages to the human
87
+ - Any stored state or memory
88
+
89
+ The only place card numbers exist is inside the Stripe client's internal payment flow functions, scoped to a single function call. The model only ever sees card IDs and last-4 digits.
90
+
91
+ ## Profile Data Usage
92
+
93
+ Access profile data only when needed for the current transaction:
94
+ - `user_profile_get` to retrieve specific fields (never dump full profile)
95
+ - Passport data only for international bookings
96
+ - Loyalty program numbers only when booking with that program
97
+ - Emergency contact only when the booking service requires it
98
+
99
+ ## Self-Test
100
+
101
+ Before first use, run the commerce self-test to verify all services are configured:
102
+ - Stripe: creates and deactivates a test virtual card
103
+ - Duffel: runs a test flight search
104
+ - LiteAPI: verifies API key in vault
105
+
106
+ Report results to the human with actionable next steps for any failures.
@@ -0,0 +1,116 @@
1
+ # Commerce Setup Guide
2
+
3
+ Human-facing guide for configuring the agent's commerce capabilities.
4
+
5
+ ## Architecture Overview
6
+
7
+ The agent's commerce system has three pillars:
8
+
9
+ 1. **User Profile**: Secure storage of personal data (name, passport, payment preferences) in Vaultwarden, accessed only when needed for specific transactions.
10
+
11
+ 2. **Booking Delegation**: Direct API access to flight (Duffel) and hotel (LiteAPI) booking systems, plus browser automation fallback for sites without APIs.
12
+
13
+ 3. **Payment**: Stripe Issuing virtual cards created per-transaction, with spend limits and merchant category restrictions. Card numbers never enter the model's context.
14
+
15
+ ## Setup Steps
16
+
17
+ ### 1. Stripe Issuing (Required for Payments)
18
+
19
+ - Create a Stripe account: https://dashboard.stripe.com
20
+ - Enable Stripe Issuing (requires business verification)
21
+ - Create a restricted API key: https://dashboard.stripe.com/apikeys
22
+ - Permissions needed: `issuing_cards:write`, `issuing_cards:read`
23
+ - Store in vault: `credential_store stripe.com --password <key>`
24
+
25
+ ### 2. Duffel (Flights)
26
+
27
+ - Create a Duffel account: https://app.duffel.com
28
+ - Generate a sandbox token: https://app.duffel.com/tokens
29
+ - Store in vault: `credential_store duffel.com --password <token>`
30
+
31
+ ### 3. LiteAPI (Hotels)
32
+
33
+ - Create a LiteAPI account: https://dashboard.liteapi.travel
34
+ - Get your sandbox API key
35
+ - Store in vault: `credential_store liteapi.travel --password <key>`
36
+ - Configure MCP server in agent.json (see commerce-setup.md skill for details)
37
+
38
+ ### 4. User Profile
39
+
40
+ Store your travel profile with the agent:
41
+ ```
42
+ user_profile_store '{"legalName":{"first":"...","last":"..."},"email":"...","phone":"...","preferences":{}}'
43
+ ```
44
+
45
+ ## Tool Reference
46
+
47
+ ### User Profile Tools
48
+ | Tool | Description | Trust Level |
49
+ |------|-------------|-------------|
50
+ | `user_profile_store` | Store/update profile fields | Family |
51
+ | `user_profile_get` | Retrieve a specific profile field | Family |
52
+ | `user_profile_delete` | Delete entire profile | Family |
53
+
54
+ ### Payment Tools
55
+ | Tool | Description | Trust Level |
56
+ |------|-------------|-------------|
57
+ | `stripe_create_card` | Create a virtual card | Family |
58
+ | `stripe_deactivate_card` | Deactivate a card | Family |
59
+ | `stripe_list_cards` | List active cards | Family |
60
+
61
+ ### Flight Tools
62
+ | Tool | Description | Trust Level |
63
+ |------|-------------|-------------|
64
+ | `flight_search` | Search for flights | Friend |
65
+ | `flight_hold` | Hold a flight offer | Family |
66
+ | `flight_book` | Book a flight | Family |
67
+ | `flight_cancel` | Cancel a booking | Family |
68
+
69
+ ### Hotel Tools
70
+ Hotels are accessed through the LiteAPI MCP server. Tools are dynamically registered when the MCP server connects. Common tools include search, get rates, and book.
71
+
72
+ ## Security Model
73
+
74
+ ### Credential Gateway
75
+ All API keys are stored in the agent's Vaultwarden vault. They are never exposed as environment variables or stored in config files. MCP servers receive credentials at startup via `vault:` env resolution.
76
+
77
+ ### Card Number Isolation
78
+ Virtual card numbers exist only within the Stripe client's internal functions during a payment flow. They are never:
79
+ - Returned to the model in tool outputs
80
+ - Included in nerves events or logs
81
+ - Stored in any persistent state
82
+ - Shown in chat messages
83
+
84
+ The model only sees card IDs (e.g., `ic_...`) and last-4 digits.
85
+
86
+ ### Trust Gating
87
+ - **Family trust required**: All profile, payment, and booking tools
88
+ - **Friend trust sufficient**: Flight search (read-only, no payment)
89
+ - **Stranger/acquaintance**: No commerce access
90
+
91
+ ### Vault Resolution Failures
92
+ When a `vault:` reference cannot be resolved for an MCP server:
93
+ - The specific server is skipped (not crashed)
94
+ - Other servers continue to start normally
95
+ - An error event is emitted with the specific resolution failure
96
+ - Error messages are actionable (e.g., "item not found", "field empty")
97
+
98
+ ## Verification Tiers
99
+
100
+ ### Tier 1: Real-Tested
101
+ - User profile vault CRUD (tested against live Vaultwarden)
102
+ - MCP credential injection (tested with real `vault:` env resolution)
103
+ - Tool registration and guardrail enforcement
104
+
105
+ ### Tier 2: Mocked
106
+ - Stripe Issuing API calls (mocked SDK)
107
+ - Duffel flight API calls (mocked HTTP)
108
+ - LiteAPI hotel API calls (MCP config only)
109
+ - Full booking flow (profile -> search -> card -> book -> confirm -> deactivate)
110
+ - Card number leakage verification
111
+
112
+ ### Post-Handoff
113
+ - Real Stripe Issuing card creation (requires live Stripe account with Issuing)
114
+ - Real Duffel booking (requires live API key)
115
+ - Real LiteAPI hotel search (requires live MCP server)
116
+ - Browser automation for Pattern B sites