@ouro.bot/cli 0.1.0-alpha.231 → 0.1.0-alpha.234
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 +35 -3
- package/dist/heart/core.js +1 -1
- package/dist/heart/daemon/daemon.js +2 -3
- package/dist/heart/identity.js +14 -1
- package/dist/heart/mcp/mcp-server.js +45 -2
- 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-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 +1 -1
- package/dist/senses/cli.js +1 -1
- package/dist/senses/inner-dialog.js +1 -1
- 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
|
@@ -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
|
];
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.vaultToolDefinitions = void 0;
|
|
37
|
+
const crypto = __importStar(require("node:crypto"));
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const identity_1 = require("../heart/identity");
|
|
41
|
+
const vault_setup_1 = require("./vault-setup");
|
|
42
|
+
const runtime_1 = require("../nerves/runtime");
|
|
43
|
+
exports.vaultToolDefinitions = [
|
|
44
|
+
{
|
|
45
|
+
tool: {
|
|
46
|
+
type: "function",
|
|
47
|
+
function: {
|
|
48
|
+
name: "vault_setup",
|
|
49
|
+
description: "Set up the agent's credential vault. Creates a Bitwarden account on the configured Vaultwarden server. One-time operation — idempotent if vault already exists.",
|
|
50
|
+
parameters: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
handler: async () => {
|
|
57
|
+
const agentName = (0, identity_1.getAgentName)();
|
|
58
|
+
(0, runtime_1.emitNervesEvent)({
|
|
59
|
+
component: "repertoire",
|
|
60
|
+
event: "repertoire.vault_tool_call",
|
|
61
|
+
message: "vault_setup invoked",
|
|
62
|
+
meta: { tool: "vault_setup", agentName },
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
const config = (0, identity_1.loadAgentConfig)();
|
|
66
|
+
const { email, serverUrl } = (0, identity_1.resolveVaultConfig)(agentName, config.vault);
|
|
67
|
+
// Generate a strong random master password (32 bytes -> base64)
|
|
68
|
+
const masterPassword = crypto.randomBytes(32).toString("base64");
|
|
69
|
+
// Store master password in secrets.json BEFORE creating the account
|
|
70
|
+
// so it's not lost if the process crashes after registration
|
|
71
|
+
const secretsPath = (0, identity_1.getAgentSecretsPath)(agentName);
|
|
72
|
+
const secretsDir = path.dirname(secretsPath);
|
|
73
|
+
if (!fs.existsSync(secretsDir)) {
|
|
74
|
+
fs.mkdirSync(secretsDir, { recursive: true, mode: 0o700 });
|
|
75
|
+
}
|
|
76
|
+
let secrets = {};
|
|
77
|
+
if (fs.existsSync(secretsPath)) {
|
|
78
|
+
try {
|
|
79
|
+
secrets = JSON.parse(fs.readFileSync(secretsPath, "utf-8"));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// If secrets.json is corrupt, start fresh but don't lose the file
|
|
83
|
+
secrets = {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
secrets.vault = {
|
|
87
|
+
...(typeof secrets.vault === "object" && secrets.vault !== null ? secrets.vault : {}),
|
|
88
|
+
masterPassword,
|
|
89
|
+
email,
|
|
90
|
+
serverUrl,
|
|
91
|
+
};
|
|
92
|
+
fs.writeFileSync(secretsPath, JSON.stringify(secrets, null, 2) + "\n", {
|
|
93
|
+
mode: 0o600,
|
|
94
|
+
});
|
|
95
|
+
// Create the vault account
|
|
96
|
+
const result = await (0, vault_setup_1.createVaultAccount)(agentName, serverUrl, email, masterPassword);
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
return `Vault setup failed: ${result.error}`;
|
|
99
|
+
}
|
|
100
|
+
return `Vault created at ${serverUrl} for ${email}. Master password stored in secrets.json.`;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
/* v8 ignore next -- defensive: error handling @preserve */
|
|
104
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
105
|
+
return `Vault setup error: ${reason}`;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
summaryKeys: [],
|
|
109
|
+
},
|
|
110
|
+
];
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.surfaceToolDef = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
|
|
4
|
+
exports.resetMcpDefinitions = resetMcpDefinitions;
|
|
4
5
|
exports.getToolsForChannel = getToolsForChannel;
|
|
5
6
|
exports.isConfirmationRequired = isConfirmationRequired;
|
|
6
7
|
exports.isConfirmationAlwaysRequired = isConfirmationAlwaysRequired;
|
|
@@ -16,6 +17,7 @@ const runtime_1 = require("../nerves/runtime");
|
|
|
16
17
|
const guardrails_1 = require("./guardrails");
|
|
17
18
|
const identity_1 = require("../heart/identity");
|
|
18
19
|
const tools_surface_1 = require("./tools-surface");
|
|
20
|
+
const mcp_tools_1 = require("./mcp-tools");
|
|
19
21
|
function safeGetAgentRoot() {
|
|
20
22
|
try {
|
|
21
23
|
return (0, identity_1.getAgentRoot)();
|
|
@@ -36,6 +38,13 @@ var tools_surface_2 = require("./tools-surface");
|
|
|
36
38
|
Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: function () { return tools_surface_2.surfaceToolDef; } });
|
|
37
39
|
// All tool definitions in a single registry
|
|
38
40
|
const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, tools_surface_1.surfaceToolDefinition];
|
|
41
|
+
// MCP tool definitions — populated each time getToolsForChannel() is called with an mcpManager.
|
|
42
|
+
// Kept separate from allDefinitions so execTool/isConfirmation* can find them.
|
|
43
|
+
let mcpDefinitions = [];
|
|
44
|
+
/** Exported for testing — reset the MCP definitions cache. */
|
|
45
|
+
function resetMcpDefinitions() {
|
|
46
|
+
mcpDefinitions = [];
|
|
47
|
+
}
|
|
39
48
|
function baseToolsForCapabilities() {
|
|
40
49
|
// Use baseToolDefinitions at call time so dynamically-added tools are included
|
|
41
50
|
return tools_base_1.baseToolDefinitions.map((d) => d.tool);
|
|
@@ -66,7 +75,7 @@ function filterByCapability(toolList, providerCapabilities) {
|
|
|
66
75
|
// Teams/integration tools are included only if their integration is in availableIntegrations.
|
|
67
76
|
// When toolPreferences is provided, matching preferences are appended to tool descriptions.
|
|
68
77
|
// When providerCapabilities is provided, tools with requiredCapability are filtered.
|
|
69
|
-
function getToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities) {
|
|
78
|
+
function getToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities, mcpManager) {
|
|
70
79
|
const baseTools = baseToolsForCapabilities();
|
|
71
80
|
const bluebubblesTools = capabilities?.channel === "bluebubbles"
|
|
72
81
|
? tools_bluebubbles_1.bluebubblesToolDefinitions.map((d) => d.tool)
|
|
@@ -98,18 +107,29 @@ function getToolsForChannel(capabilities, toolPreferences, _context, providerCap
|
|
|
98
107
|
result = [...baseTools, ...bluebubblesTools, ...enrichedIntegrationTools];
|
|
99
108
|
}
|
|
100
109
|
}
|
|
110
|
+
// Append first-class MCP tools when mcpManager is provided
|
|
111
|
+
if (mcpManager) {
|
|
112
|
+
mcpDefinitions = (0, mcp_tools_1.mcpToolsAsDefinitions)(mcpManager);
|
|
113
|
+
const mcpSchemas = mcpDefinitions.map((d) => d.tool);
|
|
114
|
+
result = [...result, ...mcpSchemas];
|
|
115
|
+
}
|
|
101
116
|
return filterByCapability(result, providerCapabilities);
|
|
102
117
|
}
|
|
118
|
+
// Look up a tool definition from the combined registry (native + MCP).
|
|
119
|
+
function findDefinition(toolName) {
|
|
120
|
+
return allDefinitions.find((d) => d.tool.function.name === toolName)
|
|
121
|
+
?? mcpDefinitions.find((d) => d.tool.function.name === toolName);
|
|
122
|
+
}
|
|
103
123
|
// Check whether a tool requires user confirmation before execution.
|
|
104
124
|
// Reads from ToolDefinition.confirmationRequired instead of a separate Set.
|
|
105
125
|
function isConfirmationRequired(toolName) {
|
|
106
|
-
const def =
|
|
126
|
+
const def = findDefinition(toolName);
|
|
107
127
|
return def?.confirmationRequired === true;
|
|
108
128
|
}
|
|
109
129
|
// Check whether a tool's confirmation is non-bypassable (cannot be skipped by skipConfirmation).
|
|
110
130
|
// Used for T2 config proposals that must always require operator approval.
|
|
111
131
|
function isConfirmationAlwaysRequired(toolName) {
|
|
112
|
-
const def =
|
|
132
|
+
const def = findDefinition(toolName);
|
|
113
133
|
return def?.confirmationAlwaysRequired === true;
|
|
114
134
|
}
|
|
115
135
|
function normalizeGuardArgs(_name, args) {
|
|
@@ -122,8 +142,8 @@ async function execTool(name, args, ctx) {
|
|
|
122
142
|
message: "tool execution started",
|
|
123
143
|
meta: { name, ...(name === "shell" && args.command ? { command: args.command } : {}) },
|
|
124
144
|
});
|
|
125
|
-
// Look up from combined registry
|
|
126
|
-
const def =
|
|
145
|
+
// Look up from combined registry (native + MCP)
|
|
146
|
+
const def = findDefinition(name);
|
|
127
147
|
if (!def) {
|
|
128
148
|
(0, runtime_1.emitNervesEvent)({
|
|
129
149
|
level: "error",
|
|
@@ -135,10 +155,13 @@ async function execTool(name, args, ctx) {
|
|
|
135
155
|
return `unknown: ${name}`;
|
|
136
156
|
}
|
|
137
157
|
// Guardrail check: structural + trust-level
|
|
158
|
+
const mcpDef = mcpDefinitions.find((d) => d.tool.function.name === name);
|
|
138
159
|
const guardContext = {
|
|
139
160
|
readPaths: tools_base_1.editFileReadTracker,
|
|
140
161
|
trustLevel: ctx?.context?.friend?.trustLevel,
|
|
141
162
|
agentRoot: safeGetAgentRoot(),
|
|
163
|
+
...(mcpDef?.mcpServer ? { mcpServerName: mcpDef.mcpServer } : {}),
|
|
164
|
+
...(ctx?.context?.isGroupChat !== undefined ? { isGroupChat: (ctx?.context).isGroupChat } : {}),
|
|
142
165
|
};
|
|
143
166
|
const guardArgs = normalizeGuardArgs(name, args);
|
|
144
167
|
const guardResult = (0, guardrails_1.guardInvocation)(name, guardArgs, guardContext);
|
|
@@ -194,7 +217,7 @@ function summarizeUnknownArgs(args) {
|
|
|
194
217
|
return summarizeKeyValues(args, keys);
|
|
195
218
|
}
|
|
196
219
|
function summarizeArgs(name, args) {
|
|
197
|
-
const def =
|
|
220
|
+
const def = findDefinition(name);
|
|
198
221
|
if (def && def.summaryKeys !== undefined) {
|
|
199
222
|
return summarizeKeyValues(args, def.summaryKeys);
|
|
200
223
|
}
|
|
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
exports.TRAVEL_ADVISORY_RSS_URL = void 0;
|
|
12
12
|
exports.getWeather = getWeather;
|
|
13
13
|
exports.getWeatherByCity = getWeatherByCity;
|
|
14
|
+
exports.isoToCountryName = isoToCountryName;
|
|
14
15
|
exports.parseAdvisoryLevel = parseAdvisoryLevel;
|
|
15
16
|
exports.parseCountryName = parseCountryName;
|
|
16
17
|
exports.parseAdvisoryText = parseAdvisoryText;
|
|
@@ -69,6 +70,49 @@ function parseWeatherResponse(data) {
|
|
|
69
70
|
* Returns XML/RSS with per-country advisory items.
|
|
70
71
|
*/
|
|
71
72
|
exports.TRAVEL_ADVISORY_RSS_URL = "https://travel.state.gov/_res/rss/TAsTWs.xml";
|
|
73
|
+
/**
|
|
74
|
+
* Mapping of ISO 3166-1 alpha-2 codes to country names, ONLY for codes
|
|
75
|
+
* where ISO differs from FIPS 10-4 (which the State Dept RSS feed uses).
|
|
76
|
+
*
|
|
77
|
+
* The RSS feed Country-Tag uses FIPS codes. Callers typically pass ISO codes.
|
|
78
|
+
* When the codes match (e.g. AF for Afghanistan), no lookup is needed.
|
|
79
|
+
* This table handles the ~30 codes where they diverge.
|
|
80
|
+
*/
|
|
81
|
+
const ISO_FIPS_DIVERGENCE = {
|
|
82
|
+
AT: "Austria", // FIPS: AU
|
|
83
|
+
AU: "Australia", // FIPS: AS
|
|
84
|
+
BD: "Bangladesh", // FIPS: BG
|
|
85
|
+
BN: "Brunei", // FIPS: BX
|
|
86
|
+
BO: "Bolivia", // FIPS: BL
|
|
87
|
+
CH: "Switzerland", // FIPS: SZ
|
|
88
|
+
CZ: "Czechia", // FIPS: EZ
|
|
89
|
+
DE: "Germany", // FIPS: GM
|
|
90
|
+
ES: "Spain", // FIPS: SP
|
|
91
|
+
FI: "Finland", // FIPS: FI → same, but title might say "Finland"
|
|
92
|
+
GQ: "Equatorial Guinea", // FIPS: EK
|
|
93
|
+
HR: "Croatia", // FIPS: HR → same
|
|
94
|
+
IE: "Ireland", // FIPS: EI
|
|
95
|
+
KP: "North Korea", // FIPS: KN
|
|
96
|
+
KR: "South Korea", // FIPS: KS
|
|
97
|
+
LT: "Lithuania", // FIPS: LH
|
|
98
|
+
MM: "Burma", // FIPS: BM (State Dept uses "Burma (Myanmar)")
|
|
99
|
+
NO: "Norway", // FIPS: NO → same
|
|
100
|
+
PH: "Philippines", // FIPS: RP
|
|
101
|
+
RO: "Romania", // FIPS: RO → same
|
|
102
|
+
SE: "Sweden", // FIPS: SW
|
|
103
|
+
TL: "Timor-Leste", // FIPS: TT
|
|
104
|
+
TW: "Taiwan", // FIPS: TW → same
|
|
105
|
+
VA: "Holy See", // FIPS: VT
|
|
106
|
+
YE: "Yemen", // FIPS: YM
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Look up the expected country name for an ISO alpha-2 code,
|
|
110
|
+
* but ONLY when that code diverges from the FIPS code used in the RSS feed.
|
|
111
|
+
* Returns undefined for codes where ISO == FIPS (no title fallback needed).
|
|
112
|
+
*/
|
|
113
|
+
function isoToCountryName(isoCode) {
|
|
114
|
+
return ISO_FIPS_DIVERGENCE[isoCode.toUpperCase()];
|
|
115
|
+
}
|
|
72
116
|
/**
|
|
73
117
|
* Parse advisory level (1-4) from an RSS item title like
|
|
74
118
|
* "Afghanistan - Level 4: Do Not Travel"
|
|
@@ -103,31 +147,53 @@ async function getTravelAdvisory(countryCode) {
|
|
|
103
147
|
throw new Error(`Travel advisory API error: ${res.status} ${res.statusText}`);
|
|
104
148
|
}
|
|
105
149
|
const xml = await res.text();
|
|
150
|
+
const upperCode = countryCode.toUpperCase();
|
|
151
|
+
// When the caller's ISO code diverges from FIPS, we need a title-based fallback.
|
|
152
|
+
const expectedCountryName = isoToCountryName(upperCode);
|
|
106
153
|
// Parse RSS items via regex (no XML parser dependency needed).
|
|
107
154
|
// Each <item> contains <title>, <pubDate>, and <category domain="Country-Tag">.
|
|
108
155
|
const itemPattern = /<item>([\s\S]*?)<\/item>/g;
|
|
156
|
+
let titleFallbackMatch = null;
|
|
109
157
|
let match;
|
|
110
158
|
while ((match = itemPattern.exec(xml)) !== null) {
|
|
111
159
|
const block = match[1];
|
|
112
|
-
// Extract country tag from <category domain="Country-Tag">XX</category>
|
|
113
|
-
const tagMatch = block.match(/<category\s+domain="Country-Tag">(.*?)<\/category>/);
|
|
114
|
-
const tag = tagMatch?.[1]?.trim().toUpperCase();
|
|
115
|
-
// The RSS feed uses FIPS country codes (e.g. "AF" for Afghanistan),
|
|
116
|
-
// which sometimes differ from ISO 3166. Try exact match first.
|
|
117
|
-
if (tag !== countryCode.toUpperCase())
|
|
118
|
-
continue;
|
|
119
160
|
const titleMatch = block.match(/<title>(.*?)<\/title>/);
|
|
120
161
|
const title = titleMatch?.[1] ?? "";
|
|
121
162
|
const pubDateMatch = block.match(/<pubDate>(.*?)<\/pubDate>/);
|
|
122
163
|
const pubDate = pubDateMatch?.[1]?.trim() ?? "";
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
// Extract country tag from <category domain="Country-Tag">XX</category>
|
|
165
|
+
const tagMatch = block.match(/<category\s+domain="Country-Tag">(.*?)<\/category>/);
|
|
166
|
+
const tag = tagMatch?.[1]?.trim().toUpperCase();
|
|
167
|
+
// Primary match: FIPS tag matches the requested code directly.
|
|
168
|
+
// Skip this path when the caller's ISO code is known to diverge from FIPS —
|
|
169
|
+
// e.g. ISO "ES" = Spain, but FIPS "ES" = El Salvador. In that case, only
|
|
170
|
+
// the title-based fallback should match.
|
|
171
|
+
if (tag === upperCode && !expectedCountryName) {
|
|
172
|
+
return {
|
|
173
|
+
countryCode,
|
|
174
|
+
countryName: parseCountryName(title),
|
|
175
|
+
advisoryLevel: parseAdvisoryLevel(title),
|
|
176
|
+
advisoryText: parseAdvisoryText(title),
|
|
177
|
+
lastUpdated: pubDate,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Title-based match: when ISO differs from FIPS, match by country name.
|
|
181
|
+
// Uses startsWith to handle names like "Burma (Myanmar)" matching "Burma".
|
|
182
|
+
if (expectedCountryName && !titleFallbackMatch) {
|
|
183
|
+
const titleCountry = parseCountryName(title);
|
|
184
|
+
if (titleCountry.toLowerCase().startsWith(expectedCountryName.toLowerCase())) {
|
|
185
|
+
titleFallbackMatch = {
|
|
186
|
+
countryCode,
|
|
187
|
+
countryName: titleCountry,
|
|
188
|
+
advisoryLevel: parseAdvisoryLevel(title),
|
|
189
|
+
advisoryText: parseAdvisoryText(title),
|
|
190
|
+
lastUpdated: pubDate,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
130
194
|
}
|
|
195
|
+
if (titleFallbackMatch)
|
|
196
|
+
return titleFallbackMatch;
|
|
131
197
|
throw new Error(`No travel advisory found for country code "${countryCode}"`);
|
|
132
198
|
});
|
|
133
199
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vault setup module — Bitwarden/Vaultwarden account creation.
|
|
4
|
+
*
|
|
5
|
+
* Implements the Bitwarden registration protocol using Node.js crypto:
|
|
6
|
+
* - PBKDF2-SHA256 for master key derivation
|
|
7
|
+
* - HKDF-SHA256 for key stretching
|
|
8
|
+
* - AES-256-CBC for symmetric key protection
|
|
9
|
+
* - RSA-2048 keypair for asymmetric encryption
|
|
10
|
+
*
|
|
11
|
+
* All crypto follows the Bitwarden security whitepaper:
|
|
12
|
+
* https://bitwarden.com/help/bitwarden-security-white-paper/
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.deriveMasterKey = deriveMasterKey;
|
|
49
|
+
exports.deriveMasterPasswordHash = deriveMasterPasswordHash;
|
|
50
|
+
exports.deriveStretchedMasterKey = deriveStretchedMasterKey;
|
|
51
|
+
exports.makeProtectedSymmetricKey = makeProtectedSymmetricKey;
|
|
52
|
+
exports.createVaultAccount = createVaultAccount;
|
|
53
|
+
const crypto = __importStar(require("node:crypto"));
|
|
54
|
+
const runtime_1 = require("../nerves/runtime");
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Crypto primitives
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Derive the master key from password and email using PBKDF2-SHA256.
|
|
60
|
+
* Email is lowercased and used as the salt per Bitwarden spec.
|
|
61
|
+
*/
|
|
62
|
+
function deriveMasterKey(password, email, iterations) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
crypto.pbkdf2(password, email.toLowerCase(), iterations, 32, "sha256", (err, key) => {
|
|
65
|
+
/* v8 ignore next -- defensive: pbkdf2 rejects on invalid input @preserve */
|
|
66
|
+
if (err)
|
|
67
|
+
reject(err);
|
|
68
|
+
else
|
|
69
|
+
resolve(key);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Derive the master password hash: PBKDF2-SHA256(masterKey, password, 1 iteration).
|
|
75
|
+
* This hash is sent to the server for authentication — it never sees the master key.
|
|
76
|
+
*/
|
|
77
|
+
function deriveMasterPasswordHash(masterKey, password) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
crypto.pbkdf2(masterKey, password, 1, 32, "sha256", (err, hash) => {
|
|
80
|
+
/* v8 ignore next -- defensive: pbkdf2 rejects on invalid input @preserve */
|
|
81
|
+
if (err)
|
|
82
|
+
reject(err);
|
|
83
|
+
else
|
|
84
|
+
resolve(hash.toString("base64"));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Stretch the master key using HKDF-Expand-only (RFC 5869 §2.3) to produce a 64-byte key.
|
|
90
|
+
* First 32 bytes = encryption key, last 32 bytes = MAC key.
|
|
91
|
+
*
|
|
92
|
+
* CRITICAL: Bitwarden uses HKDF-Expand ONLY (no Extract step).
|
|
93
|
+
* Node.js crypto.hkdfSync() does Extract+Expand which produces DIFFERENT output.
|
|
94
|
+
* Reference: https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-crypto/src/util.rs
|
|
95
|
+
* Bitwarden calls Hkdf::<Sha256>::from_prk(masterKey).expand(info, output) — Expand only.
|
|
96
|
+
*/
|
|
97
|
+
function deriveStretchedMasterKey(masterKey) {
|
|
98
|
+
const encKey = hkdfExpandOnly(masterKey, "enc", 32);
|
|
99
|
+
const macKey = hkdfExpandOnly(masterKey, "mac", 32);
|
|
100
|
+
return Buffer.concat([encKey, macKey]);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* HKDF-Expand only (RFC 5869 §2.3) — no Extract step.
|
|
104
|
+
* Matches Bitwarden's Hkdf::from_prk(prk).expand(info).
|
|
105
|
+
*/
|
|
106
|
+
function hkdfExpandOnly(prk, info, length) {
|
|
107
|
+
const hashLen = 32; // SHA-256
|
|
108
|
+
const n = Math.ceil(length / hashLen);
|
|
109
|
+
let okm = Buffer.alloc(0);
|
|
110
|
+
let t = Buffer.alloc(0);
|
|
111
|
+
for (let i = 1; i <= n; i++) {
|
|
112
|
+
t = crypto.createHmac("sha256", prk)
|
|
113
|
+
.update(Buffer.concat([t, Buffer.from(info, "utf8"), Buffer.from([i])]))
|
|
114
|
+
.digest();
|
|
115
|
+
okm = Buffer.concat([okm, t]);
|
|
116
|
+
}
|
|
117
|
+
return okm.subarray(0, length);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Encrypt data with AES-256-CBC and HMAC-SHA256 MAC.
|
|
121
|
+
* Returns a Bitwarden "type 2" cipherstring: "2.<iv>|<ct>|<mac>"
|
|
122
|
+
*/
|
|
123
|
+
function encryptWithStretchedKey(data, stretchedKey) {
|
|
124
|
+
const encKey = stretchedKey.subarray(0, 32);
|
|
125
|
+
const macKey = stretchedKey.subarray(32, 64);
|
|
126
|
+
const iv = crypto.randomBytes(16);
|
|
127
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", encKey, iv);
|
|
128
|
+
const ct = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
129
|
+
// MAC covers iv + ct
|
|
130
|
+
const mac = crypto.createHmac("sha256", macKey)
|
|
131
|
+
.update(iv)
|
|
132
|
+
.update(ct)
|
|
133
|
+
.digest();
|
|
134
|
+
return `2.${iv.toString("base64")}|${ct.toString("base64")}|${mac.toString("base64")}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Generate a 64-byte symmetric key, encrypt it with the stretched master key.
|
|
138
|
+
* Returns the "protected symmetric key" cipherstring.
|
|
139
|
+
*/
|
|
140
|
+
function makeProtectedSymmetricKey(stretchedMasterKey) {
|
|
141
|
+
const symKey = crypto.randomBytes(64);
|
|
142
|
+
return encryptWithStretchedKey(symKey, stretchedMasterKey);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Generate an RSA-2048 keypair.
|
|
146
|
+
* Returns { publicKey: base64-DER, privateKeyDer: Buffer }.
|
|
147
|
+
*/
|
|
148
|
+
function generateRsaKeypair() {
|
|
149
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
150
|
+
modulusLength: 2048,
|
|
151
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
152
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
publicKeyB64: publicKey.toString("base64"),
|
|
156
|
+
privateKeyDer: privateKey,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Registration
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
const KDF_PBKDF2 = 0;
|
|
163
|
+
const KDF_ITERATIONS = 600000;
|
|
164
|
+
/**
|
|
165
|
+
* Create a Bitwarden account on the configured Vaultwarden server.
|
|
166
|
+
* Uses the Bitwarden registration API with standard KDF implementation.
|
|
167
|
+
*/
|
|
168
|
+
async function createVaultAccount(agentName, serverUrl, email, masterPassword) {
|
|
169
|
+
(0, runtime_1.emitNervesEvent)({
|
|
170
|
+
event: "repertoire.vault_setup_start",
|
|
171
|
+
component: "repertoire",
|
|
172
|
+
message: `creating vault account for ${agentName}`,
|
|
173
|
+
meta: { agentName, serverUrl, email },
|
|
174
|
+
});
|
|
175
|
+
try {
|
|
176
|
+
// Step 1: Derive keys
|
|
177
|
+
const masterKey = await deriveMasterKey(masterPassword, email, KDF_ITERATIONS);
|
|
178
|
+
const masterPasswordHash = await deriveMasterPasswordHash(masterKey, masterPassword);
|
|
179
|
+
const stretchedKey = deriveStretchedMasterKey(masterKey);
|
|
180
|
+
// Step 2: Generate symmetric key (64 bytes = 32 enc + 32 mac), encrypt with stretched key
|
|
181
|
+
const symKey = crypto.randomBytes(64);
|
|
182
|
+
const protectedSymKey = encryptWithStretchedKey(symKey, stretchedKey);
|
|
183
|
+
// Step 3: Generate RSA keypair, encrypt private key with the symmetric key
|
|
184
|
+
const { publicKeyB64, privateKeyDer } = generateRsaKeypair();
|
|
185
|
+
const encryptedPrivateKey = encryptWithStretchedKey(privateKeyDer, symKey);
|
|
186
|
+
// Step 4: POST registration
|
|
187
|
+
const res = await fetch(`${serverUrl}/api/accounts/register`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
name: agentName,
|
|
192
|
+
email,
|
|
193
|
+
masterPasswordHash,
|
|
194
|
+
masterPasswordHint: null,
|
|
195
|
+
key: protectedSymKey,
|
|
196
|
+
kdf: KDF_PBKDF2,
|
|
197
|
+
kdfIterations: KDF_ITERATIONS,
|
|
198
|
+
keys: {
|
|
199
|
+
publicKey: publicKeyB64,
|
|
200
|
+
encryptedPrivateKey,
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
let errorDetail;
|
|
206
|
+
try {
|
|
207
|
+
const body = await res.json();
|
|
208
|
+
errorDetail = body.message ?? `HTTP ${res.status} ${res.statusText}`;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
errorDetail = `HTTP ${res.status} ${res.statusText}`;
|
|
212
|
+
}
|
|
213
|
+
(0, runtime_1.emitNervesEvent)({
|
|
214
|
+
level: "error",
|
|
215
|
+
event: "repertoire.vault_setup_error",
|
|
216
|
+
component: "repertoire",
|
|
217
|
+
message: `vault registration failed: ${errorDetail}`,
|
|
218
|
+
meta: { agentName, serverUrl, email, reason: errorDetail },
|
|
219
|
+
});
|
|
220
|
+
return { success: false, email, serverUrl, error: errorDetail };
|
|
221
|
+
}
|
|
222
|
+
(0, runtime_1.emitNervesEvent)({
|
|
223
|
+
event: "repertoire.vault_setup_end",
|
|
224
|
+
component: "repertoire",
|
|
225
|
+
message: `vault account created for ${agentName}`,
|
|
226
|
+
meta: { agentName, serverUrl, email },
|
|
227
|
+
});
|
|
228
|
+
return { success: true, email, serverUrl };
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
232
|
+
(0, runtime_1.emitNervesEvent)({
|
|
233
|
+
level: "error",
|
|
234
|
+
event: "repertoire.vault_setup_error",
|
|
235
|
+
component: "repertoire",
|
|
236
|
+
message: `vault setup failed: ${reason}`,
|
|
237
|
+
meta: { agentName, serverUrl, email, reason },
|
|
238
|
+
});
|
|
239
|
+
return { success: false, email, serverUrl, error: reason };
|
|
240
|
+
}
|
|
241
|
+
}
|