@ouro.bot/cli 0.1.0-alpha.411 → 0.1.0-alpha.412

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 CHANGED
@@ -1,6 +1,14 @@
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.412",
6
+ "changes": [
7
+ "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.",
8
+ "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.",
9
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault concurrency fix release."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.411",
6
14
  "changes": [
@@ -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
- return new Promise((resolve, reject) => {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.411",
3
+ "version": "0.1.0-alpha.412",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",