@ouro.bot/cli 0.1.0-alpha.411 → 0.1.0-alpha.413
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 +1 -1
- package/dist/heart/daemon/cli-exec.js +15 -9
- package/dist/heart/daemon/up-progress.js +12 -1
- package/dist/heart/provider-credentials.js +3 -0
- package/dist/repertoire/bitwarden-store.js +108 -1
- 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.413",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`ouro up` now shows real-time per-agent progress during provider checks -- the spinner displays which agent and vault operation is in progress (`slugger: reading vault items...`) instead of a static label with no detail.",
|
|
8
|
+
"Added `onProgress` callback to `refreshProviderCredentialPool` and `updateDetail()` method to `UpProgress` for sub-step spinner detail, threaded through the full `executeUpCommand` -> `checkAgentProviders` -> provider health check chain.",
|
|
9
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the progress threading release."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.412",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Cross-process bw CLI lock prevents concurrent access to the same Bitwarden app data directory, fixing the root cause of intermittent vault timeout errors during `ouro up` provider checks on multi-agent machines.",
|
|
16
|
+
"Two-layer locking in `execBw`: in-process async mutex serializes within a Node.js process, cross-process `O_EXCL` file lock with PID stale detection serializes across processes.",
|
|
17
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault concurrency fix release."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.411",
|
|
6
22
|
"changes": [
|
|
@@ -332,7 +332,7 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
|
|
|
332
332
|
if (stateResult.disabled)
|
|
333
333
|
return { ok: true };
|
|
334
334
|
const ping = deps.pingProvider ?? (await Promise.resolve().then(() => __importStar(require("../provider-ping")))).pingProvider;
|
|
335
|
-
const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName);
|
|
335
|
+
const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, deps.onProgress ? { onProgress: deps.onProgress } : undefined);
|
|
336
336
|
const pingGroups = new Map();
|
|
337
337
|
const lanes = ["outward", "inner"];
|
|
338
338
|
for (const lane of lanes) {
|
|
@@ -98,13 +98,14 @@ const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
|
|
|
98
98
|
const DEFAULT_DAEMON_STARTUP_STABILITY_WINDOW_MS = 1_500;
|
|
99
99
|
const DEFAULT_DAEMON_STARTUP_RETRY_LIMIT = 1;
|
|
100
100
|
const DEFAULT_DAEMON_STARTUP_LOG_LINES = 10;
|
|
101
|
-
async function checkAgentProviders(deps, agentsOverride) {
|
|
101
|
+
async function checkAgentProviders(deps, agentsOverride, onProgress) {
|
|
102
102
|
const agents = agentsOverride ?? await listCliAgents(deps);
|
|
103
103
|
const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
104
104
|
const degraded = [];
|
|
105
105
|
for (const agent of [...new Set(agents)]) {
|
|
106
106
|
try {
|
|
107
|
-
|
|
107
|
+
onProgress?.(`${agent}: checking providers...`);
|
|
108
|
+
const result = await checkAgentProviderHealth(agent, bundlesRoot, deps, onProgress);
|
|
108
109
|
if (result.ok)
|
|
109
110
|
continue;
|
|
110
111
|
const errorReason = result.error ?? "agent provider health check failed";
|
|
@@ -136,9 +137,14 @@ async function checkAgentProviders(deps, agentsOverride) {
|
|
|
136
137
|
}
|
|
137
138
|
return degraded;
|
|
138
139
|
}
|
|
139
|
-
async function checkAgentProviderHealth(agentName, bundlesRoot, deps) {
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
async function checkAgentProviderHealth(agentName, bundlesRoot, deps, onProgress) {
|
|
141
|
+
const liveDeps = {};
|
|
142
|
+
if (deps.homeDir)
|
|
143
|
+
liveDeps.homeDir = deps.homeDir;
|
|
144
|
+
if (onProgress)
|
|
145
|
+
liveDeps.onProgress = onProgress;
|
|
146
|
+
if (liveDeps.homeDir || liveDeps.onProgress) {
|
|
147
|
+
return (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agentName, bundlesRoot, liveDeps);
|
|
142
148
|
}
|
|
143
149
|
return (0, agent_config_check_1.checkAgentConfigWithProviderHealth)(agentName, bundlesRoot);
|
|
144
150
|
}
|
|
@@ -151,8 +157,8 @@ async function listCliAgents(deps) {
|
|
|
151
157
|
}
|
|
152
158
|
return [];
|
|
153
159
|
}
|
|
154
|
-
async function checkAlreadyRunningAgentProviders(deps) {
|
|
155
|
-
return checkAgentProviders(deps);
|
|
160
|
+
async function checkAlreadyRunningAgentProviders(deps, onProgress) {
|
|
161
|
+
return checkAgentProviders(deps, undefined, onProgress);
|
|
156
162
|
}
|
|
157
163
|
function readinessIssueFromDegraded(entry) {
|
|
158
164
|
return entry.issue ?? (0, readiness_repair_1.genericReadinessIssue)({
|
|
@@ -2578,7 +2584,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2578
2584
|
let providerChecksAlreadyRun = false;
|
|
2579
2585
|
if (!daemonAliveBeforeStart) {
|
|
2580
2586
|
progress.startPhase("provider checks");
|
|
2581
|
-
const preflightProviderDegraded = await checkAgentProviders(deps);
|
|
2587
|
+
const preflightProviderDegraded = await checkAgentProviders(deps, undefined, (msg) => progress.updateDetail(msg));
|
|
2582
2588
|
providerChecksAlreadyRun = true;
|
|
2583
2589
|
progress.completePhase("provider checks", providerRepairCountSummary(preflightProviderDegraded.length));
|
|
2584
2590
|
if (preflightProviderDegraded.length > 0) {
|
|
@@ -2616,7 +2622,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2616
2622
|
}, { initialAlive: daemonAliveBeforeStart });
|
|
2617
2623
|
if (!providerChecksAlreadyRun || daemonResult.alreadyRunning) {
|
|
2618
2624
|
progress.startPhase("provider checks");
|
|
2619
|
-
const providerDegraded = await checkAlreadyRunningAgentProviders(deps);
|
|
2625
|
+
const providerDegraded = await checkAlreadyRunningAgentProviders(deps, (msg) => progress.updateDetail(msg));
|
|
2620
2626
|
daemonResult.stability = mergeStartupStability(daemonResult.stability, providerDegraded);
|
|
2621
2627
|
progress.completePhase("provider checks", providerRepairCountSummary(providerDegraded.length));
|
|
2622
2628
|
}
|
|
@@ -52,6 +52,16 @@ class UpProgress {
|
|
|
52
52
|
return;
|
|
53
53
|
this.write(label);
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Update the sub-step detail on the current spinner phase. Rendered as
|
|
57
|
+
* "label (Xs) -- detail" in TTY mode. No-op in non-TTY mode or when
|
|
58
|
+
* no phase is active.
|
|
59
|
+
*/
|
|
60
|
+
updateDetail(detail) {
|
|
61
|
+
if (!this.isTTY || !this.currentPhase)
|
|
62
|
+
return;
|
|
63
|
+
this.currentPhase.detail = detail;
|
|
64
|
+
}
|
|
55
65
|
/**
|
|
56
66
|
* Mark the current phase as done. In non-TTY mode, immediately writes
|
|
57
67
|
* a static line. Emits a nerves event for observability.
|
|
@@ -94,7 +104,8 @@ class UpProgress {
|
|
|
94
104
|
const elapsedSec = (elapsed / 1000).toFixed(1);
|
|
95
105
|
const frameIndex = Math.floor(elapsed / 80) % SPINNER_FRAMES.length;
|
|
96
106
|
const spinner = SPINNER_FRAMES[frameIndex];
|
|
97
|
-
|
|
107
|
+
const detailSuffix = this.currentPhase.detail ? ` \u2014 ${this.currentPhase.detail}` : "";
|
|
108
|
+
lines.push(` ${BOLD}${spinner}${RESET} ${this.currentPhase.label} ${DIM}(${elapsedSec}s)${detailSuffix}${RESET}`);
|
|
98
109
|
}
|
|
99
110
|
let output = "";
|
|
100
111
|
if (this.prevLineCount > 0) {
|
|
@@ -224,6 +224,7 @@ function readProviderCredentialPool(agentName) {
|
|
|
224
224
|
async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
225
225
|
try {
|
|
226
226
|
const store = (0, credential_access_1.getCredentialStore)(agentName);
|
|
227
|
+
options.onProgress?.(`reading vault items for ${agentName}...`);
|
|
227
228
|
const items = await store.list();
|
|
228
229
|
const providers = {};
|
|
229
230
|
let updatedAt = new Date(0).toISOString();
|
|
@@ -233,6 +234,7 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
|
233
234
|
const provider = item.domain.slice(VAULT_ITEM_PREFIX.length);
|
|
234
235
|
if (!isAgentProvider(provider))
|
|
235
236
|
continue;
|
|
237
|
+
options.onProgress?.(`reading ${provider} credentials...`);
|
|
236
238
|
const raw = await store.getRawSecret(item.domain, "password");
|
|
237
239
|
const payload = validateProviderCredentialPayload(JSON.parse(raw), provider);
|
|
238
240
|
const record = recordFromPayload(payload);
|
|
@@ -240,6 +242,7 @@ async function refreshProviderCredentialPool(agentName, options = {}) {
|
|
|
240
242
|
if (record.updatedAt > updatedAt)
|
|
241
243
|
updatedAt = record.updatedAt;
|
|
242
244
|
}
|
|
245
|
+
options.onProgress?.("parsing provider credentials...");
|
|
243
246
|
const pool = {
|
|
244
247
|
schemaVersion: 1,
|
|
245
248
|
updatedAt,
|
|
@@ -45,6 +45,7 @@ exports.BitwardenCredentialStore = void 0;
|
|
|
45
45
|
exports.sanitizeCredentialErrorDetail = sanitizeCredentialErrorDetail;
|
|
46
46
|
const node_child_process_1 = require("node:child_process");
|
|
47
47
|
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
48
49
|
const runtime_1 = require("../nerves/runtime");
|
|
49
50
|
const bw_installer_1 = require("./bw-installer");
|
|
50
51
|
const MAX_ERROR_DETAIL_LENGTH = 500;
|
|
@@ -130,13 +131,118 @@ function isBwConfigLogoutRequired(err) {
|
|
|
130
131
|
const message = err.message.toLowerCase();
|
|
131
132
|
return message.includes("logout") && message.includes("required");
|
|
132
133
|
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Cross-process bw CLI lock
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// The bw CLI cannot handle concurrent access to the same app data directory.
|
|
138
|
+
// Two processes (e.g. daemon worker + ouro up CLI) hitting the same dir
|
|
139
|
+
// simultaneously corrupt bw's local state, producing empty/garbled output.
|
|
140
|
+
//
|
|
141
|
+
// We use two layers:
|
|
142
|
+
// 1. In-process async mutex: a Map<string, Promise<void>> keyed by appDataDir
|
|
143
|
+
// serializes calls within a single Node.js process.
|
|
144
|
+
// 2. Cross-process file lock: fs.openSync(lockPath, 'wx') with PID stale
|
|
145
|
+
// detection serializes across processes.
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
const BW_LOCK_FILENAME = ".ouro-bw.lock";
|
|
148
|
+
const BW_LOCK_TIMEOUT_MS = 30_000;
|
|
149
|
+
const BW_LOCK_POLL_MS = 100;
|
|
150
|
+
/** In-process async mutex keyed by appDataDir. */
|
|
151
|
+
const inProcessLocks = new Map();
|
|
152
|
+
function isPidAlive(pid) {
|
|
153
|
+
try {
|
|
154
|
+
process.kill(pid, 0);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function acquireFileLock(lockPath) {
|
|
162
|
+
const content = `${process.pid}\n`;
|
|
163
|
+
const deadline = Date.now() + BW_LOCK_TIMEOUT_MS;
|
|
164
|
+
while (true) {
|
|
165
|
+
try {
|
|
166
|
+
const fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL);
|
|
167
|
+
fs.writeSync(fd, content);
|
|
168
|
+
fs.closeSync(fd);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (err.code !== "EEXIST") {
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
// Lock file exists -- check for stale lock
|
|
176
|
+
try {
|
|
177
|
+
const existing = fs.readFileSync(lockPath, "utf8").trim();
|
|
178
|
+
const pid = parseInt(existing, 10);
|
|
179
|
+
if (!isNaN(pid) && !isPidAlive(pid)) {
|
|
180
|
+
// Stale lock -- remove and retry immediately
|
|
181
|
+
try {
|
|
182
|
+
fs.unlinkSync(lockPath);
|
|
183
|
+
}
|
|
184
|
+
catch { /* race with another cleaner is fine */ }
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch { /* v8 ignore next -- race: lock file disappeared between openSync and readFileSync @preserve */
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (Date.now() >= deadline) {
|
|
192
|
+
throw new Error(`bw CLI lock timeout: could not acquire ${lockPath} within ${BW_LOCK_TIMEOUT_MS}ms`);
|
|
193
|
+
}
|
|
194
|
+
// Yield to the event loop before retrying
|
|
195
|
+
await delay(BW_LOCK_POLL_MS);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function releaseFileLock(lockPath) {
|
|
200
|
+
try {
|
|
201
|
+
fs.unlinkSync(lockPath);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Already removed or never created -- safe to ignore
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function withBwLock(appDataDir, fn) {
|
|
208
|
+
if (!appDataDir) {
|
|
209
|
+
// No appDataDir means the default bw data location. Still need in-process
|
|
210
|
+
// serialization but cannot do cross-process file lock without a dir.
|
|
211
|
+
return fn();
|
|
212
|
+
}
|
|
213
|
+
const lockKey = appDataDir;
|
|
214
|
+
const lockPath = path.join(appDataDir, BW_LOCK_FILENAME);
|
|
215
|
+
// In-process serialization: chain onto the previous promise for this key
|
|
216
|
+
const previous = inProcessLocks.get(lockKey) ?? Promise.resolve();
|
|
217
|
+
let releaseLock;
|
|
218
|
+
const current = new Promise((resolve) => { releaseLock = resolve; });
|
|
219
|
+
inProcessLocks.set(lockKey, current);
|
|
220
|
+
await previous;
|
|
221
|
+
let fileLockAcquired = false;
|
|
222
|
+
try {
|
|
223
|
+
// Cross-process file lock
|
|
224
|
+
await acquireFileLock(lockPath);
|
|
225
|
+
fileLockAcquired = true;
|
|
226
|
+
return await fn();
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
if (fileLockAcquired) {
|
|
230
|
+
releaseFileLock(lockPath);
|
|
231
|
+
}
|
|
232
|
+
releaseLock();
|
|
233
|
+
// Clean up the map entry if we are the latest
|
|
234
|
+
if (inProcessLocks.get(lockKey) === current) {
|
|
235
|
+
inProcessLocks.delete(lockKey);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
133
239
|
function execBw(args, sessionToken, appDataDir, stdin) {
|
|
134
240
|
const env = {
|
|
135
241
|
...process.env,
|
|
136
242
|
...(sessionToken ? { BW_SESSION: sessionToken } : {}),
|
|
137
243
|
...(appDataDir ? { BITWARDENCLI_APPDATA_DIR: appDataDir } : {}),
|
|
138
244
|
};
|
|
139
|
-
|
|
245
|
+
const runCommand = () => new Promise((resolve, reject) => {
|
|
140
246
|
const child = (0, node_child_process_1.execFile)("bw", args, { timeout: 30_000, env }, (err, stdout, stderr) => {
|
|
141
247
|
if (err) {
|
|
142
248
|
if (isBwNotInstalled(err)) {
|
|
@@ -152,6 +258,7 @@ function execBw(args, sessionToken, appDataDir, stdin) {
|
|
|
152
258
|
child?.stdin?.end(stdin);
|
|
153
259
|
}
|
|
154
260
|
});
|
|
261
|
+
return withBwLock(appDataDir, runCommand);
|
|
155
262
|
}
|
|
156
263
|
/** Check if the error indicates the bw CLI binary is not installed. */
|
|
157
264
|
function isBwNotInstalled(err) {
|