@ouro.bot/cli 0.1.0-alpha.401 → 0.1.0-alpha.403

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 CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.403",
6
+ "changes": [
7
+ "Vault recovery help and parser usage now render repeatable `--from <json>` imports explicitly as `--from <json> [--from <json> ...]`, so the rescue command reads like a repeatable flag instead of a duplicated typo.",
8
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault recovery help clarity release."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.402",
13
+ "changes": [
14
+ "Added `credential_generate_password`, a family-trusted credential tool that mints strong signup passwords and tells the agent to persist the exact accepted password with `credential_store` once the site accepts it.",
15
+ "`credential_store` now rejects blank required fields up front, trims credential metadata, redacts secret-bearing store errors, and confirms that credentials were stored and verified instead of silently accepting junk inputs or leaking password-shaped text.",
16
+ "User profile vault merges now fail closed when the existing profile cannot be read or is malformed, instead of treating every read failure as \"missing\" and overwriting saved personal data.",
17
+ "Browser and travel skills now teach the truthful signup secret flow: generate a password, use it for the interactive signup, and only claim success after `credential_store` succeeds.",
18
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the agent-saved secret hardening release."
19
+ ]
20
+ },
4
21
  {
5
22
  "version": "0.1.0-alpha.401",
6
23
  "changes": [
@@ -282,7 +282,7 @@ const SUBCOMMAND_HELP = {
282
282
  },
283
283
  "vault recover": {
284
284
  description: "Create an agent vault at the stable agent email and import local JSON credential exports",
285
- usage: "ouro vault recover --agent <name> --from <json> [--from <json>] [--email <email>] [--server <url>] [--store <store>]",
285
+ usage: "ouro vault recover --agent <name> --from <json> [--from <json> ...] [--email <email>] [--server <url>] [--store <store>]",
286
286
  example: "ouro vault recover --agent ouroboros --from ./credentials.json",
287
287
  },
288
288
  "vault unlock": {
@@ -486,7 +486,7 @@ function parseVaultCommand(args) {
486
486
  }
487
487
  const value = rest[i + 1];
488
488
  if (!value)
489
- throw new Error("Usage: ouro vault recover --agent <name> --from <json> [--from <json>]");
489
+ throw new Error("Usage: ouro vault recover --agent <name> --from <json> [--from <json> ...]");
490
490
  sources.push(value);
491
491
  i += 1;
492
492
  continue;
@@ -522,7 +522,7 @@ function parseVaultCommand(args) {
522
522
  }
523
523
  if (sub === "recover") {
524
524
  if (sources.length === 0) {
525
- throw new Error("Usage: ouro vault recover --agent <name> --from <json> [--from <json>]");
525
+ throw new Error("Usage: ouro vault recover --agent <name> --from <json> [--from <json> ...]");
526
526
  }
527
527
  return {
528
528
  kind: "vault.recover",
@@ -287,7 +287,7 @@ function checkWriteTrustGuardrails(toolName, args, context) {
287
287
  // --- credential tool trust gating ---
288
288
  // Credential write tools: family only
289
289
  const CREDENTIAL_FAMILY_TOOLS = new Set([
290
- "credential_store", "credential_delete", "vault_setup",
290
+ "credential_generate_password", "credential_store", "credential_delete", "vault_setup",
291
291
  // User profile tools: family only
292
292
  "user_profile_store", "user_profile_get", "user_profile_delete",
293
293
  // Payment tools: family only
@@ -1,8 +1,139 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.credentialToolDefinitions = void 0;
37
+ const crypto = __importStar(require("node:crypto"));
4
38
  const credential_access_1 = require("./credential-access");
5
39
  const runtime_1 = require("../nerves/runtime");
40
+ const DEFAULT_PASSWORD_LENGTH = 24;
41
+ const MIN_PASSWORD_LENGTH = 12;
42
+ const MAX_PASSWORD_LENGTH = 128;
43
+ const MAX_ERROR_DETAIL_LENGTH = 500;
44
+ const PASSWORD_CHARSETS = {
45
+ lower: "abcdefghijkmnopqrstuvwxyz",
46
+ upper: "ABCDEFGHJKLMNPQRSTUVWXYZ",
47
+ digits: "23456789",
48
+ symbols: "!@#$%^&*()-_=+[]{}:,.?",
49
+ };
50
+ function sanitizeCredentialToolError(err, secrets = []) {
51
+ const raw = err instanceof Error ? err.message : String(err);
52
+ const scrubbed = raw
53
+ .split(/\r?\n/)
54
+ .filter((line) => !line.trim().startsWith("Command failed:"))
55
+ .join("\n")
56
+ .trim();
57
+ let sanitized = scrubbed || "command failed";
58
+ const uniqueSecrets = [...new Set(secrets.filter((value) => typeof value === "string" && value.length >= 4))]
59
+ .sort((left, right) => right.length - left.length);
60
+ for (const secret of uniqueSecrets) {
61
+ sanitized = sanitized.split(secret).join("[redacted]");
62
+ }
63
+ return sanitized
64
+ .replace(/[A-Za-z0-9+/=]{80,}/g, "[redacted]")
65
+ .slice(0, MAX_ERROR_DETAIL_LENGTH);
66
+ }
67
+ function requireTrimmedText(value, fieldName) {
68
+ if (typeof value !== "string" || value.trim().length === 0) {
69
+ throw new Error(`${fieldName} must be a non-empty string.`);
70
+ }
71
+ return value.trim();
72
+ }
73
+ function requireNonBlankSecret(value, fieldName) {
74
+ if (typeof value !== "string" || value.trim().length === 0) {
75
+ throw new Error(`${fieldName} must be a non-empty string.`);
76
+ }
77
+ return value;
78
+ }
79
+ function optionalTrimmedText(value, fieldName) {
80
+ if (value === undefined)
81
+ return undefined;
82
+ if (typeof value !== "string") {
83
+ throw new Error(`${fieldName} must be a string if provided.`);
84
+ }
85
+ const trimmed = value.trim();
86
+ return trimmed.length > 0 ? trimmed : undefined;
87
+ }
88
+ function parsePasswordLength(value) {
89
+ if (value === undefined || value === null || value === "")
90
+ return DEFAULT_PASSWORD_LENGTH;
91
+ const parsed = typeof value === "number" ? value : Number(value);
92
+ if (!Number.isInteger(parsed) || parsed < MIN_PASSWORD_LENGTH || parsed > MAX_PASSWORD_LENGTH) {
93
+ throw new Error(`length must be an integer between ${MIN_PASSWORD_LENGTH} and ${MAX_PASSWORD_LENGTH}.`);
94
+ }
95
+ return parsed;
96
+ }
97
+ function parseSymbolsFlag(value) {
98
+ if (value === undefined || value === null || value === "")
99
+ return true;
100
+ if (typeof value === "boolean")
101
+ return value;
102
+ /* v8 ignore next -- handler tests cover string "true"/"false"; branch mapping is noisy here @preserve */
103
+ if (typeof value === "string") {
104
+ const normalized = value.trim().toLowerCase();
105
+ if (normalized === "true")
106
+ return true;
107
+ if (normalized === "false")
108
+ return false;
109
+ }
110
+ throw new Error("symbols must be true or false.");
111
+ }
112
+ function randomChar(alphabet) {
113
+ /* v8 ignore next -- crypto.randomInt stays within bounds; fallback is defensive @preserve */
114
+ return alphabet[crypto.randomInt(0, alphabet.length)] ?? alphabet[0];
115
+ }
116
+ function secureShuffle(chars) {
117
+ for (let index = chars.length - 1; index > 0; index -= 1) {
118
+ const swapIndex = crypto.randomInt(0, index + 1);
119
+ [chars[index], chars[swapIndex]] = [chars[swapIndex], chars[index]];
120
+ }
121
+ }
122
+ function generatePassword(length, symbols) {
123
+ const charsets = [
124
+ PASSWORD_CHARSETS.lower,
125
+ PASSWORD_CHARSETS.upper,
126
+ PASSWORD_CHARSETS.digits,
127
+ ...(symbols ? [PASSWORD_CHARSETS.symbols] : []),
128
+ ];
129
+ const chars = charsets.map((alphabet) => randomChar(alphabet));
130
+ const combinedAlphabet = charsets.join("");
131
+ while (chars.length < length) {
132
+ chars.push(randomChar(combinedAlphabet));
133
+ }
134
+ secureShuffle(chars);
135
+ return chars.join("");
136
+ }
6
137
  exports.credentialToolDefinitions = [
7
138
  {
8
139
  tool: {
@@ -44,12 +175,65 @@ exports.credentialToolDefinitions = [
44
175
  },
45
176
  summaryKeys: ["domain"],
46
177
  },
178
+ {
179
+ tool: {
180
+ type: "function",
181
+ function: {
182
+ name: "credential_generate_password",
183
+ description: "Generate a strong password for a new account. Use it to complete signup, then immediately call credential_store with the exact accepted password so the vault becomes the source of truth.",
184
+ parameters: {
185
+ type: "object",
186
+ properties: {
187
+ domain: {
188
+ type: "string",
189
+ description: "Domain this password will be used for (e.g. 'airbnb.com')",
190
+ },
191
+ length: {
192
+ type: "integer",
193
+ description: "Optional password length. Defaults to 24. Allowed range: 12 to 128.",
194
+ },
195
+ symbols: {
196
+ type: "boolean",
197
+ description: "Whether to include punctuation symbols. Defaults to true.",
198
+ },
199
+ },
200
+ required: ["domain"],
201
+ },
202
+ },
203
+ },
204
+ handler: async (args) => {
205
+ let domain = "";
206
+ try {
207
+ domain = requireTrimmedText(args.domain, "domain");
208
+ const length = parsePasswordLength(args.length);
209
+ const symbols = parseSymbolsFlag(args.symbols);
210
+ (0, runtime_1.emitNervesEvent)({
211
+ component: "repertoire",
212
+ event: "repertoire.credential_tool_call",
213
+ message: "credential_generate_password invoked",
214
+ meta: { tool: "credential_generate_password", domain, length, symbols },
215
+ });
216
+ const password = generatePassword(length, symbols);
217
+ return JSON.stringify({
218
+ domain,
219
+ password,
220
+ length,
221
+ symbols,
222
+ nextStep: "Use this password for signup, then call credential_store with the exact accepted password.",
223
+ }, null, 2);
224
+ }
225
+ catch (err) {
226
+ return `Credential password generation error: ${sanitizeCredentialToolError(err)}`;
227
+ }
228
+ },
229
+ summaryKeys: ["domain", "length", "symbols"],
230
+ },
47
231
  {
48
232
  tool: {
49
233
  type: "function",
50
234
  function: {
51
235
  name: "credential_store",
52
- description: "Store credentials the agent acquired (e.g. during sign-up). The password is accepted in args because the model generated it. Stored passwords are never returned later — only metadata is visible.",
236
+ description: "Store credentials the agent acquired or just used successfully during signup. Prefer credential_generate_password for new passwords, then call this tool once the site accepts the exact password. Stored passwords are never returned later — only metadata is visible.",
53
237
  parameters: {
54
238
  type: "object",
55
239
  properties: {
@@ -75,6 +259,10 @@ exports.credentialToolDefinitions = [
75
259
  },
76
260
  },
77
261
  handler: async (args) => {
262
+ let domain = "";
263
+ let username = "";
264
+ let password = "";
265
+ let notes;
78
266
  (0, runtime_1.emitNervesEvent)({
79
267
  component: "repertoire",
80
268
  event: "repertoire.credential_tool_call",
@@ -82,17 +270,21 @@ exports.credentialToolDefinitions = [
82
270
  meta: { tool: "credential_store", domain: args.domain },
83
271
  });
84
272
  try {
273
+ domain = requireTrimmedText(args.domain, "domain");
274
+ username = requireTrimmedText(args.username, "username");
275
+ password = requireNonBlankSecret(args.password, "password");
276
+ notes = optionalTrimmedText(args.notes, "notes");
85
277
  const store = (0, credential_access_1.getCredentialStore)();
86
- await store.store(args.domain, {
87
- username: args.username,
88
- password: args.password,
89
- notes: args.notes,
278
+ await store.store(domain, {
279
+ username,
280
+ password,
281
+ notes,
90
282
  });
91
- return `Credentials stored for "${args.domain}".`;
283
+ return `Credentials stored and verified for "${domain}".`;
92
284
  }
93
285
  catch (err) {
94
286
  /* v8 ignore next -- defensive: store.store wraps errors @preserve */
95
- return `Credential store error: ${err instanceof Error ? err.message : String(err)}`;
287
+ return `Credential store error: ${sanitizeCredentialToolError(err, [password, username, notes])}`;
96
288
  }
97
289
  },
98
290
  summaryKeys: ["domain"],
@@ -23,6 +23,10 @@ const runtime_1 = require("../nerves/runtime");
23
23
  function profileKey(friendId) {
24
24
  return `user-profile/${friendId}`;
25
25
  }
26
+ function isMissingUserProfileError(err) {
27
+ const message = err instanceof Error ? err.message : String(err);
28
+ return /no credential found/i.test(message) || /field "password" not found/i.test(message);
29
+ }
26
30
  // ---------------------------------------------------------------------------
27
31
  // CRUD operations
28
32
  // ---------------------------------------------------------------------------
@@ -47,7 +51,8 @@ async function storeUserProfile(friendId, profile, store) {
47
51
  }
48
52
  /**
49
53
  * Retrieve the full user profile for a friend.
50
- * Returns null if no profile exists or if the stored data is invalid.
54
+ * Returns null if no profile exists. Throws if the stored data is malformed
55
+ * or the vault cannot be read.
51
56
  */
52
57
  async function getUserProfile(friendId, store) {
53
58
  (0, runtime_1.emitNervesEvent)({
@@ -59,11 +64,19 @@ async function getUserProfile(friendId, store) {
59
64
  try {
60
65
  const raw = await store.getRawSecret(profileKey(friendId), "password");
61
66
  const parsed = JSON.parse(raw);
67
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
68
+ throw new Error(`stored user profile for ${friendId} is malformed`);
69
+ }
62
70
  return parsed;
63
- /* v8 ignore next 2 -- platform-dependent v8 branch counting on catch @preserve */
64
71
  }
65
- catch {
66
- return null;
72
+ catch (err) {
73
+ if (err instanceof SyntaxError) {
74
+ throw new Error(`stored user profile for ${friendId} is malformed`);
75
+ }
76
+ if (isMissingUserProfileError(err)) {
77
+ return null;
78
+ }
79
+ throw err;
67
80
  }
68
81
  }
69
82
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.401",
3
+ "version": "0.1.0-alpha.403",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -46,6 +46,13 @@ Follow this pattern for every page interaction:
46
46
  5. Wait for redirect, then verify login succeeded via snapshot
47
47
  6. NEVER hardcode credentials -- use `credential_get` to retrieve login info
48
48
 
49
+ ### Sign-up Flows
50
+ 1. Use `credential_generate_password` to mint a strong password for the target domain
51
+ 2. Fill the signup form with that password
52
+ 3. If the site rejects the password policy, generate a new one that matches the site rules
53
+ 4. Once the site accepts the exact password, call `credential_store` immediately
54
+ 5. Do not claim a new credential is saved until `credential_store` succeeds
55
+
49
56
  ### Search Forms (Hotels, Flights, Rentals)
50
57
  1. Navigate to the search page
51
58
  2. Snapshot to identify input fields
@@ -98,7 +105,7 @@ The stealth configuration handles most fingerprinting automatically. Additionall
98
105
  - **CAPTCHAs**: Stop and ask the user to solve manually.
99
106
  - **Stale elements**: Re-snapshot the page and retry the interaction.
100
107
  - **Blocked/403**: The site may have detected automation. Wait 30 seconds and try with a different approach (e.g., direct URL instead of navigation).
101
- - **Session expired**: Re-login using stored stored credentials.
108
+ - **Session expired**: Re-login using stored credentials.
102
109
 
103
110
  ## Human Confirmation Gates
104
111
 
@@ -74,10 +74,13 @@ Present top 3-5 options comparing:
74
74
 
75
75
  Credentials are managed through the credential access layer, which stores
76
76
  agent-owned secrets in the agent's Bitwarden/Vaultwarden credential vault.
77
- Raw passwords never enter model context.
77
+ Stored passwords stay in the vault. A brand-new signup password may enter
78
+ model context briefly when the agent explicitly generates or types it during
79
+ an interactive sign-up flow.
78
80
 
79
81
  - Use `credential_get` to check what credentials exist for a domain (metadata only, never passwords)
80
- - Use `credential_store` to save credentials the agent acquired (e.g., during sign-up for a service)
82
+ - Use `credential_generate_password` to mint a strong password for a new sign-up
83
+ - Use `credential_store` to save credentials the agent acquired after the site accepts them
81
84
  - The credential gateway automatically injects secrets into API requests via `getRawSecret()`
82
85
  - Use browser-navigation skill form patterns for entering credentials during interactive sessions
83
86
 
@@ -85,7 +88,7 @@ Raw passwords never enter model context.
85
88
  - Agent-owned credentials live in the agent's Bitwarden/Vaultwarden vault
86
89
  - Travel credentials such as Duffel and Stripe are ordinary vault credential items
87
90
  - The agent can sign up for services and store its own credentials
88
- - Stored passwords are never returned to the model — only metadata (domain, username, notes)
91
+ - Existing stored passwords are never returned to the model — only metadata (domain, username, notes)
89
92
 
90
93
  ### Post-Booking
91
94
  - Save confirmation details (confirmation number, dates, hotel name, airline, booking reference)
@@ -117,9 +120,10 @@ Track and reference these travel preferences:
117
120
  - `travel_advisory` - US State Dept advisory by country code
118
121
  - `geocode_search` - Location/POI search with coordinates
119
122
  - `credential_get` - Check credential metadata for a domain (never returns passwords)
120
- - `credential_store` - Store credentials the agent acquired (family trust, confirmation required)
123
+ - `credential_generate_password` - Generate a strong password for a new sign-up (family trust)
124
+ - `credential_store` - Store credentials the agent acquired (family trust)
121
125
  - `credential_list` - List stored credential domains
122
- - `credential_delete` - Delete stored credentials (family trust, confirmation required)
126
+ - `credential_delete` - Delete stored credentials (family trust)
123
127
 
124
128
  ### MCP Tools (when configured)
125
129
  - Browser tools via `@playwright/mcp` - see `browser-navigation` skill