@ouro.bot/cli 0.1.0-alpha.400 → 0.1.0-alpha.402

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,26 @@
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.402",
6
+ "changes": [
7
+ "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.",
8
+ "`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.",
9
+ "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.",
10
+ "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.",
11
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the agent-saved secret hardening release."
12
+ ]
13
+ },
14
+ {
15
+ "version": "0.1.0-alpha.401",
16
+ "changes": [
17
+ "Bitwarden-backed credential writes now prove success by reading the saved item back immediately and verifying its name, username, password, and notes before returning success.",
18
+ "Post-save verification now prefers `bw get item <id>` when create/edit returns a usable id, falls back cleanly when stdout is malformed or id-less, and retries once when the local Bitwarden session expires mid-verification.",
19
+ "Malformed or invalid `bw get item` responses now fail with short sanitized errors instead of leaving auth and vault save flows in an ambiguous half-success state.",
20
+ "Runtime auth's Bitwarden harness coverage now exercises the same post-save readback path as the real vault store, and the store test suite covers missing items, malformed responses, field-by-field mismatches, and multi-field mismatch guidance.",
21
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault write readback verification release."
22
+ ]
23
+ },
4
24
  {
5
25
  "version": "0.1.0-alpha.400",
6
26
  "changes": [
@@ -184,6 +184,34 @@ function parseBwItems(stdout, context) {
184
184
  throw new Error(`bw CLI error: invalid JSON from ${context}`);
185
185
  }
186
186
  }
187
+ function parseBwItem(stdout, context) {
188
+ let parsed;
189
+ try {
190
+ parsed = JSON.parse(stdout);
191
+ if (!isBwLoginItem(parsed)) {
192
+ throw new Error("expected login item");
193
+ }
194
+ return parsed;
195
+ }
196
+ catch {
197
+ if (parsed !== undefined) {
198
+ throw new Error(`bw CLI error: invalid item from ${context}`);
199
+ }
200
+ throw new Error(`bw CLI error: invalid JSON from ${context}`);
201
+ }
202
+ }
203
+ function parseBwItemId(stdout) {
204
+ try {
205
+ const parsed = JSON.parse(stdout);
206
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
207
+ return null;
208
+ const id = parsed.id;
209
+ return typeof id === "string" && id.trim().length > 0 ? id : null;
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
187
215
  // ---------------------------------------------------------------------------
188
216
  // BitwardenCredentialStore
189
217
  // ---------------------------------------------------------------------------
@@ -387,12 +415,20 @@ class BitwardenCredentialStore {
387
415
  notes: data.notes ?? null,
388
416
  };
389
417
  const encoded = Buffer.from(JSON.stringify(item)).toString("base64");
418
+ let savedItem;
390
419
  if (existing) {
391
- await execBw(["edit", "item", existing.id], session, this.appDataDir, encoded);
420
+ const stdout = await execBw(["edit", "item", existing.id], session, this.appDataDir, encoded);
421
+ const savedItemId = parseBwItemId(stdout) ?? existing.id;
422
+ savedItem = await this.findItemById(savedItemId, session);
392
423
  }
393
424
  else {
394
- await execBw(["create", "item"], session, this.appDataDir, encoded);
425
+ const stdout = await execBw(["create", "item"], session, this.appDataDir, encoded);
426
+ const savedItemId = parseBwItemId(stdout);
427
+ savedItem = savedItemId
428
+ ? await this.findItemById(savedItemId, session)
429
+ : await this.findItemByDomain(domain, session);
395
430
  }
431
+ this.assertStoredCredentialMatches(domain, data, savedItem);
396
432
  });
397
433
  (0, runtime_1.emitNervesEvent)({
398
434
  event: "repertoire.bw_credential_store_end",
@@ -457,5 +493,27 @@ class BitwardenCredentialStore {
457
493
  // Find exact match by name
458
494
  return items.find((item) => item.name === domain) ?? null;
459
495
  }
496
+ async findItemById(id, session) {
497
+ const stdout = await execBw(["get", "item", id], session, this.appDataDir);
498
+ return parseBwItem(stdout, "bw get item");
499
+ }
500
+ assertStoredCredentialMatches(domain, data, item) {
501
+ if (!item) {
502
+ throw new Error(`bw CLI error: credential save verification failed for ${domain}: saved item could not be read back after write`);
503
+ }
504
+ const mismatches = [];
505
+ if (item.name !== domain)
506
+ mismatches.push("name");
507
+ if ((item.login?.username ?? "") !== (data.username ?? ""))
508
+ mismatches.push("username");
509
+ if ((item.login?.password ?? "") !== data.password)
510
+ mismatches.push("password");
511
+ if ((item.notes ?? null) !== (data.notes ?? null))
512
+ mismatches.push("notes");
513
+ if (mismatches.length > 0) {
514
+ const label = mismatches.length === 1 ? "field" : "fields";
515
+ throw new Error(`bw CLI error: credential save verification failed for ${domain}: saved item did not match requested ${label} ${mismatches.join(", ")}`);
516
+ }
517
+ }
460
518
  }
461
519
  exports.BitwardenCredentialStore = BitwardenCredentialStore;
@@ -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.400",
3
+ "version": "0.1.0-alpha.402",
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