@hamp10/agentforge 0.2.34 → 0.2.36

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.36",
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,55 @@ 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
+ }
639
+ const knownProjectsRoot = mkdtempSync(path.join(tmpdir(), 'agentforge-known-projects-'));
640
+ const knownAgentWorkspace = mkdtempSync(path.join(tmpdir(), 'agentforge-known-agent-workspace-'));
641
+ try {
642
+ const knownProject = path.join(knownProjectsRoot, 'Hamp.com');
643
+ const knownDomainsDir = path.join(knownProject, 'public_html', 'domains');
644
+ mkdirSync(knownDomainsDir, { recursive: true });
645
+ writeFileSync(path.join(knownDomainsDir, 'agentforge-ai.html'), '<!doctype html><html><body><h1>AgentForge.ai</h1></body></html>');
646
+ writeFileSync(path.join(knownDomainsDir, 'adgenius-ai.html'), '<!doctype html><html><body><h1>AdGenius.ai</h1></body></html>');
647
+ symlinkSync(knownProject, path.join(knownAgentWorkspace, 'Hamp.com'), 'dir');
648
+ writeFileSync(path.join(knownAgentWorkspace, 'AGENTFORGE.md'), '# AgentForge.ai platform notes');
649
+ const knownProjectCli = Object.create(OpenClawCLI.prototype);
650
+ knownProjectCli._knownProjectsRoots = () => [knownProjectsRoot];
651
+ const mixedKnownProjectTask = 'Can you make Hamp.com listing pages for AgentBoard.ai and Scrimmage.ai from scratch, and fix the AgentForge.ai listing page?';
652
+ assert.doesNotThrow(
653
+ () => knownProjectCli._guardDirectFileWritePath(
654
+ path.join(knownAgentWorkspace, 'Hamp.com', 'public_html', 'domains', 'agentboard-ai.html'),
655
+ knownAgentWorkspace,
656
+ { task: mixedKnownProjectTask }
657
+ ),
658
+ 'agent-workspace root files such as AGENTFORGE.md should not count as scoped project pages when the target resolves into a known project'
659
+ );
660
+ assert.throws(
661
+ () => knownProjectCli._guardDirectFileWritePath(
662
+ path.join(knownAgentWorkspace, 'Hamp.com', 'Hamp.com', 'public_html', 'domains', 'agentboard-ai.html'),
663
+ knownAgentWorkspace,
664
+ { task: mixedKnownProjectTask }
665
+ ),
666
+ /nested duplicate of the current project/i,
667
+ 'known-project symlink writes should still reject same-name nested project-copy paths from temp agent workspaces'
668
+ );
669
+ } finally {
670
+ rmSync(knownProjectsRoot, { recursive: true, force: true });
671
+ rmSync(knownAgentWorkspace, { recursive: true, force: true });
672
+ }
615
673
  assert.equal(
616
674
  cli._directScopeFileCandidates(fixture.repo, ['gamma']).some(candidate => candidate.startsWith(`${path.basename(fixture.repo)}/`)),
617
675
  false,
@@ -381,9 +381,62 @@ 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
- return this._knownProjectsRoots().find(root => existsSync(root) && this._isPathInside(candidate, root)) || null;
418
+ const realCandidate = this._realPathForMaybeMissingPath(candidate);
419
+ return this._knownProjectsRoots().find(root => {
420
+ if (!existsSync(root)) return false;
421
+ const realRoot = this._realPathForMaybeMissingPath(root);
422
+ return this._isPathInside(realCandidate, realRoot);
423
+ }) || null;
424
+ }
425
+
426
+ _projectDirForKnownProjectPath(candidate) {
427
+ if (!candidate) return null;
428
+ const realCandidate = this._realPathForMaybeMissingPath(candidate);
429
+ const projectsRoot = this._projectsRootForPath(realCandidate);
430
+ if (!projectsRoot) return null;
431
+ let realProjectsRoot = projectsRoot;
432
+ try {
433
+ realProjectsRoot = realpathSync(projectsRoot);
434
+ } catch { /* non-fatal */ }
435
+ const relative = path.relative(realProjectsRoot, realCandidate);
436
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
437
+ const [projectName] = relative.split(path.sep).filter(Boolean);
438
+ if (!projectName) return null;
439
+ return path.join(realProjectsRoot, projectName);
387
440
  }
388
441
 
389
442
  _isProjectScopedWorkDir(workDir) {
@@ -867,8 +920,8 @@ export class OpenClawCLI extends EventEmitter {
867
920
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
868
921
  const [firstPart] = relative.split(path.sep).filter(Boolean);
869
922
  if (!firstPart || firstPart.toLowerCase() !== path.basename(workDir).toLowerCase()) return false;
870
- if (this._gitTracksPathOrPrefix(workDir, relative, firstPart)) return false;
871
- return true;
923
+ const canonicalPath = this._nestedProjectCopyCanonicalPath(filePath, workDir);
924
+ return !!canonicalPath && (existsSync(canonicalPath) || existsSync(path.dirname(canonicalPath)));
872
925
  }
873
926
 
874
927
  _gitTracksPathOrPrefix(workDir, relativePath, prefix = '') {
@@ -912,11 +965,11 @@ export class OpenClawCLI extends EventEmitter {
912
965
 
913
966
  _nestedProjectDuplicateCanonicalPath(filePath, workDir) {
914
967
  const canonicalPath = this._nestedProjectCopyCanonicalPath(filePath, workDir);
915
- if (!canonicalPath || canonicalPath === filePath || !existsSync(canonicalPath)) return null;
968
+ if (!canonicalPath || canonicalPath === filePath) return null;
916
969
  if (!this._isDirectPageSourcePath(filePath) || !this._isDirectPageSourcePath(canonicalPath)) return null;
917
970
  const canonicalRel = path.relative(workDir, canonicalPath);
918
971
  if (!canonicalRel || canonicalRel.startsWith('..') || path.isAbsolute(canonicalRel)) return null;
919
- if (!this._gitTracksPathOrPrefix(workDir, canonicalRel, canonicalRel.split(path.sep)[0])) return null;
972
+ if (!existsSync(canonicalPath) && !existsSync(path.dirname(canonicalPath))) return null;
920
973
  return canonicalPath;
921
974
  }
922
975
 
@@ -956,6 +1009,15 @@ export class OpenClawCLI extends EventEmitter {
956
1009
  }
957
1010
 
958
1011
  _guardDirectFileWritePath(filePath, workDir, options = {}) {
1012
+ const realFilePath = this._realPathForMaybeMissingPath(filePath);
1013
+ const projectDirForFile = this._projectDirForKnownProjectPath(realFilePath);
1014
+ if (projectDirForFile && this._isPathInside(realFilePath, projectDirForFile)) {
1015
+ filePath = realFilePath;
1016
+ workDir = projectDirForFile;
1017
+ } else {
1018
+ filePath = this._canonicalizeLinkedProjectPath(filePath, workDir);
1019
+ }
1020
+ workDir = this._realPathForMaybeMissingPath(workDir);
959
1021
  const { slugs, pageOnly } = this._extractDirectExplicitScope(options.task);
960
1022
  const relative = path.relative(workDir, filePath);
961
1023
  const relativePath = (!relative || relative.startsWith('..') || path.isAbsolute(relative))
@@ -967,7 +1029,7 @@ export class OpenClawCLI extends EventEmitter {
967
1029
  : null;
968
1030
  if (duplicateCanonicalPath) {
969
1031
  throw this._directScopeViolationError([
970
- 'Refusing to write into a nested duplicate of the current project when the canonical target file also exists.',
1032
+ 'Refusing to write into a nested duplicate of the current project.',
971
1033
  `Workspace: ${workDir}`,
972
1034
  `Target: ${filePath}`,
973
1035
  `Use the canonical project path instead: ${duplicateCanonicalPath}`,