@ouro.bot/cli 0.1.0-alpha.230 → 0.1.0-alpha.233
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 +34 -2
- package/dist/heart/core.js +1 -1
- package/dist/heart/daemon/daemon.js +2 -3
- package/dist/heart/identity.js +14 -1
- package/dist/mind/prompt.js +2 -19
- package/dist/nerves/coverage/file-completeness.js +1 -0
- package/dist/repertoire/bitwarden-store.js +256 -0
- package/dist/repertoire/credential-access.js +37 -6
- package/dist/repertoire/guardrails.js +20 -2
- package/dist/repertoire/mcp-tools.js +61 -0
- package/dist/repertoire/tools-base.js +2 -0
- package/dist/repertoire/tools-credential.js +0 -2
- package/dist/repertoire/tools-session.js +3 -4
- package/dist/repertoire/tools-surface.js +3 -2
- package/dist/repertoire/tools-vault.js +110 -0
- package/dist/repertoire/tools.js +29 -6
- package/dist/repertoire/travel-api-client.js +80 -14
- package/dist/repertoire/vault-setup.js +241 -0
- package/dist/senses/bluebubbles/index.js +16 -1
- package/dist/senses/cli.js +1 -1
- package/dist/senses/inner-dialog.js +8 -6
- package/dist/senses/shared-turn.js +4 -0
- package/dist/senses/teams.js +1 -1
- package/package.json +1 -1
- package/skills/travel-planning.md +2 -2
package/changelog.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
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.233",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat: first-class MCP tools \u2014 MCP tools now appear in the agent tool list directly (no shell indirection). Agent can call browser_navigate, browser_click etc. as native tools.",
|
|
8
|
+
"fix: daemon MCP pre-init poisoned singleton \u2014 removed eager getSharedMcpManager() at daemon startup that cached null before agent identity was set.",
|
|
9
|
+
"fix: removed dead mcpManager field from BuildSystemOptions \u2014 MCP manager now flows through runAgentOptions.",
|
|
10
|
+
"fix: MCP tool results filter to text-only content types.",
|
|
11
|
+
"includes: vault integration, travel advisory fix, credential access layer, HKDF-Expand crypto fix."
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.1.0-alpha.232",
|
|
16
|
+
"changes": [
|
|
17
|
+
"feat: first-class MCP tools \u2014 MCP server tools now appear in the agent's active tool list (e.g. browser_navigate, duffel_search_flights) and are callable directly by the model, eliminating fragile shell indirection",
|
|
18
|
+
"feat: mcpToolsAsDefinitions() converts McpManager tools to ToolDefinition objects with {server}_{tool} naming",
|
|
19
|
+
"feat: first-class MCP trust gating \u2014 mcpServerName on GuardContext enables per-server trust rules (browser blocked for acquaintance, blocked in group chat)",
|
|
20
|
+
"refactor: removed mcpToolsSection() from system prompt \u2014 MCP tools no longer need prompt documentation",
|
|
21
|
+
"fix: execTool, isConfirmationRequired, summarizeArgs now check combined native+MCP registry"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "0.1.0-alpha.231",
|
|
26
|
+
"changes": [
|
|
27
|
+
"fix: defense-in-depth group chat blocking for proactive BB delivery \u2014 sendProactiveBlueBubblesMessageToSession now rejects group chat keys (;+;) unless intent is explicit_cross_chat (bridge/delegation responses). All upper-layer paths (surface tool, send_message tool, inner-dialog delegation) now filter to DM sessions only for proactive outreach, and pass explicit_cross_chat intent for bridge/delegation returns where group responses are legitimate.",
|
|
28
|
+
"fix: travel advisory now resolves ISO codes that differ from FIPS (ES -> Spain, not El Salvador)",
|
|
29
|
+
"fix: MCP bridge sense now includes MCP tool descriptions in system prompt (browser tools visible)",
|
|
30
|
+
"fix: removed confirmationRequired from credential_store/credential_delete (trust gating sufficient)",
|
|
31
|
+
"feat: vault integration \u2014 Bitwarden/Vaultwarden account creation with PBKDF2/HKDF/AES-256-CBC crypto",
|
|
32
|
+
"feat: vault_setup tool for one-time vault provisioning (family trust gated)",
|
|
33
|
+
"feat: BitwardenCredentialStore wrapping bw CLI for agent-owned vault access"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
4
36
|
{
|
|
5
37
|
"version": "0.1.0-alpha.230",
|
|
6
38
|
"changes": [
|
|
@@ -10,14 +42,14 @@
|
|
|
10
42
|
{
|
|
11
43
|
"version": "0.1.0-alpha.229",
|
|
12
44
|
"changes": [
|
|
13
|
-
"fix: BB session key resolution prefers DM (;-;) over group chat (;+;)
|
|
45
|
+
"fix: BB session key resolution prefers DM (;-;) over group chat (;+;) \u2014 alphabetical sort put group chats first, causing proactive messages to land in group chats instead of personal DMs",
|
|
14
46
|
"chore: remove temporary debug traces (send-message-debug.log, friends.get_called event)"
|
|
15
47
|
]
|
|
16
48
|
},
|
|
17
49
|
{
|
|
18
50
|
"version": "0.1.0-alpha.227",
|
|
19
51
|
"changes": [
|
|
20
|
-
"fix: direct filesystem name resolution in sendProactiveBlueBubblesMessageToSession
|
|
52
|
+
"fix: direct filesystem name resolution in sendProactiveBlueBubblesMessageToSession \u2014 bypass store.get()/listAll() with raw fs reads on friends directory when store lookup fails"
|
|
21
53
|
]
|
|
22
54
|
},
|
|
23
55
|
{
|
package/dist/heart/core.js
CHANGED
|
@@ -481,7 +481,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
481
481
|
}
|
|
482
482
|
catch { /* unsupported */ }
|
|
483
483
|
const toolPreferences = currentContext?.friend?.toolPreferences;
|
|
484
|
-
const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities);
|
|
484
|
+
const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities, options?.mcpManager);
|
|
485
485
|
// Augment tool context with reasoning effort controls from provider
|
|
486
486
|
const augmentedToolContext = options?.toolContext
|
|
487
487
|
? {
|
|
@@ -320,9 +320,8 @@ class OuroDaemon {
|
|
|
320
320
|
/* v8 ignore stop */
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
(0, mcp_manager_1.getSharedMcpManager)().catch(() => { });
|
|
323
|
+
// MCP connections are lazily initialized per-agent during senseTurn
|
|
324
|
+
// (daemon manages multiple agents; agent identity must be set before loading MCP config)
|
|
326
325
|
/* v8 ignore start -- orphan cleanup + pidfile: calls process management functions @preserve */
|
|
327
326
|
killOrphanProcesses();
|
|
328
327
|
/* v8 ignore stop */
|
package/dist/heart/identity.js
CHANGED
|
@@ -33,7 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = exports.PROVIDER_CREDENTIALS = void 0;
|
|
36
|
+
exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_VAULT_SERVER_URL = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = exports.PROVIDER_CREDENTIALS = void 0;
|
|
37
|
+
exports.resolveVaultConfig = resolveVaultConfig;
|
|
37
38
|
exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
|
|
38
39
|
exports.getAgentName = getAgentName;
|
|
39
40
|
exports.getRepoRoot = getRepoRoot;
|
|
@@ -74,6 +75,17 @@ exports.DEFAULT_AGENT_PHRASES = {
|
|
|
74
75
|
tool: ["running tool"],
|
|
75
76
|
followup: ["processing"],
|
|
76
77
|
};
|
|
78
|
+
exports.DEFAULT_VAULT_SERVER_URL = "https://vault.ouro.bot";
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the vault config for an agent, applying defaults.
|
|
81
|
+
* If vault is not configured in agent.json, returns default values.
|
|
82
|
+
*/
|
|
83
|
+
function resolveVaultConfig(agentName, config) {
|
|
84
|
+
return {
|
|
85
|
+
email: config?.email ?? `${agentName}@ouro.bot`,
|
|
86
|
+
serverUrl: config?.serverUrl ?? exports.DEFAULT_VAULT_SERVER_URL,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
77
89
|
exports.DEFAULT_AGENT_SENSES = {
|
|
78
90
|
cli: { enabled: true },
|
|
79
91
|
teams: { enabled: false },
|
|
@@ -403,6 +415,7 @@ function loadAgentConfig() {
|
|
|
403
415
|
senses: normalizeSenses(parsed.senses, configFile),
|
|
404
416
|
mcpServers: parsed.mcpServers,
|
|
405
417
|
phrases: parsed.phrases,
|
|
418
|
+
vault: parsed.vault,
|
|
406
419
|
};
|
|
407
420
|
(0, runtime_1.emitNervesEvent)({
|
|
408
421
|
event: "identity.resolve",
|
package/dist/mind/prompt.js
CHANGED
|
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.resetPsycheCache = resetPsycheCache;
|
|
37
37
|
exports.buildSessionSummary = buildSessionSummary;
|
|
38
38
|
exports.bodyMapSection = bodyMapSection;
|
|
39
|
-
exports.mcpToolsSection = mcpToolsSection;
|
|
40
39
|
exports.runtimeInfoSection = runtimeInfoSection;
|
|
41
40
|
exports.toolRestrictionSection = toolRestrictionSection;
|
|
42
41
|
exports.startOfTurnPacketSection = startOfTurnPacketSection;
|
|
@@ -219,23 +218,8 @@ my bones give me the \`ouro\` cli. always pass \`--agent ${agentName}\`:
|
|
|
219
218
|
|
|
220
219
|
provider/model changes via \`ouro config model\` or \`ouro auth switch\` take effect on the next turn automatically — no restart needed.`;
|
|
221
220
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return "";
|
|
225
|
-
const allTools = mcpManager.listAllTools();
|
|
226
|
-
if (allTools.length === 0)
|
|
227
|
-
return "";
|
|
228
|
-
const lines = [
|
|
229
|
-
`## mcp tools (use ouro mcp call <server> <tool> --args '{...}')`,
|
|
230
|
-
];
|
|
231
|
-
for (const entry of allTools) {
|
|
232
|
-
lines.push(`### ${entry.server}`);
|
|
233
|
-
for (const tool of entry.tools) {
|
|
234
|
-
lines.push(`- ${tool.name}: ${tool.description}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return lines.join("\n");
|
|
238
|
-
}
|
|
221
|
+
// mcpToolsSection removed — MCP tools are now first-class citizens in the tool registry
|
|
222
|
+
// and appear in the model's active tool list directly. No system prompt section needed.
|
|
239
223
|
function readBundleMeta() {
|
|
240
224
|
try {
|
|
241
225
|
const metaPath = path.join((0, identity_1.getAgentRoot)(), "bundle-meta.json");
|
|
@@ -1045,7 +1029,6 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
1045
1029
|
// Group 3: my tools & capabilities
|
|
1046
1030
|
"# my tools & capabilities",
|
|
1047
1031
|
toolsSection(channel, options, context),
|
|
1048
|
-
mcpToolsSection(options?.mcpManager),
|
|
1049
1032
|
reasoningEffortSection(options),
|
|
1050
1033
|
skillsSection(),
|
|
1051
1034
|
toolContractsSection(options),
|
|
@@ -57,6 +57,7 @@ const DISPATCH_EXEMPT_PATTERNS = [
|
|
|
57
57
|
"repertoire/tools-continuity",
|
|
58
58
|
"repertoire/tools-surface",
|
|
59
59
|
"repertoire/tools-config",
|
|
60
|
+
"repertoire/tools-base",
|
|
60
61
|
// CLI sub-modules: cli-exec.ts is the router with emitNervesEvent calls;
|
|
61
62
|
// cli-parse and cli-render are pure functions with no side effects.
|
|
62
63
|
"daemon/cli-parse",
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bitwarden CLI credential store — wraps `bw` CLI for the agent's own vault.
|
|
4
|
+
*
|
|
5
|
+
* Unlike AacCredentialStore (which accesses someone else's vault via approval),
|
|
6
|
+
* this store authenticates directly as the agent using its own master password.
|
|
7
|
+
* The agent owns the vault, so no human-in-the-loop is needed.
|
|
8
|
+
*
|
|
9
|
+
* Requires the `bw` CLI to be installed. Session tokens are cached in-memory.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.BitwardenCredentialStore = void 0;
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const runtime_1 = require("../nerves/runtime");
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// bw CLI wrapper
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function execBw(args, sessionToken) {
|
|
19
|
+
const env = sessionToken
|
|
20
|
+
? { ...process.env, BW_SESSION: sessionToken }
|
|
21
|
+
: process.env;
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
(0, node_child_process_1.execFile)("bw", args, { timeout: 30_000, env }, (err, stdout) => {
|
|
24
|
+
if (err) {
|
|
25
|
+
reject(new Error(`bw CLI error: ${err.message}`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
resolve(stdout);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// BitwardenCredentialStore
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
class BitwardenCredentialStore {
|
|
36
|
+
serverUrl;
|
|
37
|
+
email;
|
|
38
|
+
masterPassword;
|
|
39
|
+
sessionToken = null;
|
|
40
|
+
constructor(serverUrl, email, masterPassword) {
|
|
41
|
+
this.serverUrl = serverUrl;
|
|
42
|
+
this.email = email;
|
|
43
|
+
this.masterPassword = masterPassword;
|
|
44
|
+
}
|
|
45
|
+
isReady() {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Ensure the bw CLI is authenticated and unlocked.
|
|
50
|
+
* Handles three states: logged out → login, locked → unlock, already unlocked → no-op.
|
|
51
|
+
*/
|
|
52
|
+
async login() {
|
|
53
|
+
// Check current status
|
|
54
|
+
let status = {};
|
|
55
|
+
try {
|
|
56
|
+
const raw = await execBw(["status"]);
|
|
57
|
+
status = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// CLI not configured or broken — proceed with full setup
|
|
61
|
+
}
|
|
62
|
+
// Configure server URL if needed (only works when logged out)
|
|
63
|
+
if (status.status === "unauthenticated" || !status.serverUrl) {
|
|
64
|
+
try {
|
|
65
|
+
await execBw(["config", "server", this.serverUrl]);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// "Logout required" means already logged in — that's fine, skip config
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (status.status === "locked") {
|
|
72
|
+
// Already logged in, just needs unlock
|
|
73
|
+
const unlockOutput = await execBw(["unlock", this.masterPassword, "--raw"]);
|
|
74
|
+
this.sessionToken = unlockOutput.trim();
|
|
75
|
+
}
|
|
76
|
+
else if (status.status === "unauthenticated" || !status.status) {
|
|
77
|
+
// Not logged in — full login
|
|
78
|
+
const loginOutput = await execBw(["login", this.email, this.masterPassword, "--raw"]);
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(loginOutput);
|
|
81
|
+
this.sessionToken = parsed.access_token ?? loginOutput.trim();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
this.sessionToken = loginOutput.trim();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Status is "unlocked" — already good, just need the session token
|
|
89
|
+
const unlockOutput = await execBw(["unlock", this.masterPassword, "--raw"]);
|
|
90
|
+
this.sessionToken = unlockOutput.trim();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async ensureSession() {
|
|
94
|
+
if (!this.sessionToken) {
|
|
95
|
+
await this.login();
|
|
96
|
+
}
|
|
97
|
+
/* v8 ignore next -- defensive: login() always sets sessionToken on success @preserve */
|
|
98
|
+
return this.sessionToken ?? undefined;
|
|
99
|
+
}
|
|
100
|
+
async get(domain) {
|
|
101
|
+
(0, runtime_1.emitNervesEvent)({
|
|
102
|
+
event: "repertoire.bw_credential_get_start",
|
|
103
|
+
component: "repertoire",
|
|
104
|
+
message: `getting credential via bw for ${domain}`,
|
|
105
|
+
meta: { domain, backend: "bitwarden" },
|
|
106
|
+
});
|
|
107
|
+
const session = await this.ensureSession();
|
|
108
|
+
const item = await this.findItemByDomain(domain, session);
|
|
109
|
+
if (!item) {
|
|
110
|
+
(0, runtime_1.emitNervesEvent)({
|
|
111
|
+
event: "repertoire.bw_credential_get_end",
|
|
112
|
+
component: "repertoire",
|
|
113
|
+
message: `no bw credential for ${domain}`,
|
|
114
|
+
meta: { domain, found: false, backend: "bitwarden" },
|
|
115
|
+
});
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
(0, runtime_1.emitNervesEvent)({
|
|
119
|
+
event: "repertoire.bw_credential_get_end",
|
|
120
|
+
component: "repertoire",
|
|
121
|
+
message: `bw credential found for ${domain}`,
|
|
122
|
+
meta: { domain, found: true, backend: "bitwarden" },
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
domain: item.name,
|
|
126
|
+
username: item.login?.username,
|
|
127
|
+
notes: item.notes ?? undefined,
|
|
128
|
+
createdAt: item.revisionDate ?? new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async getRawSecret(domain, field) {
|
|
132
|
+
const session = await this.ensureSession();
|
|
133
|
+
const item = await this.findItemByDomain(domain, session);
|
|
134
|
+
if (!item) {
|
|
135
|
+
throw new Error(`no credential found for domain "${domain}"`);
|
|
136
|
+
}
|
|
137
|
+
// Map common field names to bw item structure
|
|
138
|
+
let value;
|
|
139
|
+
if (field === "password") {
|
|
140
|
+
value = item.login?.password;
|
|
141
|
+
}
|
|
142
|
+
else if (field === "username") {
|
|
143
|
+
value = item.login?.username;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
value = item[field];
|
|
147
|
+
}
|
|
148
|
+
if (value === undefined || value === null) {
|
|
149
|
+
throw new Error(`field "${field}" not found for domain "${domain}"`);
|
|
150
|
+
}
|
|
151
|
+
return String(value);
|
|
152
|
+
}
|
|
153
|
+
async store(domain, data) {
|
|
154
|
+
(0, runtime_1.emitNervesEvent)({
|
|
155
|
+
event: "repertoire.bw_credential_store_start",
|
|
156
|
+
component: "repertoire",
|
|
157
|
+
message: `storing credential via bw for ${domain}`,
|
|
158
|
+
meta: { domain, backend: "bitwarden" },
|
|
159
|
+
});
|
|
160
|
+
const session = await this.ensureSession();
|
|
161
|
+
// Create a new login item
|
|
162
|
+
const item = {
|
|
163
|
+
type: 1, // Login type
|
|
164
|
+
name: domain,
|
|
165
|
+
login: {
|
|
166
|
+
username: data.username ?? "",
|
|
167
|
+
password: data.password,
|
|
168
|
+
uris: [{ match: null, uri: `https://${domain}` }],
|
|
169
|
+
},
|
|
170
|
+
notes: data.notes ?? null,
|
|
171
|
+
};
|
|
172
|
+
const encoded = Buffer.from(JSON.stringify(item)).toString("base64");
|
|
173
|
+
await execBw(["create", "item", encoded], session);
|
|
174
|
+
(0, runtime_1.emitNervesEvent)({
|
|
175
|
+
event: "repertoire.bw_credential_store_end",
|
|
176
|
+
component: "repertoire",
|
|
177
|
+
message: `credential stored via bw for ${domain}`,
|
|
178
|
+
meta: { domain, backend: "bitwarden" },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async list() {
|
|
182
|
+
(0, runtime_1.emitNervesEvent)({
|
|
183
|
+
event: "repertoire.bw_credential_list_start",
|
|
184
|
+
component: "repertoire",
|
|
185
|
+
message: "listing bw credentials",
|
|
186
|
+
meta: { backend: "bitwarden" },
|
|
187
|
+
});
|
|
188
|
+
const session = await this.ensureSession();
|
|
189
|
+
try {
|
|
190
|
+
const stdout = await execBw(["list", "items"], session);
|
|
191
|
+
const items = JSON.parse(stdout);
|
|
192
|
+
const results = items.map((item) => ({
|
|
193
|
+
domain: item.name,
|
|
194
|
+
username: item.login?.username,
|
|
195
|
+
notes: item.notes ?? undefined,
|
|
196
|
+
createdAt: item.revisionDate ?? new Date().toISOString(),
|
|
197
|
+
}));
|
|
198
|
+
(0, runtime_1.emitNervesEvent)({
|
|
199
|
+
event: "repertoire.bw_credential_list_end",
|
|
200
|
+
component: "repertoire",
|
|
201
|
+
message: "bw credentials listed",
|
|
202
|
+
meta: { backend: "bitwarden", count: results.length },
|
|
203
|
+
});
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
(0, runtime_1.emitNervesEvent)({
|
|
208
|
+
event: "repertoire.bw_credential_list_end",
|
|
209
|
+
component: "repertoire",
|
|
210
|
+
message: "bw credential list failed",
|
|
211
|
+
meta: { backend: "bitwarden", count: 0 },
|
|
212
|
+
});
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async delete(domain) {
|
|
217
|
+
(0, runtime_1.emitNervesEvent)({
|
|
218
|
+
event: "repertoire.bw_credential_delete_start",
|
|
219
|
+
component: "repertoire",
|
|
220
|
+
message: `deleting credential via bw for ${domain}`,
|
|
221
|
+
meta: { domain, backend: "bitwarden" },
|
|
222
|
+
});
|
|
223
|
+
const session = await this.ensureSession();
|
|
224
|
+
const item = await this.findItemByDomain(domain, session);
|
|
225
|
+
if (!item) {
|
|
226
|
+
(0, runtime_1.emitNervesEvent)({
|
|
227
|
+
event: "repertoire.bw_credential_delete_end",
|
|
228
|
+
component: "repertoire",
|
|
229
|
+
message: `no bw credential to delete for ${domain}`,
|
|
230
|
+
meta: { domain, deleted: false, backend: "bitwarden" },
|
|
231
|
+
});
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
await execBw(["delete", "item", item.id], session);
|
|
235
|
+
(0, runtime_1.emitNervesEvent)({
|
|
236
|
+
event: "repertoire.bw_credential_delete_end",
|
|
237
|
+
component: "repertoire",
|
|
238
|
+
message: `credential deleted via bw for ${domain}`,
|
|
239
|
+
meta: { domain, deleted: true, backend: "bitwarden" },
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
// --- Private ---
|
|
244
|
+
async findItemByDomain(domain, session) {
|
|
245
|
+
try {
|
|
246
|
+
const stdout = await execBw(["list", "items", "--search", domain], session);
|
|
247
|
+
const items = JSON.parse(stdout);
|
|
248
|
+
// Find exact match by name
|
|
249
|
+
return items.find((item) => item.name === domain) ?? items[0] ?? null;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
exports.BitwardenCredentialStore = BitwardenCredentialStore;
|
|
@@ -417,29 +417,60 @@ exports.AacCredentialStore = AacCredentialStore;
|
|
|
417
417
|
let _store = null;
|
|
418
418
|
/**
|
|
419
419
|
* Get the credential store singleton.
|
|
420
|
-
*
|
|
420
|
+
*
|
|
421
|
+
* Priority:
|
|
422
|
+
* 1. BitwardenCredentialStore — if vault config exists in agent.json + secrets.json
|
|
423
|
+
* 2. BuiltInCredentialStore — fallback for agents without a vault (local-only, encrypted files)
|
|
424
|
+
*
|
|
425
|
+
* Future auth providers (1Password, etc.) would be added here as additional backends.
|
|
421
426
|
*/
|
|
422
427
|
function getCredentialStore() {
|
|
423
428
|
if (_store)
|
|
424
429
|
return _store;
|
|
425
|
-
// Default to BuiltInCredentialStore — aac detection is async and
|
|
426
|
-
// would need to be done at startup. For now, always use built-in.
|
|
427
|
-
// Callers can explicitly create AacCredentialStore if needed.
|
|
428
430
|
let agentName;
|
|
431
|
+
let backend = "built-in";
|
|
429
432
|
try {
|
|
430
433
|
// Dynamic import to avoid circular dependency at module level
|
|
431
434
|
const identity = require("../heart/identity");
|
|
432
435
|
agentName = identity.getAgentName();
|
|
436
|
+
// Try to load vault config from agent.json + secrets.json
|
|
437
|
+
const config = identity.loadAgentConfig?.();
|
|
438
|
+
const vaultConfig = identity.resolveVaultConfig?.(agentName, config?.vault);
|
|
439
|
+
const secretsPath = identity.getAgentSecretsPath?.(agentName);
|
|
440
|
+
let vaultSecrets;
|
|
441
|
+
/* v8 ignore start -- requires real agent secrets.json + vault config + bw CLI @preserve */
|
|
442
|
+
if (secretsPath) {
|
|
443
|
+
try {
|
|
444
|
+
const fs = require("fs");
|
|
445
|
+
const secrets = JSON.parse(fs.readFileSync(secretsPath, "utf-8"));
|
|
446
|
+
vaultSecrets = secrets.vault;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// No secrets file or no vault section — fall through to built-in
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const serverUrl = vaultConfig?.serverUrl;
|
|
453
|
+
const email = vaultConfig?.email;
|
|
454
|
+
const masterPassword = vaultSecrets?.masterPassword;
|
|
455
|
+
if (serverUrl && email && masterPassword) {
|
|
456
|
+
const { BitwardenCredentialStore } = require("./bitwarden-store");
|
|
457
|
+
_store = new BitwardenCredentialStore(serverUrl, email, masterPassword);
|
|
458
|
+
backend = "bitwarden";
|
|
459
|
+
}
|
|
460
|
+
/* v8 ignore stop */
|
|
433
461
|
}
|
|
434
462
|
catch {
|
|
435
463
|
agentName = "default";
|
|
436
464
|
}
|
|
437
|
-
|
|
465
|
+
/* v8 ignore next -- false branch only reachable when BitwardenCredentialStore was created above @preserve */
|
|
466
|
+
if (!_store) {
|
|
467
|
+
_store = new BuiltInCredentialStore(agentName);
|
|
468
|
+
}
|
|
438
469
|
(0, runtime_1.emitNervesEvent)({
|
|
439
470
|
event: "repertoire.credential_store_init",
|
|
440
471
|
component: "repertoire",
|
|
441
472
|
message: "credential store initialized",
|
|
442
|
-
meta: { backend
|
|
473
|
+
meta: { backend, agentName: agentName },
|
|
443
474
|
});
|
|
444
475
|
return _store;
|
|
445
476
|
}
|
|
@@ -284,7 +284,7 @@ function checkWriteTrustGuardrails(toolName, args, context) {
|
|
|
284
284
|
}
|
|
285
285
|
// --- credential tool trust gating ---
|
|
286
286
|
// Credential write tools: family only
|
|
287
|
-
const CREDENTIAL_FAMILY_TOOLS = new Set(["credential_store", "credential_delete"]);
|
|
287
|
+
const CREDENTIAL_FAMILY_TOOLS = new Set(["credential_store", "credential_delete", "vault_setup"]);
|
|
288
288
|
// Credential read tools: friend+
|
|
289
289
|
const CREDENTIAL_TRUSTED_TOOLS = new Set(["credential_get", "credential_list"]);
|
|
290
290
|
// Travel tools: friend+ (weather_lookup accesses vault credentials indirectly;
|
|
@@ -303,12 +303,30 @@ function checkCredentialTrustGuardrails(toolName, context) {
|
|
|
303
303
|
}
|
|
304
304
|
return allow;
|
|
305
305
|
}
|
|
306
|
+
function checkFirstClassMcpTrust(context) {
|
|
307
|
+
if (!context.mcpServerName)
|
|
308
|
+
return allow;
|
|
309
|
+
const rules = MCP_SERVER_TRUST[context.mcpServerName];
|
|
310
|
+
if (!rules)
|
|
311
|
+
return allow;
|
|
312
|
+
if (!trustLevelSatisfied(rules.minTrust, context.trustLevel ?? "friend")) {
|
|
313
|
+
return deny(REASONS.needsTrust);
|
|
314
|
+
}
|
|
315
|
+
if (rules.blockGroupChat && context.isGroupChat) {
|
|
316
|
+
return deny("browser tools are only available in 1:1 conversations, not group chats.");
|
|
317
|
+
}
|
|
318
|
+
return allow;
|
|
319
|
+
}
|
|
306
320
|
function checkTrustLevelGuardrails(toolName, args, context) {
|
|
307
321
|
// Credential tools have their own trust rules that apply at all levels
|
|
308
322
|
const credentialResult = checkCredentialTrustGuardrails(toolName, context);
|
|
309
323
|
if (!credentialResult.allowed)
|
|
310
324
|
return credentialResult;
|
|
311
|
-
// MCP
|
|
325
|
+
// First-class MCP tool trust (e.g. browser_navigate) — applies at all trust levels
|
|
326
|
+
const firstClassMcpResult = checkFirstClassMcpTrust(context);
|
|
327
|
+
if (!firstClassMcpResult.allowed)
|
|
328
|
+
return firstClassMcpResult;
|
|
329
|
+
// MCP server-specific trust via shell (e.g. ouro mcp call browser) — applies at all trust levels
|
|
312
330
|
if (toolName === "shell") {
|
|
313
331
|
const mcpResult = checkMcpServerTrust(args.command || "", context);
|
|
314
332
|
if (!mcpResult.allowed)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* First-class MCP tool integration — converts MCP server tools into ToolDefinitions
|
|
4
|
+
* so the model can call them directly without shell indirection.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.mcpToolsAsDefinitions = mcpToolsAsDefinitions;
|
|
8
|
+
const runtime_1 = require("../nerves/runtime");
|
|
9
|
+
/**
|
|
10
|
+
* Convert all tools from an McpManager into ToolDefinition objects.
|
|
11
|
+
* Each tool gets named `{server}_{tool}` (e.g., `browser_navigate`).
|
|
12
|
+
* The handler calls `mcpManager.callTool()` and returns concatenated text content.
|
|
13
|
+
*/
|
|
14
|
+
function mcpToolsAsDefinitions(mcpManager) {
|
|
15
|
+
if (!mcpManager)
|
|
16
|
+
return [];
|
|
17
|
+
return mcpManager.listAllTools().flatMap((entry) => entry.tools.map((tool) => ({
|
|
18
|
+
tool: {
|
|
19
|
+
type: "function",
|
|
20
|
+
function: {
|
|
21
|
+
name: `${entry.server}_${tool.name}`,
|
|
22
|
+
description: tool.description || `MCP tool: ${tool.name} (server: ${entry.server})`,
|
|
23
|
+
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
handler: async (args) => {
|
|
27
|
+
(0, runtime_1.emitNervesEvent)({
|
|
28
|
+
event: "mcp.tool_start",
|
|
29
|
+
component: "repertoire",
|
|
30
|
+
message: `calling MCP tool ${entry.server}/${tool.name}`,
|
|
31
|
+
meta: { server: entry.server, tool: tool.name },
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
const result = await mcpManager.callTool(entry.server, tool.name, args);
|
|
35
|
+
const text = result.content
|
|
36
|
+
.filter((c) => c.type === "text" && c.text)
|
|
37
|
+
.map((c) => c.text)
|
|
38
|
+
.join("");
|
|
39
|
+
(0, runtime_1.emitNervesEvent)({
|
|
40
|
+
event: "mcp.tool_end",
|
|
41
|
+
component: "repertoire",
|
|
42
|
+
message: `MCP tool ${entry.server}/${tool.name} completed`,
|
|
43
|
+
meta: { server: entry.server, tool: tool.name },
|
|
44
|
+
});
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
49
|
+
(0, runtime_1.emitNervesEvent)({
|
|
50
|
+
level: "error",
|
|
51
|
+
event: "mcp.tool_error",
|
|
52
|
+
component: "repertoire",
|
|
53
|
+
message: `MCP tool ${entry.server}/${tool.name} failed: ${reason}`,
|
|
54
|
+
meta: { server: entry.server, tool: tool.name, reason },
|
|
55
|
+
});
|
|
56
|
+
return `[mcp error] ${entry.server}/${tool.name}: ${reason}`;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
mcpServer: entry.server,
|
|
60
|
+
})));
|
|
61
|
+
}
|
|
@@ -10,6 +10,7 @@ const tools_continuity_1 = require("./tools-continuity");
|
|
|
10
10
|
const tools_config_1 = require("./tools-config");
|
|
11
11
|
const tools_1 = require("./coding/tools");
|
|
12
12
|
const tools_credential_1 = require("./tools-credential");
|
|
13
|
+
const tools_vault_1 = require("./tools-vault");
|
|
13
14
|
const tools_travel_1 = require("./tools-travel");
|
|
14
15
|
// Re-export flow tools for consumers that import them from tools-base
|
|
15
16
|
var tools_flow_1 = require("./tools-flow");
|
|
@@ -35,6 +36,7 @@ exports.baseToolDefinitions = [
|
|
|
35
36
|
...tools_config_1.configToolDefinitions,
|
|
36
37
|
...tools_1.codingToolDefinitions,
|
|
37
38
|
...tools_credential_1.credentialToolDefinitions,
|
|
39
|
+
...tools_vault_1.vaultToolDefinitions,
|
|
38
40
|
...tools_travel_1.travelToolDefinitions,
|
|
39
41
|
];
|
|
40
42
|
// Convenience array of just the tool schemas (no handler/integration metadata).
|
|
@@ -95,7 +95,6 @@ exports.credentialToolDefinitions = [
|
|
|
95
95
|
return `Credential store error: ${err instanceof Error ? err.message : String(err)}`;
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
|
-
confirmationRequired: true,
|
|
99
98
|
summaryKeys: ["domain"],
|
|
100
99
|
},
|
|
101
100
|
{
|
|
@@ -178,7 +177,6 @@ exports.credentialToolDefinitions = [
|
|
|
178
177
|
return `Credential delete error: ${err instanceof Error ? err.message : String(err)}`;
|
|
179
178
|
}
|
|
180
179
|
},
|
|
181
|
-
confirmationRequired: true,
|
|
182
180
|
summaryKeys: ["domain"],
|
|
183
181
|
},
|
|
184
182
|
];
|
|
@@ -592,11 +592,10 @@ exports.sessionToolDefinitions = [
|
|
|
592
592
|
const bbDir = path.join(agentRoot, "state", "sessions", friendId, "bluebubbles");
|
|
593
593
|
if (fs.existsSync(bbDir)) {
|
|
594
594
|
const files = fs.readdirSync(bbDir).filter((f) => f.endsWith(".json"));
|
|
595
|
-
//
|
|
595
|
+
// Only use DM sessions (;-;) for proactive delivery — never group chats (;+;)
|
|
596
596
|
const dmFile = files.find((f) => f.includes(";-;"));
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
resolvedKey = chosen.replace(/\.json$/, "");
|
|
597
|
+
if (dmFile) {
|
|
598
|
+
resolvedKey = dmFile.replace(/\.json$/, "");
|
|
600
599
|
}
|
|
601
600
|
}
|
|
602
601
|
}
|
|
@@ -118,6 +118,7 @@ exports.surfaceToolDefinition = {
|
|
|
118
118
|
friendId: bridgeTarget.friendId,
|
|
119
119
|
sessionKey: bridgeTarget.key,
|
|
120
120
|
text: content,
|
|
121
|
+
intent: "explicit_cross_chat",
|
|
121
122
|
});
|
|
122
123
|
if (proactiveResult.delivered) {
|
|
123
124
|
// Inject surfaced content into the target session so it knows what was delivered
|
|
@@ -145,8 +146,8 @@ exports.surfaceToolDefinition = {
|
|
|
145
146
|
// Priority 2: Try proactive delivery first, then queue to freshest session
|
|
146
147
|
const allFriendSessions = (0, session_activity_1.listSessionActivity)({ sessionsDir, friendsDir, agentName, activeThresholdMs: Number.MAX_SAFE_INTEGER })
|
|
147
148
|
.filter((s) => s.friendId === friendId && s.channel !== "inner");
|
|
148
|
-
// 2a: Attempt proactive BB delivery on
|
|
149
|
-
const bbSession = allFriendSessions.find((s) => s.channel === "bluebubbles");
|
|
149
|
+
// 2a: Attempt proactive BB delivery on a DM session (;-; = individual, never ;+; = group)
|
|
150
|
+
const bbSession = allFriendSessions.find((s) => s.channel === "bluebubbles" && s.key.includes(";-;"));
|
|
150
151
|
if (bbSession) {
|
|
151
152
|
const { sendProactiveBlueBubblesMessageToSession } = await Promise.resolve().then(() => __importStar(require("../senses/bluebubbles")));
|
|
152
153
|
const proactiveResult = await sendProactiveBlueBubblesMessageToSession({
|