@ouro.bot/cli 0.1.0-alpha.446 → 0.1.0-alpha.448
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 +16 -0
- package/dist/heart/daemon/agent-config-check.js +14 -1
- package/dist/heart/daemon/cli-exec.js +41 -15
- package/dist/heart/daemon/connect-bay.js +51 -8
- package/dist/heart/provider-binding-resolver.js +16 -0
- package/dist/heart/provider-credentials.js +29 -5
- package/dist/heart/provider-visibility.js +4 -2
- package/dist/repertoire/bitwarden-store.js +66 -25
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
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.448",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`ouro status` and agent prompt provider visibility now distinguish \"the daemon has not loaded provider credentials in this process\" from \"the agent vault is missing credentials.\" A saved live check that passed stays ready instead of being downgraded to stale/missing just because status rendering is running in a fresh daemon process.",
|
|
8
|
+
"Provider visibility now carries a safe `not-loaded` credential state and renders it as `checked previously`, preserving the last live-check result without reading or printing secrets during ordinary status/prompt rendering.",
|
|
9
|
+
"Regression coverage locks the installed-product failure shape: an empty in-process provider cache plus ready provider state no longer produces misleading auth repair guidance."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.447",
|
|
14
|
+
"changes": [
|
|
15
|
+
"`ouro connect` now checks only the providers selected for the current agent lanes during preflight, and those live reads avoid mutating the older provider snapshot cache. That keeps the command focused on what this machine actually needs instead of paying whole-vault latency for unrelated provider records.",
|
|
16
|
+
"Structured vault reads for `providers/*` and `runtime/config` now reuse one short-lived Bitwarden item listing per store instance and skip redundant `bw sync` calls while the local Bitwarden cache is fresh, cutting repeated vault startup work without adding any disk credential cache.",
|
|
17
|
+
"The connections screen now lets the fresh live provider check outrank stale local credential visibility, so a provider that just passed appears ready and a provider that just failed shows the real live-check failure instead of falling back to misleading `credentials missing` guidance. `@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the connect preflight latency release."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.446",
|
|
6
22
|
"changes": [
|
|
@@ -330,12 +330,20 @@ function selectedProviderPlan(agentName, state) {
|
|
|
330
330
|
...["outward", "inner"].map((lane) => `- ${laneAudienceLabel(lane)}: ${bindingLabel(state.lanes[lane])}`),
|
|
331
331
|
].join("\n");
|
|
332
332
|
}
|
|
333
|
+
function selectedProvidersForState(state) {
|
|
334
|
+
return [...new Set(["outward", "inner"].map((lane) => state.lanes[lane].provider))];
|
|
335
|
+
}
|
|
333
336
|
function mapVaultRefreshProgress(agentName, onProgress) {
|
|
334
337
|
return (message) => {
|
|
335
338
|
if (message.startsWith("reading vault items for ")) {
|
|
336
339
|
onProgress(`${agentName}: opening saved provider credentials in the vault`);
|
|
337
340
|
return;
|
|
338
341
|
}
|
|
342
|
+
const providerRead = message.match(/^reading ([a-z0-9-]+) credentials\.\.\.$/i);
|
|
343
|
+
if (providerRead) {
|
|
344
|
+
onProgress(`${agentName}: reading saved ${providerRead[1]} credentials`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
339
347
|
if (message === "parsing provider credentials...") {
|
|
340
348
|
onProgress(`${agentName}: organizing saved provider credentials`);
|
|
341
349
|
}
|
|
@@ -381,7 +389,12 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
|
|
|
381
389
|
return { ok: true };
|
|
382
390
|
deps.onProgress?.(selectedProviderPlan(agentName, stateResult.state));
|
|
383
391
|
const ping = deps.pingProvider ?? (await Promise.resolve().then(() => __importStar(require("../provider-ping")))).pingProvider;
|
|
384
|
-
const
|
|
392
|
+
const providers = selectedProvidersForState(stateResult.state);
|
|
393
|
+
const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, {
|
|
394
|
+
...(deps.onProgress ? { onProgress: mapVaultRefreshProgress(agentName, deps.onProgress) } : {}),
|
|
395
|
+
providers,
|
|
396
|
+
skipCache: true,
|
|
397
|
+
});
|
|
385
398
|
const pingGroups = new Map();
|
|
386
399
|
const lanes = ["outward", "inner"];
|
|
387
400
|
for (const lane of lanes) {
|
|
@@ -2060,11 +2060,33 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
2060
2060
|
onProgress?.("loading this machine's settings");
|
|
2061
2061
|
const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
|
|
2062
2062
|
const { teamsEnabled, blueBubblesEnabled } = readConnectBaySenseFlags(agent, deps);
|
|
2063
|
-
let perplexityStatus;
|
|
2064
|
-
let perplexityDetailLines;
|
|
2065
2063
|
const perplexityApiKey = runtimeConfig.ok
|
|
2066
2064
|
? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
|
|
2067
2065
|
: null;
|
|
2066
|
+
const embeddingsApiKey = runtimeConfig.ok
|
|
2067
|
+
? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
|
|
2068
|
+
: null;
|
|
2069
|
+
const shouldVerifyPerplexity = runtimeConfig.ok && !!perplexityApiKey;
|
|
2070
|
+
const shouldVerifyEmbeddings = runtimeConfig.ok && !!embeddingsApiKey;
|
|
2071
|
+
let perplexityVerification;
|
|
2072
|
+
let embeddingsVerification;
|
|
2073
|
+
if (shouldVerifyPerplexity && shouldVerifyEmbeddings) {
|
|
2074
|
+
onProgress?.("verifying Perplexity search and memory embeddings");
|
|
2075
|
+
[perplexityVerification, embeddingsVerification] = await Promise.all([
|
|
2076
|
+
(0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey),
|
|
2077
|
+
(0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey),
|
|
2078
|
+
]);
|
|
2079
|
+
}
|
|
2080
|
+
else if (shouldVerifyPerplexity) {
|
|
2081
|
+
onProgress?.("verifying Perplexity search");
|
|
2082
|
+
perplexityVerification = await (0, runtime_capability_check_1.verifyPerplexityCapability)(perplexityApiKey);
|
|
2083
|
+
}
|
|
2084
|
+
else if (shouldVerifyEmbeddings) {
|
|
2085
|
+
onProgress?.("verifying memory embeddings");
|
|
2086
|
+
embeddingsVerification = await (0, runtime_capability_check_1.verifyEmbeddingsCapability)(embeddingsApiKey);
|
|
2087
|
+
}
|
|
2088
|
+
let perplexityStatus;
|
|
2089
|
+
let perplexityDetailLines;
|
|
2068
2090
|
if (!runtimeConfig.ok) {
|
|
2069
2091
|
perplexityStatus = runtimeConfigReadStatus(runtimeConfig);
|
|
2070
2092
|
perplexityDetailLines = [];
|
|
@@ -2074,16 +2096,15 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
2074
2096
|
perplexityDetailLines = ["no API key saved yet"];
|
|
2075
2097
|
}
|
|
2076
2098
|
else {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2099
|
+
perplexityStatus = perplexityVerification?.ok ? "ready" : "needs attention";
|
|
2100
|
+
perplexityDetailLines = [
|
|
2101
|
+
perplexityVerification?.ok
|
|
2102
|
+
? "verified live just now"
|
|
2103
|
+
: `live check failed: ${perplexityVerification.summary}`,
|
|
2104
|
+
];
|
|
2081
2105
|
}
|
|
2082
2106
|
let embeddingsStatus;
|
|
2083
2107
|
let embeddingsDetailLines;
|
|
2084
|
-
const embeddingsApiKey = runtimeConfig.ok
|
|
2085
|
-
? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
|
|
2086
|
-
: null;
|
|
2087
2108
|
if (!runtimeConfig.ok) {
|
|
2088
2109
|
embeddingsStatus = runtimeConfigReadStatus(runtimeConfig);
|
|
2089
2110
|
embeddingsDetailLines = [];
|
|
@@ -2093,10 +2114,12 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
2093
2114
|
embeddingsDetailLines = ["no API key saved yet"];
|
|
2094
2115
|
}
|
|
2095
2116
|
else {
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2117
|
+
embeddingsStatus = embeddingsVerification?.ok ? "ready" : "needs attention";
|
|
2118
|
+
embeddingsDetailLines = [
|
|
2119
|
+
embeddingsVerification?.ok
|
|
2120
|
+
? "verified live just now"
|
|
2121
|
+
: `live check failed: ${embeddingsVerification.summary}`,
|
|
2122
|
+
];
|
|
2100
2123
|
}
|
|
2101
2124
|
const teamsStatus = runtimeConfig.ok
|
|
2102
2125
|
? hasRuntimeConfigValue(runtimeConfig.config, "teams.clientId")
|
|
@@ -2943,7 +2966,7 @@ async function executeProviderCheck(command, deps) {
|
|
|
2943
2966
|
throw error;
|
|
2944
2967
|
}
|
|
2945
2968
|
}
|
|
2946
|
-
function renderProviderCredentialLine(credential) {
|
|
2969
|
+
function renderProviderCredentialLine(agentName, credential) {
|
|
2947
2970
|
if (credential.status === "present") {
|
|
2948
2971
|
const credentialFields = credential.credentialFields.length > 0 ? ` credentials: ${credential.credentialFields.join(", ")}` : " credentials: none";
|
|
2949
2972
|
const configFields = credential.configFields.length > 0 ? ` config: ${credential.configFields.join(", ")}` : " config: none";
|
|
@@ -2952,6 +2975,9 @@ function renderProviderCredentialLine(credential) {
|
|
|
2952
2975
|
if (credential.status === "invalid-pool") {
|
|
2953
2976
|
return `credentials: vault unavailable (${credential.error}); repair: ${credential.repair.command}`;
|
|
2954
2977
|
}
|
|
2978
|
+
if (credential.status === "not-loaded") {
|
|
2979
|
+
return `credentials: not loaded in this process; run \`ouro provider refresh --agent ${agentName}\` to read the vault now`;
|
|
2980
|
+
}
|
|
2955
2981
|
return `credentials: missing; repair: ${credential.repair.command}`;
|
|
2956
2982
|
}
|
|
2957
2983
|
async function executeProviderStatus(command, deps) {
|
|
@@ -2987,7 +3013,7 @@ async function executeProviderStatus(command, deps) {
|
|
|
2987
3013
|
const binding = resolved.binding;
|
|
2988
3014
|
lines.push(` ${lane}: ${binding.provider} / ${binding.model} (${binding.source})`);
|
|
2989
3015
|
lines.push(` readiness: ${binding.readiness.status}${binding.readiness.error ? ` (${binding.readiness.error})` : ""}`);
|
|
2990
|
-
lines.push(` ${renderProviderCredentialLine(binding.credential)}`);
|
|
3016
|
+
lines.push(` ${renderProviderCredentialLine(command.agent, binding.credential)}`);
|
|
2991
3017
|
for (const warning of binding.warnings) {
|
|
2992
3018
|
lines.push(` warning: ${warning.message}`);
|
|
2993
3019
|
}
|
|
@@ -84,6 +84,40 @@ function resolveProviderHealthCommand(providerHealth, status) {
|
|
|
84
84
|
}
|
|
85
85
|
return undefined;
|
|
86
86
|
}
|
|
87
|
+
function providerHealthTargetLane(providerHealth) {
|
|
88
|
+
const issue = providerHealth?.issue;
|
|
89
|
+
const actionLane = issue?.actions
|
|
90
|
+
.map((action) => "lane" in action && action.lane ? action.lane : undefined)
|
|
91
|
+
.find((lane) => lane === "outward" || lane === "inner");
|
|
92
|
+
if (actionLane)
|
|
93
|
+
return actionLane;
|
|
94
|
+
const text = [issue?.summary, issue?.detail, providerHealth?.error]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(" ")
|
|
97
|
+
.toLowerCase();
|
|
98
|
+
if (/\boutward provider\b/.test(text) || /\boutward lane\b/.test(text))
|
|
99
|
+
return "outward";
|
|
100
|
+
if (/\binner provider\b/.test(text) || /\binner lane\b/.test(text))
|
|
101
|
+
return "inner";
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function providerHealthAppliesToLane(providerHealth, lane) {
|
|
105
|
+
const targetLane = providerHealthTargetLane(providerHealth);
|
|
106
|
+
return !targetLane || targetLane === lane.lane;
|
|
107
|
+
}
|
|
108
|
+
function providerHealthDetail(providerHealth, status) {
|
|
109
|
+
if (status === "locked")
|
|
110
|
+
return "vault locked on this machine";
|
|
111
|
+
if (status === "needs credentials")
|
|
112
|
+
return "credentials missing";
|
|
113
|
+
if (status === "needs setup") {
|
|
114
|
+
return providerHealth?.issue?.detail ?? providerHealth?.error ?? "needs setup";
|
|
115
|
+
}
|
|
116
|
+
const detail = providerHealth?.issue?.detail ?? providerHealth?.error;
|
|
117
|
+
if (!detail)
|
|
118
|
+
return "live check needs attention";
|
|
119
|
+
return /failed live check/i.test(detail) ? detail : `failed live check: ${detail}`;
|
|
120
|
+
}
|
|
87
121
|
function isProblemStatus(status) {
|
|
88
122
|
return status !== "ready" && status !== "attached";
|
|
89
123
|
}
|
|
@@ -160,6 +194,23 @@ function summarizeProviderLane(agent, lane, providerHealth) {
|
|
|
160
194
|
};
|
|
161
195
|
}
|
|
162
196
|
const fallbackAction = providerHealthCommand ?? lane.credential.repairCommand;
|
|
197
|
+
if (providerHealth?.ok) {
|
|
198
|
+
return {
|
|
199
|
+
lane: lane.lane,
|
|
200
|
+
status: "ready",
|
|
201
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
202
|
+
detail: "ready",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (providerHealthStatus && providerHealthAppliesToLane(providerHealth, lane)) {
|
|
206
|
+
return {
|
|
207
|
+
lane: lane.lane,
|
|
208
|
+
status: providerHealthStatus,
|
|
209
|
+
title: `${lane.provider} / ${lane.model}`,
|
|
210
|
+
detail: providerHealthDetail(providerHealth, providerHealthStatus),
|
|
211
|
+
action: fallbackAction,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
163
214
|
if (lane.credential.status === "missing") {
|
|
164
215
|
return {
|
|
165
216
|
lane: lane.lane,
|
|
@@ -178,14 +229,6 @@ function summarizeProviderLane(agent, lane, providerHealth) {
|
|
|
178
229
|
action: fallbackAction,
|
|
179
230
|
};
|
|
180
231
|
}
|
|
181
|
-
if (providerHealth?.ok) {
|
|
182
|
-
return {
|
|
183
|
-
lane: lane.lane,
|
|
184
|
-
status: "ready",
|
|
185
|
-
title: `${lane.provider} / ${lane.model}`,
|
|
186
|
-
detail: "ready",
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
232
|
if (lane.readiness.status === "failed") {
|
|
190
233
|
return {
|
|
191
234
|
lane: lane.lane,
|
|
@@ -89,6 +89,16 @@ function resolveCredential(poolResult, provider, agentName) {
|
|
|
89
89
|
warnings: [missingCredentialWarning(provider)],
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
if ((0, provider_credentials_1.isProviderCredentialPoolNotLoaded)(poolResult)) {
|
|
93
|
+
return {
|
|
94
|
+
credential: {
|
|
95
|
+
status: "not-loaded",
|
|
96
|
+
provider,
|
|
97
|
+
poolPath: poolResult.poolPath,
|
|
98
|
+
},
|
|
99
|
+
warnings: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
92
102
|
if (poolResult.reason === "invalid" || poolResult.reason === "unavailable") {
|
|
93
103
|
return {
|
|
94
104
|
credential: {
|
|
@@ -130,6 +140,9 @@ function staleReadiness(readiness, reason) {
|
|
|
130
140
|
}
|
|
131
141
|
function resolveReadiness(input) {
|
|
132
142
|
if (!input.readiness) {
|
|
143
|
+
if (input.credential.status === "not-loaded") {
|
|
144
|
+
return { readiness: { status: "unknown" }, warnings: [] };
|
|
145
|
+
}
|
|
133
146
|
if (input.credential.status === "missing") {
|
|
134
147
|
return { readiness: { status: "unknown", reason: "credential-missing" }, warnings: [] };
|
|
135
148
|
}
|
|
@@ -170,6 +183,9 @@ function resolveReadiness(input) {
|
|
|
170
183
|
warnings: [],
|
|
171
184
|
};
|
|
172
185
|
}
|
|
186
|
+
if (input.credential.status === "not-loaded") {
|
|
187
|
+
return { readiness: readinessFromState(input.readiness), warnings: [] };
|
|
188
|
+
}
|
|
173
189
|
return { readiness: readinessFromState(input.readiness), warnings: [] };
|
|
174
190
|
}
|
|
175
191
|
function resolveEffectiveProviderBinding(input) {
|
|
@@ -33,10 +33,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR = void 0;
|
|
36
37
|
exports.splitProviderCredentialFields = splitProviderCredentialFields;
|
|
37
38
|
exports.providerCredentialsVaultPath = providerCredentialsVaultPath;
|
|
38
39
|
exports.providerCredentialItemName = providerCredentialItemName;
|
|
39
40
|
exports.providerCredentialMachineHomeDir = providerCredentialMachineHomeDir;
|
|
41
|
+
exports.isProviderCredentialPoolNotLoaded = isProviderCredentialPoolNotLoaded;
|
|
40
42
|
exports.readProviderCredentialPool = readProviderCredentialPool;
|
|
41
43
|
exports.refreshProviderCredentialPool = refreshProviderCredentialPool;
|
|
42
44
|
exports.createProviderCredentialRecord = createProviderCredentialRecord;
|
|
@@ -54,6 +56,7 @@ const credential_access_1 = require("../repertoire/credential-access");
|
|
|
54
56
|
const VALID_PROVIDERS = ["azure", "minimax", "anthropic", "openai-codex", "github-copilot"];
|
|
55
57
|
const VALID_PROVENANCE_SOURCES = ["auth-flow", "manual"];
|
|
56
58
|
const VAULT_ITEM_PREFIX = "providers/";
|
|
59
|
+
exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR = "provider credentials have not been loaded from vault";
|
|
57
60
|
const PROVIDER_FIELD_SPLITS = {
|
|
58
61
|
anthropic: {
|
|
59
62
|
credentials: ["setupToken", "refreshToken", "expiresAt"],
|
|
@@ -197,7 +200,7 @@ function recordFromPayload(payload) {
|
|
|
197
200
|
provenance: { ...payload.provenance },
|
|
198
201
|
};
|
|
199
202
|
}
|
|
200
|
-
function missingPool(agentName, error =
|
|
203
|
+
function missingPool(agentName, error = exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR) {
|
|
201
204
|
return {
|
|
202
205
|
ok: false,
|
|
203
206
|
reason: "missing",
|
|
@@ -205,10 +208,20 @@ function missingPool(agentName, error = "provider credentials have not been load
|
|
|
205
208
|
error,
|
|
206
209
|
};
|
|
207
210
|
}
|
|
211
|
+
function isProviderCredentialPoolNotLoaded(result) {
|
|
212
|
+
return !result.ok
|
|
213
|
+
&& result.reason === "missing"
|
|
214
|
+
&& result.error === exports.PROVIDER_CREDENTIAL_POOL_NOT_LOADED_ERROR;
|
|
215
|
+
}
|
|
208
216
|
function cacheResult(agentName, result) {
|
|
209
217
|
cachedPools.set(agentName, result);
|
|
210
218
|
return result;
|
|
211
219
|
}
|
|
220
|
+
function selectedProviders(providers) {
|
|
221
|
+
if (!providers || providers.length === 0)
|
|
222
|
+
return [...VALID_PROVIDERS];
|
|
223
|
+
return [...new Set(providers)];
|
|
224
|
+
}
|
|
212
225
|
function resultForProvider(poolResult, provider) {
|
|
213
226
|
if (!poolResult.ok)
|
|
214
227
|
return poolResult;
|
|
@@ -227,12 +240,13 @@ function readProviderCredentialPool(agentName) {
|
|
|
227
240
|
return cachedPools.get(agentName) ?? missingPool(agentName);
|
|
228
241
|
}
|
|
229
242
|
async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
243
|
+
const providersToRead = selectedProviders(options.providers);
|
|
230
244
|
try {
|
|
231
245
|
const store = (0, credential_access_1.getCredentialStore)(agentName);
|
|
232
246
|
options.onProgress?.(`reading vault items for ${agentName}...`);
|
|
233
247
|
const providers = {};
|
|
234
248
|
let updatedAt = new Date(0).toISOString();
|
|
235
|
-
for (const provider of
|
|
249
|
+
for (const provider of providersToRead) {
|
|
236
250
|
const itemName = providerCredentialItemName(provider);
|
|
237
251
|
options.onProgress?.(`reading ${provider} credentials...`);
|
|
238
252
|
let raw;
|
|
@@ -261,9 +275,19 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
|
261
275
|
component: "config/identity",
|
|
262
276
|
event: "config.provider_credentials_loaded",
|
|
263
277
|
message: "loaded provider credentials from vault",
|
|
264
|
-
meta: {
|
|
278
|
+
meta: {
|
|
279
|
+
agentName,
|
|
280
|
+
providerCount: Object.keys(providers).length,
|
|
281
|
+
requestedProviderCount: providersToRead.length,
|
|
282
|
+
cacheSkipped: options.skipCache === true,
|
|
283
|
+
},
|
|
265
284
|
});
|
|
266
|
-
|
|
285
|
+
const result = {
|
|
286
|
+
ok: true,
|
|
287
|
+
poolPath: providerCredentialsVaultPath(agentName),
|
|
288
|
+
pool,
|
|
289
|
+
};
|
|
290
|
+
return options.skipCache ? result : cacheResult(agentName, result);
|
|
267
291
|
}
|
|
268
292
|
catch (error) {
|
|
269
293
|
const cached = cachedPools.get(agentName);
|
|
@@ -282,7 +306,7 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
|
282
306
|
});
|
|
283
307
|
if (options.preserveCachedOnFailure && cached?.ok)
|
|
284
308
|
return cached;
|
|
285
|
-
return cacheResult(agentName, result);
|
|
309
|
+
return options.skipCache ? result : cacheResult(agentName, result);
|
|
286
310
|
}
|
|
287
311
|
}
|
|
288
312
|
function createProviderCredentialRecord(input) {
|
|
@@ -21,7 +21,7 @@ function credentialVisibility(binding) {
|
|
|
21
21
|
}
|
|
22
22
|
return {
|
|
23
23
|
status: credential.status,
|
|
24
|
-
repairCommand: credential.repair.command,
|
|
24
|
+
...("repair" in credential ? { repairCommand: credential.repair.command } : {}),
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
function readinessVisibility(binding) {
|
|
@@ -87,6 +87,8 @@ function credentialLabel(credential) {
|
|
|
87
87
|
return credential.source ?? "vault";
|
|
88
88
|
if (credential.status === "invalid-pool")
|
|
89
89
|
return "vault unavailable";
|
|
90
|
+
if (credential.status === "not-loaded")
|
|
91
|
+
return "checked previously";
|
|
90
92
|
return "missing";
|
|
91
93
|
}
|
|
92
94
|
function readinessLabel(readiness) {
|
|
@@ -102,7 +104,7 @@ function readinessLabel(readiness) {
|
|
|
102
104
|
return readiness.status;
|
|
103
105
|
}
|
|
104
106
|
function providerStatusDetail(lane) {
|
|
105
|
-
if (lane.credential.status
|
|
107
|
+
if (lane.credential.status === "missing" || lane.credential.status === "invalid-pool")
|
|
106
108
|
return undefined;
|
|
107
109
|
return lane.readiness.error;
|
|
108
110
|
}
|
|
@@ -134,15 +134,6 @@ function isBwConfigLogoutRequired(err) {
|
|
|
134
134
|
function shouldPreferExactItemLookup(domain) {
|
|
135
135
|
return domain.includes("/");
|
|
136
136
|
}
|
|
137
|
-
function isBwDirectLookupMissingError(err) {
|
|
138
|
-
const message = err.message.toLowerCase();
|
|
139
|
-
return message.includes("bw cli error: not found") || message.includes("bw cli error: item not found");
|
|
140
|
-
}
|
|
141
|
-
function isBwDirectLookupFallbackError(err) {
|
|
142
|
-
const message = err.message.toLowerCase();
|
|
143
|
-
return (message.includes("invalid json from bw get item") ||
|
|
144
|
-
message.includes("invalid item from bw get item"));
|
|
145
|
-
}
|
|
146
137
|
// ---------------------------------------------------------------------------
|
|
147
138
|
// Cross-process bw CLI lock
|
|
148
139
|
// ---------------------------------------------------------------------------
|
|
@@ -159,6 +150,9 @@ function isBwDirectLookupFallbackError(err) {
|
|
|
159
150
|
const BW_LOCK_FILENAME = ".ouro-bw.lock";
|
|
160
151
|
const BW_LOCK_TIMEOUT_MS = 30_000;
|
|
161
152
|
const BW_LOCK_POLL_MS = 100;
|
|
153
|
+
const BW_DATA_FILENAME = "data.json";
|
|
154
|
+
const BW_SYNC_MARKER_FILENAME = ".ouro-last-sync";
|
|
155
|
+
const BW_SYNC_FRESH_MS = 60_000;
|
|
162
156
|
/** In-process async mutex keyed by appDataDir. */
|
|
163
157
|
const inProcessLocks = new Map();
|
|
164
158
|
function isPidAlive(pid) {
|
|
@@ -388,6 +382,7 @@ class BitwardenCredentialStore {
|
|
|
388
382
|
appDataDir;
|
|
389
383
|
sessionToken = null;
|
|
390
384
|
bwBinaryPath = "bw";
|
|
385
|
+
structuredItemCache = null;
|
|
391
386
|
constructor(serverUrl, email, masterPassword, options = {}) {
|
|
392
387
|
this.serverUrl = serverUrl;
|
|
393
388
|
this.email = email;
|
|
@@ -488,9 +483,19 @@ class BitwardenCredentialStore {
|
|
|
488
483
|
const unlockOutput = await this.execBw(["unlock", this.masterPassword, "--raw"]);
|
|
489
484
|
this.sessionToken = unlockOutput.trim();
|
|
490
485
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
486
|
+
if (this.shouldSyncVaultAfterSession(status)) {
|
|
487
|
+
/* v8 ignore next -- defensive: loginAttempt always sets sessionToken before sync @preserve */
|
|
488
|
+
await this.execBw(["sync"], this.sessionToken ?? undefined);
|
|
489
|
+
this.writeSyncMarker();
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
(0, runtime_1.emitNervesEvent)({
|
|
493
|
+
event: "repertoire.bw_sync_skipped",
|
|
494
|
+
component: "repertoire",
|
|
495
|
+
message: "skipping bw sync because local vault cache is still fresh",
|
|
496
|
+
meta: { email: this.email, serverUrl: this.serverUrl, freshnessWindowMs: BW_SYNC_FRESH_MS },
|
|
497
|
+
});
|
|
498
|
+
}
|
|
494
499
|
}
|
|
495
500
|
async ensureSession() {
|
|
496
501
|
if (!this.sessionToken) {
|
|
@@ -512,6 +517,7 @@ class BitwardenCredentialStore {
|
|
|
512
517
|
throw err;
|
|
513
518
|
}
|
|
514
519
|
this.sessionToken = null;
|
|
520
|
+
this.structuredItemCache = null;
|
|
515
521
|
attemptedFreshSession = true;
|
|
516
522
|
}
|
|
517
523
|
}
|
|
@@ -614,6 +620,7 @@ class BitwardenCredentialStore {
|
|
|
614
620
|
notes: data.notes ?? null,
|
|
615
621
|
};
|
|
616
622
|
const encoded = Buffer.from(JSON.stringify(item)).toString("base64");
|
|
623
|
+
this.structuredItemCache = null;
|
|
617
624
|
let savedItem;
|
|
618
625
|
if (existing) {
|
|
619
626
|
const stdout = await this.execBw(["edit", "item", existing.id], session, encoded);
|
|
@@ -629,6 +636,7 @@ class BitwardenCredentialStore {
|
|
|
629
636
|
}
|
|
630
637
|
this.assertStoredCredentialMatches(domain, data, savedItem);
|
|
631
638
|
});
|
|
639
|
+
this.structuredItemCache = null;
|
|
632
640
|
(0, runtime_1.emitNervesEvent)({
|
|
633
641
|
event: "repertoire.bw_credential_store_end",
|
|
634
642
|
component: "repertoire",
|
|
@@ -677,6 +685,7 @@ class BitwardenCredentialStore {
|
|
|
677
685
|
return false;
|
|
678
686
|
}
|
|
679
687
|
await this.withSessionRetry((session) => this.execBw(["delete", "item", item.id], session));
|
|
688
|
+
this.structuredItemCache = null;
|
|
680
689
|
(0, runtime_1.emitNervesEvent)({
|
|
681
690
|
event: "repertoire.bw_credential_delete_end",
|
|
682
691
|
component: "repertoire",
|
|
@@ -688,25 +697,57 @@ class BitwardenCredentialStore {
|
|
|
688
697
|
// --- Private ---
|
|
689
698
|
async findItemByDomain(domain, session) {
|
|
690
699
|
if (shouldPreferExactItemLookup(domain)) {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const item = parseBwItem(stdout, "bw get item");
|
|
694
|
-
if (item.name === domain)
|
|
695
|
-
return item;
|
|
696
|
-
}
|
|
697
|
-
catch (error) {
|
|
698
|
-
const err = error;
|
|
699
|
-
if (isBwDirectLookupMissingError(err))
|
|
700
|
-
return null;
|
|
701
|
-
if (!isBwDirectLookupFallbackError(err))
|
|
702
|
-
throw err;
|
|
703
|
-
}
|
|
700
|
+
const items = await this.readStructuredItemCache(session);
|
|
701
|
+
return items.get(domain) ?? null;
|
|
704
702
|
}
|
|
705
703
|
const stdout = await this.execBw(["list", "items", "--search", domain], session);
|
|
706
704
|
const items = parseBwItems(stdout, "bw list items --search");
|
|
707
705
|
// Find exact match by name
|
|
708
706
|
return items.find((item) => item.name === domain) ?? null;
|
|
709
707
|
}
|
|
708
|
+
shouldSyncVaultAfterSession(status) {
|
|
709
|
+
if (status.status === "unauthenticated" || !status.status)
|
|
710
|
+
return true;
|
|
711
|
+
if (!this.appDataDir)
|
|
712
|
+
return true;
|
|
713
|
+
const freshnessTimestamp = this.latestLocalSyncTimestamp();
|
|
714
|
+
if (freshnessTimestamp !== null) {
|
|
715
|
+
return Date.now() - freshnessTimestamp > BW_SYNC_FRESH_MS;
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
latestLocalSyncTimestamp() {
|
|
720
|
+
const files = [BW_SYNC_MARKER_FILENAME, BW_DATA_FILENAME];
|
|
721
|
+
let latest = null;
|
|
722
|
+
for (const name of files) {
|
|
723
|
+
try {
|
|
724
|
+
const mtimeMs = fs.statSync(path.join(this.appDataDir, name)).mtimeMs;
|
|
725
|
+
latest = latest === null ? mtimeMs : Math.max(latest, mtimeMs);
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
// Missing freshness file is fine; use any other available timestamp.
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return latest;
|
|
732
|
+
}
|
|
733
|
+
writeSyncMarker() {
|
|
734
|
+
if (!this.appDataDir)
|
|
735
|
+
return;
|
|
736
|
+
try {
|
|
737
|
+
fs.writeFileSync(path.join(this.appDataDir, BW_SYNC_MARKER_FILENAME), `${Date.now()}\n`, { mode: 0o600 });
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// If the marker cannot be written, fall back to syncing next time.
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async readStructuredItemCache(session) {
|
|
744
|
+
if (this.structuredItemCache)
|
|
745
|
+
return this.structuredItemCache;
|
|
746
|
+
const stdout = await this.execBw(["list", "items"], session);
|
|
747
|
+
const items = parseBwItems(stdout, "bw list items");
|
|
748
|
+
this.structuredItemCache = new Map(items.map((item) => [item.name, item]));
|
|
749
|
+
return this.structuredItemCache;
|
|
750
|
+
}
|
|
710
751
|
async findItemById(id, session) {
|
|
711
752
|
const stdout = await this.execBw(["get", "item", id], session);
|
|
712
753
|
return parseBwItem(stdout, "bw get item");
|