@indigoai-us/hq-cloud 6.18.45 → 6.18.46-alpha.2

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.
Files changed (70) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +11 -0
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner.d.ts +18 -10
  5. package/dist/bin/sync-runner.d.ts.map +1 -1
  6. package/dist/bin/sync-runner.js +26 -14
  7. package/dist/bin/sync-runner.js.map +1 -1
  8. package/dist/cli/share.d.ts +16 -2
  9. package/dist/cli/share.d.ts.map +1 -1
  10. package/dist/cli/share.js +192 -16
  11. package/dist/cli/share.js.map +1 -1
  12. package/dist/cli/sync.d.ts +6 -0
  13. package/dist/cli/sync.d.ts.map +1 -1
  14. package/dist/cli/sync.js +21 -0
  15. package/dist/cli/sync.js.map +1 -1
  16. package/dist/cli/tombstones.d.ts +9 -1
  17. package/dist/cli/tombstones.d.ts.map +1 -1
  18. package/dist/cli/tombstones.js +258 -32
  19. package/dist/cli/tombstones.js.map +1 -1
  20. package/dist/journal-row-store.d.ts +3 -0
  21. package/dist/journal-row-store.d.ts.map +1 -1
  22. package/dist/journal-row-store.js +23 -0
  23. package/dist/journal-row-store.js.map +1 -1
  24. package/dist/journal-snapshot-decoder.js +2 -0
  25. package/dist/journal-snapshot-decoder.js.map +1 -1
  26. package/dist/journal-snapshot-v4.d.ts +2 -2
  27. package/dist/journal-snapshot-v4.d.ts.map +1 -1
  28. package/dist/journal-snapshot-v4.js +8 -3
  29. package/dist/journal-snapshot-v4.js.map +1 -1
  30. package/dist/journal.d.ts +1 -0
  31. package/dist/journal.d.ts.map +1 -1
  32. package/dist/journal.js +3 -1
  33. package/dist/journal.js.map +1 -1
  34. package/dist/outposts/session-heartbeat.d.ts.map +1 -1
  35. package/dist/outposts/session-heartbeat.js +20 -1
  36. package/dist/outposts/session-heartbeat.js.map +1 -1
  37. package/dist/s3.d.ts +22 -5
  38. package/dist/s3.d.ts.map +1 -1
  39. package/dist/s3.js +55 -10
  40. package/dist/s3.js.map +1 -1
  41. package/dist/symlink-target-policy.d.ts +32 -0
  42. package/dist/symlink-target-policy.d.ts.map +1 -0
  43. package/dist/symlink-target-policy.js +124 -0
  44. package/dist/symlink-target-policy.js.map +1 -0
  45. package/dist/sync/area-ledger-migration.d.ts +20 -0
  46. package/dist/sync/area-ledger-migration.d.ts.map +1 -1
  47. package/dist/sync/area-ledger-migration.js +59 -0
  48. package/dist/sync/area-ledger-migration.js.map +1 -1
  49. package/dist/sync/durable-apply.d.ts +16 -0
  50. package/dist/sync/durable-apply.d.ts.map +1 -1
  51. package/dist/sync/durable-apply.js +53 -0
  52. package/dist/sync/durable-apply.js.map +1 -1
  53. package/dist/sync/symlink-target-refusal-flag.d.ts +13 -0
  54. package/dist/sync/symlink-target-refusal-flag.d.ts.map +1 -0
  55. package/dist/sync/symlink-target-refusal-flag.js +51 -0
  56. package/dist/sync/symlink-target-refusal-flag.js.map +1 -0
  57. package/dist/sync-progress.d.ts.map +1 -1
  58. package/dist/sync-progress.js +2 -0
  59. package/dist/sync-progress.js.map +1 -1
  60. package/dist/telemetry.d.ts +4 -4
  61. package/dist/telemetry.d.ts.map +1 -1
  62. package/dist/telemetry.js +1 -1
  63. package/dist/telemetry.js.map +1 -1
  64. package/dist/types.d.ts +2 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/usage-company-fallback.d.ts +4 -3
  67. package/dist/usage-company-fallback.d.ts.map +1 -1
  68. package/dist/usage-company-fallback.js +40 -14
  69. package/dist/usage-company-fallback.js.map +1 -1
  70. package/package.json +1 -1
package/dist/cli/share.js CHANGED
@@ -7,6 +7,8 @@
7
7
  import * as fs from "fs";
8
8
  import * as os from "os";
9
9
  import * as path from "path";
10
+ import { resolveSymlinkTargetRefusalFlag } from "../sync/symlink-target-refusal-flag.js";
11
+ import { inspectSymlinkTarget, materializedTargetMatchesWire } from "../symlink-target-policy.js";
10
12
  import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
11
13
  import { ensureCompanyVaultTransport } from "../company-vault-transport.js";
12
14
  import { createSyncProgressRecorder } from "../sync-progress.js";
@@ -63,7 +65,7 @@ import { directoryFoldsCase, findCaseFoldedLocalKey, foldVaultKey, } from "./cas
63
65
  * outcome. The name is derived from the relative path so concurrent probes
64
66
  * for distinct files never collide on disk.
65
67
  */
66
- async function remoteContentDiffers(ctx, relativePath, localHash, hqRoot) {
68
+ async function remoteContentDiffers(ctx, relativePath, localHash, hqRoot, symlinkOptions) {
67
69
  const probeKey = crypto
68
70
  .createHash("sha256")
69
71
  .update(relativePath)
@@ -71,7 +73,7 @@ async function remoteContentDiffers(ctx, relativePath, localHash, hqRoot) {
71
73
  .slice(0, 16);
72
74
  const probePath = path.join(os.tmpdir(), `hq-conflict-probe-${readShortMachineId(hqRoot)}-${probeKey}.tmp`);
73
75
  try {
74
- await downloadFile(ctx, relativePath, probePath);
76
+ await downloadFile(ctx, relativePath, probePath, symlinkOptions);
75
77
  const remoteHash = fs.lstatSync(probePath).isSymbolicLink()
76
78
  ? hashSymlinkTarget(fs.readlinkSync(probePath))
77
79
  : hashFile(probePath);
@@ -273,6 +275,7 @@ export const _testing = {
273
275
  cooperativeRowVisits: snapshotCooperativeRowVisits,
274
276
  computePushPlan,
275
277
  computePushPlanCooperatively,
278
+ normalizeWindowsSymlinkTarget,
276
279
  };
277
280
  /** Snapshot of `absolutePath`, or null when it is missing or not a plain file. */
278
281
  function statSnapshot(absolutePath) {
@@ -446,7 +449,7 @@ function refusesServerOwnedPush(relativePath, ontologyEntityPush, absolutePath,
446
449
  }
447
450
  return isCloudAuthoritative(relativePath);
448
451
  }
449
- function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, ontologyEntityPush = false) {
452
+ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, ontologyEntityPush = false, symlinkContext) {
450
453
  const items = [];
451
454
  let omittedSkips = 0;
452
455
  let plannedRows = 0;
@@ -473,6 +476,18 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, onto
473
476
  // itself "looks" identical.
474
477
  if (entry.kind === "symlink") {
475
478
  const localHash = hashSymlinkTarget(entry.target);
479
+ const journalEntry = journalRows(journal).get(relativePath);
480
+ const portable = portablePushSymlinkTarget(absolutePath, relativePath, entry.target, journalEntry?.wireSymlinkTarget, symlinkContext);
481
+ if (portable.refused) {
482
+ items.push({
483
+ action: "refuse-unsafe-symlink",
484
+ absolutePath,
485
+ relativePath,
486
+ classification: portable.classification,
487
+ shape: portable.shape,
488
+ });
489
+ continue;
490
+ }
476
491
  if (skipUnchanged) {
477
492
  const existing = skipFingerprint(journal, relativePath);
478
493
  // `!localDiverges`: see the regular-file gate below — a divergent entry
@@ -488,7 +503,7 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, onto
488
503
  kind: "symlink",
489
504
  absolutePath,
490
505
  relativePath,
491
- target: entry.target,
506
+ target: portable.target,
492
507
  localHash,
493
508
  size: 0,
494
509
  });
@@ -608,12 +623,107 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat, onto
608
623
  filesToSkip += omittedSkips;
609
624
  return { items, filesToUpload, bytesToUpload, filesToSkip, preplannedFilesSkipped: omittedSkips };
610
625
  }
626
+ function portablePushSymlinkTarget(absolutePath, relativePath, localTarget, wireTarget, context) {
627
+ const pathApi = process.platform === "win32" ? path.win32 : path;
628
+ let scope;
629
+ let scopeRoot;
630
+ let linkPath = relativePath;
631
+ if (context) {
632
+ const skillFarmRoot = pathApi.resolve(context.hqRoot, ".claude", "skills");
633
+ const skillFarmRelative = pathApi.relative(skillFarmRoot, pathApi.resolve(absolutePath));
634
+ const isSkillFarmPath = skillFarmRelative !== ".." &&
635
+ !skillFarmRelative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(skillFarmRelative);
636
+ if (isSkillFarmPath) {
637
+ scope = { kind: "personal" };
638
+ scopeRoot = context.hqRoot;
639
+ linkPath = `.claude/skills/${skillFarmRelative.split(pathApi.sep).join("/")}`;
640
+ }
641
+ else if (context.personalMode) {
642
+ scope = { kind: "personal" };
643
+ scopeRoot = context.syncRoot;
644
+ linkPath = `personal/${relativePath}`;
645
+ }
646
+ else {
647
+ scope = { kind: "company", companySlug: context.companySlug };
648
+ scopeRoot = context.syncRoot;
649
+ linkPath = `companies/${context.companySlug}/${relativePath}`;
650
+ }
651
+ }
652
+ if (wireTarget !== undefined && scope) {
653
+ let wireReport;
654
+ const wireInspection = inspectSymlinkTarget({
655
+ linkPath,
656
+ target: wireTarget,
657
+ scope,
658
+ refuse: true,
659
+ emit: (event) => { wireReport = event; },
660
+ });
661
+ if (wireInspection.refused) {
662
+ const event = wireReport ?? { classification: "escapes-scope", shape: "relative-path" };
663
+ process.stderr.write(`hq-cloud symlink target ${event.classification} ${event.shape}\n`);
664
+ return { target: "", refused: true, ...event };
665
+ }
666
+ if (materializedTargetMatchesWire(absolutePath, localTarget, wireTarget)) {
667
+ return { target: wireTarget, refused: false };
668
+ }
669
+ }
670
+ let candidate = localTarget;
671
+ if (path.isAbsolute(localTarget) || path.win32.isAbsolute(localTarget)) {
672
+ if (!scope || !scopeRoot) {
673
+ const event = { classification: "windows-absolute", shape: "absolute-rooted" };
674
+ process.stderr.write(`hq-cloud symlink target ${event.classification} ${event.shape}\n`);
675
+ return { target: "", refused: true, ...event };
676
+ }
677
+ const absoluteTarget = pathApi.resolve(localTarget);
678
+ if (scope?.kind === "company" &&
679
+ relativePath === "knowledge" &&
680
+ absoluteTarget === pathApi.resolve(scopeRoot, `../../repos/private/knowledge-${scope.companySlug}`)) {
681
+ return { target: `../../repos/private/knowledge-${scope.companySlug}`, refused: false };
682
+ }
683
+ const relativeToScope = pathApi.relative(scopeRoot, absoluteTarget);
684
+ if (relativeToScope === ".." || relativeToScope.startsWith(`..${pathApi.sep}`) || pathApi.isAbsolute(relativeToScope)) {
685
+ const event = { classification: "escapes-scope", shape: "absolute-outside-scope" };
686
+ process.stderr.write(`hq-cloud symlink target ${event.classification} ${event.shape}\n`);
687
+ return { target: "", refused: true, ...event };
688
+ }
689
+ candidate = pathApi.relative(pathApi.dirname(absolutePath), absoluteTarget).replaceAll("\\", "/");
690
+ }
691
+ else if (process.platform === "win32") {
692
+ // fs.readlink on Windows returns native separators for a relative link.
693
+ // The wire target is portable and must keep POSIX separators.
694
+ candidate = normalizeWindowsSymlinkTarget(candidate);
695
+ }
696
+ if (!scope && (path.isAbsolute(candidate) || path.win32.isAbsolute(candidate))) {
697
+ const event = { classification: "escapes-scope", shape: "absolute-outside-scope" };
698
+ process.stderr.write(`hq-cloud symlink target ${event.classification} ${event.shape}\n`);
699
+ return { target: "", refused: true, ...event };
700
+ }
701
+ if (!scope)
702
+ return { target: candidate, refused: false };
703
+ let report;
704
+ const inspection = inspectSymlinkTarget({
705
+ linkPath,
706
+ target: candidate,
707
+ scope,
708
+ refuse: true,
709
+ emit: (event) => { report = event; },
710
+ });
711
+ if (inspection.refused) {
712
+ const event = report ?? { classification: "escapes-scope", shape: "relative-path" };
713
+ process.stderr.write(`hq-cloud symlink target ${event.classification} ${event.shape}\n`);
714
+ return { target: "", refused: true, ...event };
715
+ }
716
+ return { target: candidate, refused: false };
717
+ }
718
+ function normalizeWindowsSymlinkTarget(target) {
719
+ return target.replaceAll("\\", "/");
720
+ }
611
721
  /**
612
722
  * Build a full-pass plan in bounded row batches. A scoped replan deliberately
613
723
  * uses the synchronous single-row planner above, so it does not add an await
614
724
  * boundary between upload admissions and collapse the transfer pool.
615
725
  */
616
- async function computePushPlanCooperatively(filesToShare, journal, skipUnchanged, onHeartbeat, cooperativeCheckpoint, ontologyEntityPush = false) {
726
+ async function computePushPlanCooperatively(filesToShare, journal, skipUnchanged, onHeartbeat, cooperativeCheckpoint, ontologyEntityPush = false, symlinkContext) {
617
727
  // Keep each synchronous hash/stat slice small enough that a large backlog
618
728
  // cannot starve protocol heartbeats or watcher admission for tens of seconds.
619
729
  // This changes only scheduling granularity; plan decisions are unchanged.
@@ -626,7 +736,7 @@ async function computePushPlanCooperatively(filesToShare, journal, skipUnchanged
626
736
  const cooperativeWork = { rowsSinceYield: 0, lastYieldAt: 0 };
627
737
  for (let start = 0; start < filesToShare.length; start += rowsPerPlanningYield) {
628
738
  const slice = filesToShare.slice(start, start + rowsPerPlanningYield);
629
- const plan = computePushPlan(slice, journal, skipUnchanged, onHeartbeat, ontologyEntityPush);
739
+ const plan = computePushPlan(slice, journal, skipUnchanged, onHeartbeat, ontologyEntityPush, symlinkContext);
630
740
  items.push(...plan.items);
631
741
  filesToUpload += plan.filesToUpload;
632
742
  bytesToUpload += plan.bytesToUpload;
@@ -677,11 +787,11 @@ function recollectPushEntry(item, reuseKnownHash = true) {
677
787
  * classified from the bytes and journal state that exist immediately before a
678
788
  * worker starts, so callers never upload with a hash captured near pass entry.
679
789
  */
680
- function replanPushItemAtExecution(item, journal, skipUnchanged, onHeartbeat, ontologyEntityPush = false, forceRehash = false) {
790
+ function replanPushItemAtExecution(item, journal, skipUnchanged, onHeartbeat, ontologyEntityPush = false, forceRehash = false, symlinkContext) {
681
791
  const current = recollectPushEntry(item, !forceRehash);
682
792
  if (current === null)
683
793
  return null;
684
- return computePushPlan([current], journal, skipUnchanged, onHeartbeat, ontologyEntityPush).items[0] ?? null;
794
+ return computePushPlan([current], journal, skipUnchanged, onHeartbeat, ontologyEntityPush, symlinkContext).items[0] ?? null;
685
795
  }
686
796
  function uploadBodyMatchesPlan(planned, current) {
687
797
  if (planned.kind !== current.kind || planned.localHash !== current.localHash) {
@@ -1132,6 +1242,14 @@ async function createPushRunContext(options) {
1132
1242
  ? wrapFilterWithScope(transferFilter, syncRoot, options.prefixSet, onScopeExcluded)
1133
1243
  : transferFilter;
1134
1244
  const journalSlug = options.journalSlug ?? ctx.slug;
1245
+ const flagConfig = vaultConfig ?? privateFolderVaultConfig;
1246
+ const symlinkTargetRefusal = flagConfig
1247
+ ? await resolveSymlinkTargetRefusalFlag({
1248
+ config: flagConfig,
1249
+ companyUid: ctx.uid,
1250
+ companySlug: ctx.slug,
1251
+ })
1252
+ : false;
1135
1253
  if (journalSlug === PERSONAL_VAULT_JOURNAL_SLUG)
1136
1254
  migratePersonalVaultJournal();
1137
1255
  // The scoped runner has not completed its narrow filesystem walk yet, so
@@ -1144,6 +1262,7 @@ async function createPushRunContext(options) {
1144
1262
  ontologyEntityPush: false,
1145
1263
  skillRegisterLandedBytes: false,
1146
1264
  personalConnectorSettingsSync,
1265
+ symlinkTargetRefusal,
1147
1266
  skillRegistrationFallbacks: 0,
1148
1267
  paths,
1149
1268
  message,
@@ -1575,7 +1694,11 @@ async function reconcileOnePendingCompanySkill(run, entry) {
1575
1694
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-skill-metadata-"));
1576
1695
  const tempFile = path.join(tempDir, "SKILL.md");
1577
1696
  try {
1578
- await downloadFile(run.ctx, entry.relativePath, tempFile);
1697
+ await downloadFile(run.ctx, entry.relativePath, tempFile, {
1698
+ refuseUnsafeSymlinkTargets: run.symlinkTargetRefusal,
1699
+ symlinkTargetScope: { kind: "company", companySlug: run.ctx.slug },
1700
+ symlinkTargetLinkPath: `companies/${run.ctx.slug}/${entry.relativePath}`,
1701
+ });
1579
1702
  // The reconciled bytes are the REMOTE object (a peer replaced the one
1580
1703
  // this marker described), not the local file, so pass no absolutePath:
1581
1704
  // a 409 here is recorded, not forked (see reconcileLandedCompanySkill).
@@ -1928,7 +2051,7 @@ async function buildSharePlans(run) {
1928
2051
  if (pushEntries.some((entry) => isOntologyEntityKey(entry.relativePath))) {
1929
2052
  run.ontologyEntityPush = await resolveRunOntologyEntityPush(run);
1930
2053
  }
1931
- const pushPlan = await computePushPlanCooperatively(pushEntries, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.options.cooperativeCheckpoint, run.ontologyEntityPush);
2054
+ const pushPlan = await computePushPlanCooperatively(pushEntries, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.options.cooperativeCheckpoint, run.ontologyEntityPush, { hqRoot: run.hqRoot, syncRoot: run.syncRoot, personalMode: run.options.personalMode === true, companySlug: run.ctx.slug });
1932
2055
  const deletePlan = run.propagateDeletes === true
1933
2056
  ? await computeDeletePlan(run.journal, run.syncRoot, deleteScopeRoots, run.shouldSync, run.propagateDeletePolicy, run.ctx, run.options.personalMode !== true, run.skipSecretPaths)
1934
2057
  : { toDelete: [], toTombstone: [], refusedStale: [] };
@@ -2157,7 +2280,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2157
2280
  for (const previewItem of pushPlan.items) {
2158
2281
  if (previewItem.action !== "upload")
2159
2282
  continue;
2160
- const item = replanPushItemAtExecution(previewItem, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.ontologyEntityPush);
2283
+ const item = replanPushItemAtExecution(previewItem, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.ontologyEntityPush, false, { hqRoot: run.hqRoot, syncRoot: run.syncRoot, personalMode: run.options.personalMode === true, companySlug: run.ctx.slug });
2161
2284
  if (item?.action === "upload" && !isUploadSuppressedByTombstone(item)) {
2162
2285
  plannedUploadCandidates.push(item);
2163
2286
  }
@@ -2252,6 +2375,14 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2252
2375
  emitServerOwnedRefusal(run, item.relativePath);
2253
2376
  }
2254
2377
  }
2378
+ else if (item.action === "refuse-unsafe-symlink") {
2379
+ run.emit({
2380
+ type: "skip-unsafe-symlink-target",
2381
+ path: item.relativePath,
2382
+ classification: item.classification,
2383
+ shape: item.shape,
2384
+ });
2385
+ }
2255
2386
  else if (item.action === "skip-size-limit") {
2256
2387
  // A file over the size cap is a permanent, benign skip — NOT an error.
2257
2388
  let bytes = 0;
@@ -2299,7 +2430,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2299
2430
  return;
2300
2431
  const { absolutePath, relativePath, localHash } = item;
2301
2432
  const refreshUploadItem = () => {
2302
- const refreshed = replanPushItemAtExecution(item, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.ontologyEntityPush, true);
2433
+ const refreshed = replanPushItemAtExecution(item, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.ontologyEntityPush, true, { hqRoot: run.hqRoot, syncRoot: run.syncRoot, personalMode: run.options.personalMode === true, companySlug: run.ctx.slug });
2303
2434
  if (refreshed === null) {
2304
2435
  counters.filesSkipped++;
2305
2436
  return null;
@@ -2514,10 +2645,33 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2514
2645
  let isFreshCollision = false;
2515
2646
  let freshContentConverged = false;
2516
2647
  if (!journalEntry && item.kind === "symlink") {
2648
+ const remoteTarget = await readVerifiedRemoteSymlinkTarget(run.ctx, relativePath);
2649
+ const localTarget = fs.readlinkSync(absolutePath);
2650
+ if (remoteTarget !== null && materializedTargetMatchesWire(absolutePath, localTarget, remoteTarget)) {
2651
+ const localStat = fs.lstatSync(absolutePath);
2652
+ updateEntry(run.journal, relativePath, hashSymlinkTarget(localTarget), 0, "down", absolutePath, {
2653
+ remoteEtag: remoteMeta.etag,
2654
+ mtimeMs: localStat.mtimeMs,
2655
+ ctimeMs: localStat.ctimeMs,
2656
+ kind: "symlink",
2657
+ wireSymlinkTarget: remoteTarget,
2658
+ });
2659
+ flushShareJournal(run);
2660
+ counters.filesSkipped++;
2661
+ return;
2662
+ }
2517
2663
  isFreshCollision = true;
2518
2664
  }
2519
2665
  else if (!journalEntry && item.kind === "file") {
2520
- const remoteDiffers = await remoteContentDiffers(run.ctx, relativePath, localHash, run.hqRoot);
2666
+ const remoteDiffers = await remoteContentDiffers(run.ctx, relativePath, localHash, run.hqRoot, {
2667
+ refuseUnsafeSymlinkTargets: run.symlinkTargetRefusal,
2668
+ symlinkTargetScope: run.options.personalMode === true
2669
+ ? { kind: "personal" }
2670
+ : { kind: "company", companySlug: run.ctx.slug },
2671
+ symlinkTargetLinkPath: run.options.personalMode === true
2672
+ ? `personal/${relativePath}`
2673
+ : `companies/${run.ctx.slug}/${relativePath}`,
2674
+ });
2521
2675
  if (remoteDiffers) {
2522
2676
  isFreshCollision = true;
2523
2677
  }
@@ -2831,6 +2985,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
2831
2985
  mtimeMs,
2832
2986
  ctimeMs,
2833
2987
  kind: item.kind,
2988
+ ...(item.kind === "symlink" ? { wireSymlinkTarget: item.target } : {}),
2834
2989
  skillMetadataPending: skillContentAtPut !== undefined &&
2835
2990
  skillRegistrarForRun(run) !== undefined,
2836
2991
  });
@@ -3153,7 +3308,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
3153
3308
  continue;
3154
3309
  }
3155
3310
  const previewItem = queue[0];
3156
- const item = replanPushItemAtExecution(previewItem, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.ontologyEntityPush);
3311
+ const item = replanPushItemAtExecution(previewItem, run.journal, run.skipUnchanged === true, run.options.onHeartbeat, run.ontologyEntityPush, false, { hqRoot: run.hqRoot, syncRoot: run.syncRoot, personalMode: run.options.personalMode === true, companySlug: run.ctx.slug });
3157
3312
  if (item === null) {
3158
3313
  queue.shift();
3159
3314
  counters.filesSkipped++;
@@ -3330,7 +3485,16 @@ async function probeRemoteForWinner(run, item, remoteMtime) {
3330
3485
  let decision;
3331
3486
  try {
3332
3487
  fs.mkdirSync(path.dirname(probeAbs), { recursive: true });
3333
- const downloaded = await downloadFile(run.ctx, item.relativePath, probeAbs, run.options.transferPriority === "realtime" ? { bypassBandwidthGovernor: true } : {});
3488
+ const downloaded = await downloadFile(run.ctx, item.relativePath, probeAbs, {
3489
+ ...(run.options.transferPriority === "realtime" ? { bypassBandwidthGovernor: true } : {}),
3490
+ refuseUnsafeSymlinkTargets: run.symlinkTargetRefusal,
3491
+ symlinkTargetScope: run.options.personalMode === true
3492
+ ? { kind: "personal" }
3493
+ : { kind: "company", companySlug: run.ctx.slug },
3494
+ symlinkTargetLinkPath: run.options.personalMode === true
3495
+ ? `personal/${item.relativePath}`
3496
+ : `companies/${run.ctx.slug}/${item.relativePath}`,
3497
+ });
3334
3498
  fetched = { abs: probeAbs, downloaded };
3335
3499
  if (item.kind === "file" && !fs.lstatSync(probeAbs).isSymbolicLink()) {
3336
3500
  const localStat = fs.lstatSync(item.absolutePath);
@@ -3451,7 +3615,16 @@ async function adoptRemoteOnPush(run, item, remoteHash, decision, probe) {
3451
3615
  downloaded = probeBody.downloaded;
3452
3616
  }
3453
3617
  else {
3454
- downloaded = await downloadFile(run.ctx, item.relativePath, item.absolutePath, run.options.transferPriority === "realtime" ? { bypassBandwidthGovernor: true } : {});
3618
+ downloaded = await downloadFile(run.ctx, item.relativePath, item.absolutePath, {
3619
+ ...(run.options.transferPriority === "realtime" ? { bypassBandwidthGovernor: true } : {}),
3620
+ refuseUnsafeSymlinkTargets: run.symlinkTargetRefusal,
3621
+ symlinkTargetScope: run.options.personalMode === true
3622
+ ? { kind: "personal" }
3623
+ : { kind: "company", companySlug: run.ctx.slug },
3624
+ symlinkTargetLinkPath: run.options.personalMode === true
3625
+ ? `personal/${item.relativePath}`
3626
+ : `companies/${run.ctx.slug}/${item.relativePath}`,
3627
+ });
3455
3628
  }
3456
3629
  remoteAdopted = true;
3457
3630
  const localLstat = fs.lstatSync(item.absolutePath);
@@ -3469,6 +3642,9 @@ async function adoptRemoteOnPush(run, item, remoteHash, decision, probe) {
3469
3642
  ctimeMs: localLstat.ctimeMs,
3470
3643
  ...(createdBySub !== undefined ? { createdBySub } : {}),
3471
3644
  kind: isLocalSymlink ? "symlink" : "file",
3645
+ ...(isLocalSymlink && downloaded?.symlinkTarget !== undefined
3646
+ ? { wireSymlinkTarget: downloaded.symlinkTarget }
3647
+ : {}),
3472
3648
  });
3473
3649
  recordPushConflict(run, item, {
3474
3650
  detectedAt,