@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 +1 -1
- package/scripts/check-task-semantics.js +58 -0
- package/src/OpenClawCLI.js +68 -6
package/package.json
CHANGED
|
@@ -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,
|
package/src/OpenClawCLI.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
871
|
-
return
|
|
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
|
|
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 (!
|
|
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
|
|
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}`,
|