@indigoai-us/hq-cloud 6.14.41 → 6.14.42

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.
@@ -42,6 +42,7 @@ import * as readline from "readline";
42
42
  import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, _testing as shareTesting } from "./share.js";
43
43
  import { deleteRemoteFile, downloadFile, headRemoteFile, uploadFile, uploadSymlink } from "../s3.js";
44
44
  import { VaultAuthError } from "../vault-client.js";
45
+ import { hashFile } from "../journal.js";
45
46
  const mockConfig = {
46
47
  apiUrl: "https://vault-api.test",
47
48
  authToken: "test-jwt-token",
@@ -1048,7 +1049,9 @@ describe("share", () => {
1048
1049
  hqRoot: tmpDir,
1049
1050
  });
1050
1051
  expect(result.filesUploaded).toBe(1);
1051
- expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "fenced.md", undefined, { ifMatch: '"baseline-etag"' });
1052
+ // Prefer journal-baseline If-Match when present (stale-writer guard);
1053
+ // quote form is optional (object-io quoteEtag accepts both).
1054
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "fenced.md", undefined, { ifMatch: "baseline-etag" });
1052
1055
  });
1053
1056
  it("fences a brand-new key with If-None-Match:* (create-only)", async () => {
1054
1057
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1143,6 +1146,359 @@ describe("share", () => {
1143
1146
  expect(result.aborted).toBe(true);
1144
1147
  expect(result.conflictPaths).toEqual(["halting.md"]);
1145
1148
  });
1149
+ // ── Stale-writer guard (Ace / vault multi-writer LWW, 2026-08-03) ──────
1150
+ //
1151
+ // Incident: Ace held Jul-28 worker bytes (journal still at that baseline)
1152
+ // while a peer had already advanced the vault object. Push conflict
1153
+ // requires localChanged && remoteChanged, so an unchanged-but-stale local
1154
+ // (skipUnchanged=false full re-push) fell through to PUT with
1155
+ // ifMatch: *live HEAD etag* — which succeeds and last-write-wins the old
1156
+ // body over the peer. Fix: fence with journal remoteEtag so peer advance
1157
+ // yields PreconditionFailed → existing keep/abort/overwrite path.
1158
+ it("Ace-shaped stale local under keep: journal-baseline If-Match fails closed (no silent LWW overwrite)", async () => {
1159
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1160
+ fs.mkdirSync(path.join(companyRoot, "workers", "fleet-agent-monitoring", "scripts"), {
1161
+ recursive: true,
1162
+ });
1163
+ const rel = "workers/fleet-agent-monitoring/scripts/hourly-enrich.py";
1164
+ const testFile = path.join(companyRoot, rel);
1165
+ // Stale local body (Ace-shaped: old smaller worker artifact).
1166
+ const staleBytes = "# old Jul-28 enricher\n".repeat(50);
1167
+ fs.writeFileSync(testFile, staleBytes);
1168
+ const staleHash = hashFile(testFile);
1169
+ const JOURNAL_ETAG = "etag-jul28-old";
1170
+ const PEER_ETAG = "etag-reviewed-new";
1171
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1172
+ lastModified: new Date(),
1173
+ etag: `"${PEER_ETAG}"`,
1174
+ size: 34768,
1175
+ });
1176
+ // S3-shaped fence: only land the PUT when If-Match equals live HEAD.
1177
+ vi.mocked(uploadFile).mockImplementation(async (_ctx, _p, _k, _a, pc) => {
1178
+ if (pc?.ifMatch) {
1179
+ const want = String(pc.ifMatch).replace(/^"|"$/g, "");
1180
+ if (want !== PEER_ETAG) {
1181
+ throw fence412();
1182
+ }
1183
+ }
1184
+ return { etag: '"should-not-land"' };
1185
+ });
1186
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1187
+ fs.writeFileSync(journalPath, JSON.stringify({
1188
+ version: "1",
1189
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
1190
+ files: {
1191
+ [rel]: {
1192
+ // Local matches journal → localChanged=false; remote advanced →
1193
+ // remoteChanged=true. Pre-fix 3-way did NOT conflict, and
1194
+ // ifMatch used live HEAD so the stale PUT succeeded.
1195
+ hash: staleHash,
1196
+ size: staleBytes.length,
1197
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
1198
+ direction: "down",
1199
+ remoteEtag: JOURNAL_ETAG,
1200
+ },
1201
+ },
1202
+ }));
1203
+ const events = [];
1204
+ const result = await share({
1205
+ paths: [testFile],
1206
+ company: "acme",
1207
+ vaultConfig: mockConfig,
1208
+ hqRoot: tmpDir,
1209
+ // Explicit full re-push path (models skipUnchanged !== true).
1210
+ skipUnchanged: false,
1211
+ onConflict: "keep",
1212
+ onEvent: (e) => events.push(e),
1213
+ });
1214
+ // Must not land the stale body under keep.
1215
+ expect(result.filesUploaded).toBe(0);
1216
+ expect(result.conflictPaths).toEqual([rel]);
1217
+ expect(events.some((e) => e.type === "conflict" && e.path === rel && e.resolution === "keep")).toBe(true);
1218
+ // Any PUT attempt must fence on the journal baseline, not live HEAD.
1219
+ // (If the planner conflicts pre-PUT that is also fine — no unconditional
1220
+ // overwrite of the peer-advanced object.)
1221
+ for (const call of vi.mocked(uploadFile).mock.calls) {
1222
+ const pc = call[4];
1223
+ expect(pc).toBeDefined();
1224
+ expect(pc.ifNoneMatch).toBeUndefined();
1225
+ const want = String(pc.ifMatch).replace(/^"|"$/g, "");
1226
+ expect(want).toBe(JOURNAL_ETAG);
1227
+ }
1228
+ // No unfenced retry under keep.
1229
+ expect(vi.mocked(uploadFile).mock.calls.some((c) => c[4] === undefined)).toBe(false);
1230
+ // Local stale bytes preserved on disk (keep does not pull).
1231
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(staleBytes);
1232
+ });
1233
+ it("legitimate local-ahead still uploads when remote remains at journal baseline", async () => {
1234
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1235
+ fs.mkdirSync(companyRoot, { recursive: true });
1236
+ const testFile = path.join(companyRoot, "workers", "legit-edit.md");
1237
+ fs.mkdirSync(path.dirname(testFile), { recursive: true });
1238
+ fs.writeFileSync(testFile, "my ahead edit");
1239
+ const BASE = "still-current-etag";
1240
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1241
+ lastModified: new Date(Date.now() - 120_000),
1242
+ etag: `"${BASE}"`,
1243
+ size: 4,
1244
+ });
1245
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
1246
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1247
+ fs.writeFileSync(journalPath, JSON.stringify({
1248
+ version: "1",
1249
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
1250
+ files: {
1251
+ "workers/legit-edit.md": {
1252
+ hash: "stale-journal-hash",
1253
+ size: 4,
1254
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
1255
+ direction: "up",
1256
+ remoteEtag: BASE,
1257
+ },
1258
+ },
1259
+ }));
1260
+ const result = await share({
1261
+ paths: [testFile],
1262
+ company: "acme",
1263
+ vaultConfig: mockConfig,
1264
+ hqRoot: tmpDir,
1265
+ onConflict: "keep",
1266
+ });
1267
+ expect(result.filesUploaded).toBe(1);
1268
+ expect(result.conflictPaths).toEqual([]);
1269
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "workers/legit-edit.md", undefined, { ifMatch: BASE });
1270
+ });
1271
+ it("true concurrent edit (both sides changed) still conflicts under keep", async () => {
1272
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1273
+ fs.mkdirSync(companyRoot, { recursive: true });
1274
+ const testFile = path.join(companyRoot, "both-changed.md");
1275
+ fs.writeFileSync(testFile, "local concurrent");
1276
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1277
+ lastModified: new Date(),
1278
+ etag: '"remote-concurrent"',
1279
+ size: 20,
1280
+ });
1281
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1282
+ fs.writeFileSync(journalPath, JSON.stringify({
1283
+ version: "1",
1284
+ lastSync: new Date().toISOString(),
1285
+ files: {
1286
+ "both-changed.md": {
1287
+ hash: "stale-hash",
1288
+ size: 10,
1289
+ syncedAt: new Date().toISOString(),
1290
+ direction: "up",
1291
+ remoteEtag: "baseline-before-both",
1292
+ },
1293
+ },
1294
+ }));
1295
+ const result = await share({
1296
+ paths: [testFile],
1297
+ company: "acme",
1298
+ vaultConfig: mockConfig,
1299
+ hqRoot: tmpDir,
1300
+ onConflict: "keep",
1301
+ });
1302
+ expect(result.conflictPaths).toEqual(["both-changed.md"]);
1303
+ expect(result.filesUploaded).toBe(0);
1304
+ expect(uploadFile).not.toHaveBeenCalled();
1305
+ });
1306
+ it("localDiverges under keep: pull-kept divergent local cannot silently become write authority", async () => {
1307
+ // After pull --on-conflict keep, journal stamps remoteEtag of the peer
1308
+ // version AND localDiverges=true (local never matched that etag). A later
1309
+ // push must not treat "remote still at stamped etag" as license to PUT
1310
+ // the divergent local body (journal-baseline If-Match would succeed).
1311
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1312
+ fs.mkdirSync(companyRoot, { recursive: true });
1313
+ const testFile = path.join(companyRoot, "kept-divergent.md");
1314
+ const localBytes = "my kept divergent local";
1315
+ fs.writeFileSync(testFile, localBytes);
1316
+ const localHash = hashFile(testFile);
1317
+ const REMOTE = "peer-version-etag";
1318
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1319
+ lastModified: new Date(),
1320
+ etag: `"${REMOTE}"`,
1321
+ size: 40,
1322
+ });
1323
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1324
+ fs.writeFileSync(journalPath, JSON.stringify({
1325
+ version: "1",
1326
+ lastSync: new Date().toISOString(),
1327
+ files: {
1328
+ "kept-divergent.md": {
1329
+ hash: localHash,
1330
+ size: localBytes.length,
1331
+ syncedAt: new Date().toISOString(),
1332
+ direction: "down",
1333
+ remoteEtag: REMOTE,
1334
+ localDiverges: true,
1335
+ },
1336
+ },
1337
+ }));
1338
+ const events = [];
1339
+ const result = await share({
1340
+ paths: [testFile],
1341
+ company: "acme",
1342
+ vaultConfig: mockConfig,
1343
+ hqRoot: tmpDir,
1344
+ skipUnchanged: false,
1345
+ onConflict: "keep",
1346
+ onEvent: (e) => events.push(e),
1347
+ });
1348
+ expect(result.filesUploaded).toBe(0);
1349
+ expect(result.conflictPaths).toEqual(["kept-divergent.md"]);
1350
+ expect(uploadFile).not.toHaveBeenCalled();
1351
+ expect(events.some((e) => e.type === "conflict" &&
1352
+ e.path === "kept-divergent.md" &&
1353
+ e.resolution === "keep")).toBe(true);
1354
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1355
+ });
1356
+ it("localDiverges under overwrite: explicit intent may force-write", async () => {
1357
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1358
+ fs.mkdirSync(companyRoot, { recursive: true });
1359
+ const testFile = path.join(companyRoot, "force-divergent.md");
1360
+ fs.writeFileSync(testFile, "force my local");
1361
+ const localHash = hashFile(testFile);
1362
+ const REMOTE = "peer-etag";
1363
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1364
+ lastModified: new Date(),
1365
+ etag: `"${REMOTE}"`,
1366
+ size: 10,
1367
+ });
1368
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"forced-etag"' });
1369
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1370
+ fs.writeFileSync(journalPath, JSON.stringify({
1371
+ version: "1",
1372
+ lastSync: new Date().toISOString(),
1373
+ files: {
1374
+ "force-divergent.md": {
1375
+ hash: localHash,
1376
+ size: 14,
1377
+ syncedAt: new Date().toISOString(),
1378
+ direction: "down",
1379
+ remoteEtag: REMOTE,
1380
+ localDiverges: true,
1381
+ },
1382
+ },
1383
+ }));
1384
+ const result = await share({
1385
+ paths: [testFile],
1386
+ company: "acme",
1387
+ vaultConfig: mockConfig,
1388
+ hqRoot: tmpDir,
1389
+ skipUnchanged: false,
1390
+ onConflict: "overwrite",
1391
+ });
1392
+ expect(result.filesUploaded).toBe(1);
1393
+ // Overwrite path may PUT unfenced after conflict resolution, or with a
1394
+ // fence then retry — either is consent. Must not leave the file unuploaded.
1395
+ expect(uploadFile).toHaveBeenCalled();
1396
+ });
1397
+ // Journal still carries remoteEtag from a prior sync, but the remote key
1398
+ // is gone (HEAD null) — e.g. peer delete or operator purge. Recreation
1399
+ // must use If-None-Match:* (create-only), NOT If-Match on the stale
1400
+ // journal etag (which cannot match a missing object and would false-
1401
+ // conflict forever under keep). Concurrent create still 412s via the
1402
+ // existing fence path.
1403
+ it("deleted journaled remote under keep: recreates once with If-None-Match:* and updates journal", async () => {
1404
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1405
+ fs.mkdirSync(companyRoot, { recursive: true });
1406
+ const testFile = path.join(companyRoot, "recreate-deleted.md");
1407
+ const localBytes = "local edit after remote was deleted";
1408
+ fs.writeFileSync(testFile, localBytes);
1409
+ const localHash = hashFile(testFile);
1410
+ const STALE_JOURNAL_ETAG = "etag-before-delete";
1411
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1412
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"recreated-etag"' });
1413
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1414
+ fs.writeFileSync(journalPath, JSON.stringify({
1415
+ version: "1",
1416
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
1417
+ files: {
1418
+ "recreate-deleted.md": {
1419
+ hash: "stale-journal-hash",
1420
+ size: 8,
1421
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
1422
+ direction: "up",
1423
+ remoteEtag: STALE_JOURNAL_ETAG,
1424
+ },
1425
+ },
1426
+ }));
1427
+ const result = await share({
1428
+ paths: [testFile],
1429
+ company: "acme",
1430
+ vaultConfig: mockConfig,
1431
+ hqRoot: tmpDir,
1432
+ skipUnchanged: false,
1433
+ onConflict: "keep",
1434
+ });
1435
+ expect(result.filesUploaded).toBe(1);
1436
+ expect(result.conflictPaths).toEqual([]);
1437
+ expect(result.filesSkipped).toBe(0);
1438
+ // Exactly one create-fenced PUT — not journal If-Match, not unfenced.
1439
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1440
+ expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "recreate-deleted.md", undefined, { ifNoneMatch: "*" });
1441
+ const pc = vi.mocked(uploadFile).mock.calls[0][4];
1442
+ expect(pc?.ifMatch).toBeUndefined();
1443
+ expect(pc?.ifNoneMatch).toBe("*");
1444
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1445
+ expect(journal.files["recreate-deleted.md"]).toMatchObject({
1446
+ hash: localHash,
1447
+ remoteEtag: "recreated-etag",
1448
+ direction: "up",
1449
+ });
1450
+ });
1451
+ it("deleted journaled remote + concurrent create 412 under keep: existing conflict semantics", async () => {
1452
+ // Same setup as recreation, but a peer creates the key between HEAD and
1453
+ // PUT → If-None-Match:* 412s → keep surfaces conflict, no journal stamp.
1454
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1455
+ fs.mkdirSync(companyRoot, { recursive: true });
1456
+ const testFile = path.join(companyRoot, "race-recreate.md");
1457
+ fs.writeFileSync(testFile, "my recreated local");
1458
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1459
+ vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
1460
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1461
+ fs.writeFileSync(dest, "peer created first");
1462
+ return undefined;
1463
+ });
1464
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1465
+ fs.writeFileSync(journalPath, JSON.stringify({
1466
+ version: "1",
1467
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
1468
+ files: {
1469
+ "race-recreate.md": {
1470
+ hash: "old-hash",
1471
+ size: 4,
1472
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
1473
+ direction: "up",
1474
+ remoteEtag: "etag-before-delete",
1475
+ },
1476
+ },
1477
+ }));
1478
+ const events = [];
1479
+ const result = await share({
1480
+ paths: [testFile],
1481
+ company: "acme",
1482
+ vaultConfig: mockConfig,
1483
+ hqRoot: tmpDir,
1484
+ skipUnchanged: false,
1485
+ onConflict: "keep",
1486
+ onEvent: (e) => events.push(e),
1487
+ });
1488
+ expect(result.filesUploaded).toBe(0);
1489
+ expect(result.conflictPaths).toEqual(["race-recreate.md"]);
1490
+ expect(result.aborted).toBe(false);
1491
+ expect(events.some((e) => e.type === "conflict" && e.path === "race-recreate.md")).toBe(true);
1492
+ // First (only) attempt must be create-fenced; no unfenced retry under keep.
1493
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1494
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifNoneMatch: "*" });
1495
+ // Journal not advanced to a successful upload etag (entry may still be
1496
+ // the prior baseline — next pass re-evaluates).
1497
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1498
+ expect(journal.files["race-recreate.md"]?.remoteEtag).toBe("etag-before-delete");
1499
+ expect(journal.files["race-recreate.md"]?.hash).toBe("old-hash");
1500
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("my recreated local");
1501
+ });
1146
1502
  });
1147
1503
  it("scoped push (plan-exceeds-grant): syncs the in-scope subset, skips out-of-scope paths, never aborts (feedback_ded09d56)", async () => {
1148
1504
  // Real case (look-optic): a FILE_ACL grant covered {knowledge,policies,