@ouro.bot/cli 0.1.0-alpha.227 → 0.1.0-alpha.228
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 +7 -1
- package/dist/heart/auth/auth-flow.js +3 -0
- package/dist/heart/config.js +4 -0
- package/dist/repertoire/api-client.js +12 -15
- package/dist/repertoire/credential-access.js +449 -0
- package/dist/repertoire/guardrails.js +12 -12
- package/dist/repertoire/tools-base.js +2 -2
- package/dist/repertoire/tools-credential.js +184 -0
- package/dist/repertoire/travel-api-client.js +5 -15
- package/package.json +1 -1
- package/skills/browser-navigation.md +3 -3
- package/skills/travel-planning.md +23 -28
- package/dist/repertoire/bitwarden-client.js +0 -290
- package/dist/repertoire/tools-vault.js +0 -185
package/changelog.json
CHANGED
|
@@ -1,10 +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.228",
|
|
6
|
+
"changes": [
|
|
7
|
+
"refactor(repertoire): credential access layer with encrypted store, vault config types, golden path integration test"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.227",
|
|
6
12
|
"changes": [
|
|
7
|
-
"fix: direct filesystem name resolution in sendProactiveBlueBubblesMessageToSession
|
|
13
|
+
"fix: direct filesystem name resolution in sendProactiveBlueBubblesMessageToSession"
|
|
8
14
|
]
|
|
9
15
|
},
|
|
10
16
|
{
|
package/dist/heart/config.js
CHANGED
|
@@ -117,6 +117,9 @@ const DEFAULT_SECRETS_TEMPLATE = {
|
|
|
117
117
|
webhookPath: "/bluebubbles-webhook",
|
|
118
118
|
requestTimeoutMs: 30000,
|
|
119
119
|
},
|
|
120
|
+
vault: {
|
|
121
|
+
masterPassword: "",
|
|
122
|
+
},
|
|
120
123
|
integrations: {
|
|
121
124
|
perplexityApiKey: "",
|
|
122
125
|
openaiEmbeddingsApiKey: "",
|
|
@@ -138,6 +141,7 @@ function defaultRuntimeConfig() {
|
|
|
138
141
|
teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
|
|
139
142
|
bluebubbles: { ...DEFAULT_SECRETS_TEMPLATE.bluebubbles },
|
|
140
143
|
bluebubblesChannel: { ...DEFAULT_SECRETS_TEMPLATE.bluebubblesChannel },
|
|
144
|
+
vault: { ...DEFAULT_SECRETS_TEMPLATE.vault },
|
|
141
145
|
integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
|
|
142
146
|
};
|
|
143
147
|
}
|
|
@@ -10,37 +10,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
10
10
|
exports.apiRequest = apiRequest;
|
|
11
11
|
const api_error_1 = require("../heart/api-error");
|
|
12
12
|
const runtime_1 = require("../nerves/runtime");
|
|
13
|
-
const
|
|
13
|
+
const credential_access_1 = require("./credential-access");
|
|
14
14
|
/**
|
|
15
15
|
* Execute an authenticated API request with standard logging and error handling.
|
|
16
16
|
* Returns the response body as a pretty-printed JSON string.
|
|
17
17
|
*/
|
|
18
18
|
async function apiRequest(options) {
|
|
19
|
-
const { baseUrl, method, path, token, clientName, serviceLabel, connectionName, body, extraHeaders, contentType, eventMeta,
|
|
20
|
-
// Resolve token from
|
|
19
|
+
const { baseUrl, method, path, token, clientName, serviceLabel, connectionName, body, extraHeaders, contentType, eventMeta, credentialDomain, credentialField, } = options;
|
|
20
|
+
// Resolve token from credential store if credentialDomain is provided
|
|
21
21
|
let resolvedToken = token;
|
|
22
|
-
if (
|
|
22
|
+
if (credentialDomain) {
|
|
23
23
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
return (0, api_error_1.handleApiError)(new Error("Vault is locked — connect the vault first"), serviceLabel, connectionName);
|
|
27
|
-
}
|
|
28
|
-
resolvedToken = await client.getRawSecret(vaultKey, vaultField ?? "apiKey");
|
|
24
|
+
const store = (0, credential_access_1.getCredentialStore)();
|
|
25
|
+
resolvedToken = await store.getRawSecret(credentialDomain, credentialField ?? "password");
|
|
29
26
|
(0, runtime_1.emitNervesEvent)({
|
|
30
|
-
event: "client.
|
|
27
|
+
event: "client.credential_fetch",
|
|
31
28
|
component: "clients",
|
|
32
|
-
message: `fetched credential from
|
|
33
|
-
meta: { client: clientName,
|
|
29
|
+
message: `fetched credential from store for ${serviceLabel}`,
|
|
30
|
+
meta: { client: clientName, credentialDomain },
|
|
34
31
|
});
|
|
35
32
|
}
|
|
36
33
|
catch (err) {
|
|
37
34
|
(0, runtime_1.emitNervesEvent)({
|
|
38
35
|
level: "error",
|
|
39
|
-
event: "client.
|
|
36
|
+
event: "client.credential_error",
|
|
40
37
|
component: "clients",
|
|
41
|
-
message: `failed to fetch
|
|
38
|
+
message: `failed to fetch credential for ${serviceLabel}`,
|
|
42
39
|
/* v8 ignore next -- defensive: getRawSecret throws Error instances @preserve */
|
|
43
|
-
meta: { client: clientName,
|
|
40
|
+
meta: { client: clientName, credentialDomain, reason: err instanceof Error ? err.message : String(err) },
|
|
44
41
|
});
|
|
45
42
|
return (0, api_error_1.handleApiError)(err, serviceLabel, connectionName);
|
|
46
43
|
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Credential access layer — store-agnostic credential management.
|
|
4
|
+
*
|
|
5
|
+
* Two backends, one interface:
|
|
6
|
+
* - BuiltInCredentialStore: AES-256-GCM encrypted files, zero npm deps
|
|
7
|
+
* - AacCredentialStore: delegates to the `aac` CLI (Bitwarden Agent Access)
|
|
8
|
+
*
|
|
9
|
+
* Raw secrets are NEVER returned to callers except via getRawSecret(),
|
|
10
|
+
* which is reserved for internal use by the credential gateway (apiRequest).
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.AacCredentialStore = exports.BuiltInCredentialStore = void 0;
|
|
47
|
+
exports.domainToSlug = domainToSlug;
|
|
48
|
+
exports.getCredentialStore = getCredentialStore;
|
|
49
|
+
exports.resetCredentialStore = resetCredentialStore;
|
|
50
|
+
const crypto = __importStar(require("node:crypto"));
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
const os = __importStar(require("node:os"));
|
|
54
|
+
const node_child_process_1 = require("node:child_process");
|
|
55
|
+
const runtime_1 = require("../nerves/runtime");
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
/** Convert a domain string to a filesystem-safe slug. */
|
|
60
|
+
function domainToSlug(domain) {
|
|
61
|
+
return domain
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
64
|
+
.replace(/^-+|-+$/g, "");
|
|
65
|
+
}
|
|
66
|
+
class BuiltInCredentialStore {
|
|
67
|
+
vaultDir;
|
|
68
|
+
keyPath;
|
|
69
|
+
masterKey = null;
|
|
70
|
+
constructor(agentName, vaultDir, keyDir) {
|
|
71
|
+
const homeBase = process.env.WEBSITE_SITE_NAME ? "/home" : os.homedir();
|
|
72
|
+
this.vaultDir = vaultDir ?? path.join(homeBase, "AgentBundles", `${agentName}.ouro`, "vault");
|
|
73
|
+
const effectiveKeyDir = keyDir ?? path.join(homeBase, ".agentsecrets", agentName);
|
|
74
|
+
this.keyPath = path.join(effectiveKeyDir, "vault.key");
|
|
75
|
+
}
|
|
76
|
+
isReady() {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
async get(domain) {
|
|
80
|
+
(0, runtime_1.emitNervesEvent)({
|
|
81
|
+
event: "repertoire.credential_get_start",
|
|
82
|
+
component: "repertoire",
|
|
83
|
+
message: `getting credential for ${domain}`,
|
|
84
|
+
meta: { domain },
|
|
85
|
+
});
|
|
86
|
+
const stored = this.readEncrypted(domain);
|
|
87
|
+
if (!stored) {
|
|
88
|
+
(0, runtime_1.emitNervesEvent)({
|
|
89
|
+
event: "repertoire.credential_get_end",
|
|
90
|
+
component: "repertoire",
|
|
91
|
+
message: `no credential found for ${domain}`,
|
|
92
|
+
meta: { domain, found: false },
|
|
93
|
+
});
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
(0, runtime_1.emitNervesEvent)({
|
|
97
|
+
event: "repertoire.credential_get_end",
|
|
98
|
+
component: "repertoire",
|
|
99
|
+
message: `credential found for ${domain}`,
|
|
100
|
+
meta: { domain, found: true },
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
domain: stored.domain,
|
|
104
|
+
username: stored.username,
|
|
105
|
+
notes: stored.notes,
|
|
106
|
+
createdAt: stored.createdAt,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async getRawSecret(domain, field) {
|
|
110
|
+
const stored = this.readEncrypted(domain);
|
|
111
|
+
if (!stored) {
|
|
112
|
+
(0, runtime_1.emitNervesEvent)({
|
|
113
|
+
level: "error",
|
|
114
|
+
event: "repertoire.credential_get_error",
|
|
115
|
+
component: "repertoire",
|
|
116
|
+
message: `no credential found for domain "${domain}"`,
|
|
117
|
+
meta: { domain, field, reason: `no credential found for domain "${domain}"` },
|
|
118
|
+
});
|
|
119
|
+
throw new Error(`no credential found for domain "${domain}"`);
|
|
120
|
+
}
|
|
121
|
+
const value = stored[field];
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
(0, runtime_1.emitNervesEvent)({
|
|
124
|
+
level: "error",
|
|
125
|
+
event: "repertoire.credential_get_error",
|
|
126
|
+
component: "repertoire",
|
|
127
|
+
message: `field "${field}" not found for domain "${domain}"`,
|
|
128
|
+
meta: { domain, field, reason: `field "${field}" not found for domain "${domain}"` },
|
|
129
|
+
});
|
|
130
|
+
throw new Error(`field "${field}" not found for domain "${domain}"`);
|
|
131
|
+
}
|
|
132
|
+
return String(value);
|
|
133
|
+
}
|
|
134
|
+
async store(domain, data) {
|
|
135
|
+
(0, runtime_1.emitNervesEvent)({
|
|
136
|
+
event: "repertoire.credential_store_start",
|
|
137
|
+
component: "repertoire",
|
|
138
|
+
message: `storing credential for ${domain}`,
|
|
139
|
+
meta: { domain },
|
|
140
|
+
});
|
|
141
|
+
this.ensureDirs();
|
|
142
|
+
const key = this.getOrCreateKey();
|
|
143
|
+
const credential = {
|
|
144
|
+
domain,
|
|
145
|
+
username: data.username,
|
|
146
|
+
password: data.password,
|
|
147
|
+
notes: data.notes,
|
|
148
|
+
createdAt: new Date().toISOString(),
|
|
149
|
+
};
|
|
150
|
+
const plaintext = JSON.stringify(credential);
|
|
151
|
+
const iv = crypto.randomBytes(12);
|
|
152
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
153
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
154
|
+
const tag = cipher.getAuthTag();
|
|
155
|
+
const payload = {
|
|
156
|
+
iv: iv.toString("hex"),
|
|
157
|
+
tag: tag.toString("hex"),
|
|
158
|
+
data: encrypted.toString("hex"),
|
|
159
|
+
};
|
|
160
|
+
const slug = domainToSlug(domain);
|
|
161
|
+
const encPath = path.join(this.vaultDir, `${slug}.enc`);
|
|
162
|
+
fs.writeFileSync(encPath, JSON.stringify(payload), "utf-8");
|
|
163
|
+
(0, runtime_1.emitNervesEvent)({
|
|
164
|
+
event: "repertoire.credential_store_end",
|
|
165
|
+
component: "repertoire",
|
|
166
|
+
message: `credential stored for ${domain}`,
|
|
167
|
+
meta: { domain },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async list() {
|
|
171
|
+
(0, runtime_1.emitNervesEvent)({
|
|
172
|
+
event: "repertoire.credential_list_start",
|
|
173
|
+
component: "repertoire",
|
|
174
|
+
message: "listing credentials",
|
|
175
|
+
meta: {},
|
|
176
|
+
});
|
|
177
|
+
if (!fs.existsSync(this.vaultDir)) {
|
|
178
|
+
(0, runtime_1.emitNervesEvent)({
|
|
179
|
+
event: "repertoire.credential_list_end",
|
|
180
|
+
component: "repertoire",
|
|
181
|
+
message: "credential list complete (no vault dir)",
|
|
182
|
+
meta: { count: 0 },
|
|
183
|
+
});
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const files = fs.readdirSync(this.vaultDir).filter((f) => f.endsWith(".enc"));
|
|
187
|
+
const results = [];
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
const slug = file.replace(/\.enc$/, "");
|
|
190
|
+
let stored = null;
|
|
191
|
+
try {
|
|
192
|
+
stored = this.readEncryptedBySlug(slug);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Skip corrupt/unreadable files
|
|
196
|
+
}
|
|
197
|
+
if (stored) {
|
|
198
|
+
results.push({
|
|
199
|
+
domain: stored.domain,
|
|
200
|
+
username: stored.username,
|
|
201
|
+
notes: stored.notes,
|
|
202
|
+
createdAt: stored.createdAt,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
(0, runtime_1.emitNervesEvent)({
|
|
207
|
+
event: "repertoire.credential_list_end",
|
|
208
|
+
component: "repertoire",
|
|
209
|
+
message: "credential list complete",
|
|
210
|
+
meta: { count: results.length },
|
|
211
|
+
});
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
214
|
+
async delete(domain) {
|
|
215
|
+
(0, runtime_1.emitNervesEvent)({
|
|
216
|
+
event: "repertoire.credential_delete_start",
|
|
217
|
+
component: "repertoire",
|
|
218
|
+
message: `deleting credential for ${domain}`,
|
|
219
|
+
meta: { domain },
|
|
220
|
+
});
|
|
221
|
+
const slug = domainToSlug(domain);
|
|
222
|
+
const encPath = path.join(this.vaultDir, `${slug}.enc`);
|
|
223
|
+
if (!fs.existsSync(encPath)) {
|
|
224
|
+
(0, runtime_1.emitNervesEvent)({
|
|
225
|
+
event: "repertoire.credential_delete_end",
|
|
226
|
+
component: "repertoire",
|
|
227
|
+
message: `no credential to delete for ${domain}`,
|
|
228
|
+
meta: { domain, deleted: false },
|
|
229
|
+
});
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
fs.unlinkSync(encPath);
|
|
233
|
+
(0, runtime_1.emitNervesEvent)({
|
|
234
|
+
event: "repertoire.credential_delete_end",
|
|
235
|
+
component: "repertoire",
|
|
236
|
+
message: `credential deleted for ${domain}`,
|
|
237
|
+
meta: { domain, deleted: true },
|
|
238
|
+
});
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
// --- Private ---
|
|
242
|
+
ensureDirs() {
|
|
243
|
+
if (!fs.existsSync(this.vaultDir)) {
|
|
244
|
+
fs.mkdirSync(this.vaultDir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
const keyDir = path.dirname(this.keyPath);
|
|
247
|
+
if (!fs.existsSync(keyDir)) {
|
|
248
|
+
fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
getOrCreateKey() {
|
|
252
|
+
if (this.masterKey)
|
|
253
|
+
return this.masterKey;
|
|
254
|
+
if (fs.existsSync(this.keyPath)) {
|
|
255
|
+
this.masterKey = fs.readFileSync(this.keyPath);
|
|
256
|
+
return this.masterKey;
|
|
257
|
+
}
|
|
258
|
+
// Generate new 256-bit key
|
|
259
|
+
const key = crypto.randomBytes(32);
|
|
260
|
+
fs.writeFileSync(this.keyPath, key, { mode: 0o600 });
|
|
261
|
+
this.masterKey = key;
|
|
262
|
+
return key;
|
|
263
|
+
}
|
|
264
|
+
readEncrypted(domain) {
|
|
265
|
+
return this.readEncryptedBySlug(domainToSlug(domain));
|
|
266
|
+
}
|
|
267
|
+
readEncryptedBySlug(slug) {
|
|
268
|
+
const encPath = path.join(this.vaultDir, `${slug}.enc`);
|
|
269
|
+
if (!fs.existsSync(encPath))
|
|
270
|
+
return null;
|
|
271
|
+
const key = this.getOrCreateKey();
|
|
272
|
+
const raw = fs.readFileSync(encPath, "utf-8");
|
|
273
|
+
const payload = JSON.parse(raw);
|
|
274
|
+
const iv = Buffer.from(payload.iv, "hex");
|
|
275
|
+
const tag = Buffer.from(payload.tag, "hex");
|
|
276
|
+
const encrypted = Buffer.from(payload.data, "hex");
|
|
277
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
278
|
+
decipher.setAuthTag(tag);
|
|
279
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
280
|
+
return JSON.parse(decrypted.toString("utf-8"));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
exports.BuiltInCredentialStore = BuiltInCredentialStore;
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// AacCredentialStore — delegates to `aac` CLI
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
function execAac(args) {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
(0, node_child_process_1.execFile)("aac", args, { timeout: 10_000 }, (err, stdout) => {
|
|
290
|
+
if (err) {
|
|
291
|
+
reject(new Error(`aac CLI error: ${err.message}`));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
resolve(stdout);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
class AacCredentialStore {
|
|
299
|
+
aacAvailable = null;
|
|
300
|
+
isReady() {
|
|
301
|
+
// Synchronous check — returns last-known state or false if unchecked
|
|
302
|
+
return this.aacAvailable === true;
|
|
303
|
+
}
|
|
304
|
+
async checkReady() {
|
|
305
|
+
try {
|
|
306
|
+
const stdout = await execAac(["connections", "list"]);
|
|
307
|
+
const parsed = JSON.parse(stdout);
|
|
308
|
+
this.aacAvailable = Array.isArray(parsed) && parsed.length > 0;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
this.aacAvailable = false;
|
|
312
|
+
}
|
|
313
|
+
return this.aacAvailable;
|
|
314
|
+
}
|
|
315
|
+
async get(domain) {
|
|
316
|
+
(0, runtime_1.emitNervesEvent)({
|
|
317
|
+
event: "repertoire.credential_get_start",
|
|
318
|
+
component: "repertoire",
|
|
319
|
+
message: `getting credential via aac for ${domain}`,
|
|
320
|
+
meta: { domain, backend: "aac" },
|
|
321
|
+
});
|
|
322
|
+
try {
|
|
323
|
+
const stdout = await execAac(["--domain", domain, "--output", "json"]);
|
|
324
|
+
const parsed = JSON.parse(stdout);
|
|
325
|
+
if (!parsed.success) {
|
|
326
|
+
(0, runtime_1.emitNervesEvent)({
|
|
327
|
+
event: "repertoire.credential_get_end",
|
|
328
|
+
component: "repertoire",
|
|
329
|
+
message: `no aac credential for ${domain}`,
|
|
330
|
+
meta: { domain, found: false, backend: "aac" },
|
|
331
|
+
});
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const cred = parsed.credential;
|
|
335
|
+
(0, runtime_1.emitNervesEvent)({
|
|
336
|
+
event: "repertoire.credential_get_end",
|
|
337
|
+
component: "repertoire",
|
|
338
|
+
message: `aac credential found for ${domain}`,
|
|
339
|
+
meta: { domain, found: true, backend: "aac" },
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
domain,
|
|
343
|
+
username: cred.username,
|
|
344
|
+
notes: cred.notes,
|
|
345
|
+
createdAt: cred.createdAt ?? new Date().toISOString(),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
(0, runtime_1.emitNervesEvent)({
|
|
350
|
+
event: "repertoire.credential_get_end",
|
|
351
|
+
component: "repertoire",
|
|
352
|
+
message: `aac credential lookup failed for ${domain}`,
|
|
353
|
+
meta: { domain, found: false, backend: "aac" },
|
|
354
|
+
});
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async getRawSecret(domain, field) {
|
|
359
|
+
const stdout = await execAac(["--domain", domain, "--output", "json"]);
|
|
360
|
+
const parsed = JSON.parse(stdout);
|
|
361
|
+
if (!parsed.success) {
|
|
362
|
+
throw new Error(`aac lookup failed for domain "${domain}": ${parsed.error ?? "unknown error"}`);
|
|
363
|
+
}
|
|
364
|
+
const value = parsed.credential[field];
|
|
365
|
+
if (value === undefined || value === null) {
|
|
366
|
+
throw new Error(`field "${field}" not found for domain "${domain}"`);
|
|
367
|
+
}
|
|
368
|
+
return String(value);
|
|
369
|
+
}
|
|
370
|
+
async store(_domain, _data) {
|
|
371
|
+
throw new Error("store() is not supported in aac mode — aac is read-from-vault only. " +
|
|
372
|
+
"Use BuiltInCredentialStore for agent-owned credentials, or manage vault items " +
|
|
373
|
+
"directly in Bitwarden.");
|
|
374
|
+
}
|
|
375
|
+
async list() {
|
|
376
|
+
(0, runtime_1.emitNervesEvent)({
|
|
377
|
+
event: "repertoire.credential_list_start",
|
|
378
|
+
component: "repertoire",
|
|
379
|
+
message: "listing aac connections",
|
|
380
|
+
meta: { backend: "aac" },
|
|
381
|
+
});
|
|
382
|
+
try {
|
|
383
|
+
const stdout = await execAac(["connections", "list"]);
|
|
384
|
+
const parsed = JSON.parse(stdout);
|
|
385
|
+
const results = (Array.isArray(parsed) ? parsed : []).map((item) => ({
|
|
386
|
+
domain: String(item.domain ?? ""),
|
|
387
|
+
username: item.username ? String(item.username) : undefined,
|
|
388
|
+
notes: item.notes ? String(item.notes) : undefined,
|
|
389
|
+
createdAt: item.createdAt ? String(item.createdAt) : new Date().toISOString(),
|
|
390
|
+
}));
|
|
391
|
+
(0, runtime_1.emitNervesEvent)({
|
|
392
|
+
event: "repertoire.credential_list_end",
|
|
393
|
+
component: "repertoire",
|
|
394
|
+
message: "aac connections listed",
|
|
395
|
+
meta: { backend: "aac", count: results.length },
|
|
396
|
+
});
|
|
397
|
+
return results;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
(0, runtime_1.emitNervesEvent)({
|
|
401
|
+
event: "repertoire.credential_list_end",
|
|
402
|
+
component: "repertoire",
|
|
403
|
+
message: "aac connections list failed",
|
|
404
|
+
meta: { backend: "aac", count: 0 },
|
|
405
|
+
});
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async delete(_domain) {
|
|
410
|
+
throw new Error("delete() is not supported in aac mode — manage vault items directly in Bitwarden.");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
exports.AacCredentialStore = AacCredentialStore;
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Singleton + auto-detection
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
let _store = null;
|
|
418
|
+
/**
|
|
419
|
+
* Get the credential store singleton.
|
|
420
|
+
* Tries aac first (if binary exists and has sessions); falls back to BuiltInCredentialStore.
|
|
421
|
+
*/
|
|
422
|
+
function getCredentialStore() {
|
|
423
|
+
if (_store)
|
|
424
|
+
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
|
+
let agentName;
|
|
429
|
+
try {
|
|
430
|
+
// Dynamic import to avoid circular dependency at module level
|
|
431
|
+
const identity = require("../heart/identity");
|
|
432
|
+
agentName = identity.getAgentName();
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
agentName = "default";
|
|
436
|
+
}
|
|
437
|
+
_store = new BuiltInCredentialStore(agentName);
|
|
438
|
+
(0, runtime_1.emitNervesEvent)({
|
|
439
|
+
event: "repertoire.credential_store_init",
|
|
440
|
+
component: "repertoire",
|
|
441
|
+
message: "credential store initialized",
|
|
442
|
+
meta: { backend: "built-in", agentName },
|
|
443
|
+
});
|
|
444
|
+
return _store;
|
|
445
|
+
}
|
|
446
|
+
/** Reset the singleton — for tests only. */
|
|
447
|
+
function resetCredentialStore() {
|
|
448
|
+
_store = null;
|
|
449
|
+
}
|
|
@@ -282,21 +282,21 @@ function checkWriteTrustGuardrails(toolName, args, context) {
|
|
|
282
282
|
return allow;
|
|
283
283
|
return deny(REASONS.needsTrustForWrite);
|
|
284
284
|
}
|
|
285
|
-
// ---
|
|
286
|
-
//
|
|
287
|
-
const
|
|
288
|
-
//
|
|
289
|
-
const
|
|
285
|
+
// --- credential tool trust gating ---
|
|
286
|
+
// Credential write tools: family only
|
|
287
|
+
const CREDENTIAL_FAMILY_TOOLS = new Set(["credential_store", "credential_delete"]);
|
|
288
|
+
// Credential read tools: friend+
|
|
289
|
+
const CREDENTIAL_TRUSTED_TOOLS = new Set(["credential_get", "credential_list"]);
|
|
290
290
|
// Travel tools: friend+ (weather_lookup accesses vault credentials indirectly;
|
|
291
291
|
// advisory and geocode are public APIs but gated for consistency)
|
|
292
292
|
const TRAVEL_TRUSTED_TOOLS = new Set(["weather_lookup", "travel_advisory", "geocode_search"]);
|
|
293
|
-
function
|
|
294
|
-
if (
|
|
293
|
+
function checkCredentialTrustGuardrails(toolName, context) {
|
|
294
|
+
if (CREDENTIAL_FAMILY_TOOLS.has(toolName)) {
|
|
295
295
|
if (context.trustLevel === "family")
|
|
296
296
|
return allow;
|
|
297
297
|
return deny(REASONS.needsTrust);
|
|
298
298
|
}
|
|
299
|
-
if (
|
|
299
|
+
if (CREDENTIAL_TRUSTED_TOOLS.has(toolName) || TRAVEL_TRUSTED_TOOLS.has(toolName)) {
|
|
300
300
|
if ((0, types_1.isTrustedLevel)(context.trustLevel))
|
|
301
301
|
return allow;
|
|
302
302
|
return deny(REASONS.needsTrust);
|
|
@@ -304,10 +304,10 @@ function checkVaultTrustGuardrails(toolName, context) {
|
|
|
304
304
|
return allow;
|
|
305
305
|
}
|
|
306
306
|
function checkTrustLevelGuardrails(toolName, args, context) {
|
|
307
|
-
//
|
|
308
|
-
const
|
|
309
|
-
if (!
|
|
310
|
-
return
|
|
307
|
+
// Credential tools have their own trust rules that apply at all levels
|
|
308
|
+
const credentialResult = checkCredentialTrustGuardrails(toolName, context);
|
|
309
|
+
if (!credentialResult.allowed)
|
|
310
|
+
return credentialResult;
|
|
311
311
|
// MCP server-specific trust (e.g. browser blocked in group chats) — applies at all trust levels
|
|
312
312
|
if (toolName === "shell") {
|
|
313
313
|
const mcpResult = checkMcpServerTrust(args.command || "", context);
|
|
@@ -9,7 +9,7 @@ const tools_session_1 = require("./tools-session");
|
|
|
9
9
|
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
|
-
const
|
|
12
|
+
const tools_credential_1 = require("./tools-credential");
|
|
13
13
|
const tools_travel_1 = require("./tools-travel");
|
|
14
14
|
// Re-export flow tools for consumers that import them from tools-base
|
|
15
15
|
var tools_flow_1 = require("./tools-flow");
|
|
@@ -34,7 +34,7 @@ exports.baseToolDefinitions = [
|
|
|
34
34
|
...tools_continuity_1.continuityToolDefinitions,
|
|
35
35
|
...tools_config_1.configToolDefinitions,
|
|
36
36
|
...tools_1.codingToolDefinitions,
|
|
37
|
-
...
|
|
37
|
+
...tools_credential_1.credentialToolDefinitions,
|
|
38
38
|
...tools_travel_1.travelToolDefinitions,
|
|
39
39
|
];
|
|
40
40
|
// Convenience array of just the tool schemas (no handler/integration metadata).
|