@ouro.bot/cli 0.1.0-alpha.401 → 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,16 @@
|
|
|
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
|
+
},
|
|
4
14
|
{
|
|
5
15
|
"version": "0.1.0-alpha.401",
|
|
6
16
|
"changes": [
|
|
@@ -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
|
|
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(
|
|
87
|
-
username
|
|
88
|
-
password
|
|
89
|
-
notes
|
|
278
|
+
await store.store(domain, {
|
|
279
|
+
username,
|
|
280
|
+
password,
|
|
281
|
+
notes,
|
|
90
282
|
});
|
|
91
|
-
return `Credentials stored for "${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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 `
|
|
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
|
-
-
|
|
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
|
-
- `
|
|
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
|
|
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
|