@hamp10/agentforge 0.2.34 → 0.2.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -604,6 +604,15 @@ try {
604
604
  const nestedDuplicateGamma = path.join(fixture.repo, path.basename(fixture.repo), 'public_html', 'domains', 'gamma.html');
605
605
  mkdirSync(path.dirname(nestedDuplicateGamma), { recursive: true });
606
606
  writeFileSync(nestedDuplicateGamma, '<!doctype html><html><body><h1>Gamma</h1></body></html>');
607
+ assert.throws(
608
+ () => cli._guardDirectFileWritePath(
609
+ nestedDuplicateGamma,
610
+ fixture.repo,
611
+ { task: 'Can you make listing pages for Gamma?' }
612
+ ),
613
+ /nested duplicate of the current project/i,
614
+ 'same-name nested project-copy paths should be blocked even when the nested copy is tracked or preexisting'
615
+ );
607
616
  assert.doesNotThrow(
608
617
  () => cli._guardDirectFileWritePath(
609
618
  path.join(fixture.repo, 'public_html', 'domains', 'gamma.html'),
@@ -612,6 +621,21 @@ try {
612
621
  ),
613
622
  'nested duplicate project copies should not block recreating the canonical page path'
614
623
  );
624
+ const linkedWorkspace = mkdtempSync(path.join(tmpdir(), 'agentforge-linked-project-'));
625
+ try {
626
+ const linkedProjectPath = path.join(linkedWorkspace, path.basename(fixture.repo));
627
+ symlinkSync(fixture.repo, linkedProjectPath, 'dir');
628
+ assert.doesNotThrow(
629
+ () => cli._guardDirectFileWritePath(
630
+ path.join(linkedProjectPath, 'public_html', 'domains', 'gamma.html'),
631
+ fixture.repo,
632
+ { task: 'Can you make listing pages for Gamma?' }
633
+ ),
634
+ 'agent-workspace symlink paths should canonicalize to the real project before scoped page guard checks'
635
+ );
636
+ } finally {
637
+ rmSync(linkedWorkspace, { recursive: true, force: true });
638
+ }
615
639
  assert.equal(
616
640
  cli._directScopeFileCandidates(fixture.repo, ['gamma']).some(candidate => candidate.startsWith(`${path.basename(fixture.repo)}/`)),
617
641
  false,
@@ -381,6 +381,38 @@ export class OpenClawCLI extends EventEmitter {
381
381
  return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
382
382
  }
383
383
 
384
+ _realPathForMaybeMissingPath(targetPath) {
385
+ const resolved = path.resolve(String(targetPath || ''));
386
+ if (!resolved) return resolved;
387
+ if (existsSync(resolved)) {
388
+ try {
389
+ return realpathSync(resolved);
390
+ } catch {
391
+ return resolved;
392
+ }
393
+ }
394
+ const missingParts = [];
395
+ let cursor = resolved;
396
+ while (cursor && !existsSync(cursor)) {
397
+ const parent = path.dirname(cursor);
398
+ if (!parent || parent === cursor) break;
399
+ missingParts.unshift(path.basename(cursor));
400
+ cursor = parent;
401
+ }
402
+ try {
403
+ return path.join(realpathSync(cursor), ...missingParts);
404
+ } catch {
405
+ return resolved;
406
+ }
407
+ }
408
+
409
+ _canonicalizeLinkedProjectPath(filePath, workDir) {
410
+ if (!filePath || !workDir) return filePath;
411
+ const realFilePath = this._realPathForMaybeMissingPath(filePath);
412
+ const realWorkDir = this._realPathForMaybeMissingPath(workDir);
413
+ return this._isPathInside(realFilePath, realWorkDir) ? realFilePath : filePath;
414
+ }
415
+
384
416
  _projectsRootForPath(candidate) {
385
417
  if (!candidate) return null;
386
418
  return this._knownProjectsRoots().find(root => existsSync(root) && this._isPathInside(candidate, root)) || null;
@@ -867,8 +899,8 @@ export class OpenClawCLI extends EventEmitter {
867
899
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
868
900
  const [firstPart] = relative.split(path.sep).filter(Boolean);
869
901
  if (!firstPart || firstPart.toLowerCase() !== path.basename(workDir).toLowerCase()) return false;
870
- if (this._gitTracksPathOrPrefix(workDir, relative, firstPart)) return false;
871
- return true;
902
+ const canonicalPath = this._nestedProjectCopyCanonicalPath(filePath, workDir);
903
+ return !!canonicalPath && (existsSync(canonicalPath) || existsSync(path.dirname(canonicalPath)));
872
904
  }
873
905
 
874
906
  _gitTracksPathOrPrefix(workDir, relativePath, prefix = '') {
@@ -912,11 +944,11 @@ export class OpenClawCLI extends EventEmitter {
912
944
 
913
945
  _nestedProjectDuplicateCanonicalPath(filePath, workDir) {
914
946
  const canonicalPath = this._nestedProjectCopyCanonicalPath(filePath, workDir);
915
- if (!canonicalPath || canonicalPath === filePath || !existsSync(canonicalPath)) return null;
947
+ if (!canonicalPath || canonicalPath === filePath) return null;
916
948
  if (!this._isDirectPageSourcePath(filePath) || !this._isDirectPageSourcePath(canonicalPath)) return null;
917
949
  const canonicalRel = path.relative(workDir, canonicalPath);
918
950
  if (!canonicalRel || canonicalRel.startsWith('..') || path.isAbsolute(canonicalRel)) return null;
919
- if (!this._gitTracksPathOrPrefix(workDir, canonicalRel, canonicalRel.split(path.sep)[0])) return null;
951
+ if (!existsSync(canonicalPath) && !existsSync(path.dirname(canonicalPath))) return null;
920
952
  return canonicalPath;
921
953
  }
922
954
 
@@ -956,6 +988,8 @@ export class OpenClawCLI extends EventEmitter {
956
988
  }
957
989
 
958
990
  _guardDirectFileWritePath(filePath, workDir, options = {}) {
991
+ filePath = this._canonicalizeLinkedProjectPath(filePath, workDir);
992
+ workDir = this._realPathForMaybeMissingPath(workDir);
959
993
  const { slugs, pageOnly } = this._extractDirectExplicitScope(options.task);
960
994
  const relative = path.relative(workDir, filePath);
961
995
  const relativePath = (!relative || relative.startsWith('..') || path.isAbsolute(relative))
@@ -967,7 +1001,7 @@ export class OpenClawCLI extends EventEmitter {
967
1001
  : null;
968
1002
  if (duplicateCanonicalPath) {
969
1003
  throw this._directScopeViolationError([
970
- 'Refusing to write into a nested duplicate of the current project when the canonical target file also exists.',
1004
+ 'Refusing to write into a nested duplicate of the current project.',
971
1005
  `Workspace: ${workDir}`,
972
1006
  `Target: ${filePath}`,
973
1007
  `Use the canonical project path instead: ${duplicateCanonicalPath}`,