Skip to content

chore: sync core lib and CLAUDE.md from agent-core#28

Closed
avifenesh wants to merge 1 commit into
mainfrom
chore/sync-core-sync-docs-20260426-130758
Closed

chore: sync core lib and CLAUDE.md from agent-core#28
avifenesh wants to merge 1 commit into
mainfrom
chore/sync-core-sync-docs-20260426-130758

Conversation

@avifenesh

@avifenesh avifenesh commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Automated sync of lib/ and CLAUDE.md from agent-core.


Note

Medium Risk
Touches the runtime download/install path for the agent-analyzer binary, so mistakes could break installs across platforms. Risk is mitigated by extensive new tests, but the new checksum/validation and Windows PowerShell extraction flow are behavior-changing.

Overview
Adds runtime SHA-256 verification for downloaded agent-analyzer release assets by fetching and enforcing the adjacent .sha256 sidecar (with an opt-in skipChecksum escape hatch for local dev only).

Hardens installation by extracting archives into an isolated scratch directory, validating archive entry paths to block traversal/absolute/UNC/drive-letter entries, rejecting symlinks, and then copying only the expected binary into ~/.agent-sh/bin.

Reworks Windows .zip handling to avoid Expand-Archive/command-string parsing by running a dedicated PowerShell script via -File and environment variables, with entry-by-entry validation before extraction.

Adds a comprehensive lib/binary/index.test.js suite covering checksum parsing/verification, path safety checks, scratch cleanup, and basic tar/zip extraction behavior.

Reviewed by Cursor Bugbot for commit 61d39b5. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces security hardening for the binary downloader and extractor, adding SHA-256 checksum verification and strict archive entry validation to prevent path traversal and symlink attacks. Feedback was provided regarding a potential resource leak in the extractZipToScratch function, where a failure during temporary directory creation could result in orphaned directories, and a more robust cleanup pattern was suggested.

Comment thread lib/binary/index.js
Comment on lines +503 to +556
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-zip-'));
const tmpZip = path.join(scratch, '__archive.zip');
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-ps-'));
const scriptPath = path.join(scriptDir, 'extract.ps1');

try {
fs.writeFileSync(tmpZip, buf);
fs.writeFileSync(scriptPath, EXTRACT_ZIP_PS1, 'utf8');

await new Promise(function(resolve, reject) {
const child = cp.execFile(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy', 'Bypass',
'-File', scriptPath
],
{
windowsHide: true,
env: Object.assign({}, process.env, {
SRC_ZIP: tmpZip,
DEST_DIR: scratch
})
},
function(err, _stdout, stderr) {
if (err) {
reject(new Error('zip extraction failed: ' + (stderr || err.message)));
} else {
resolve();
}
}
);
// Do not write to stdin; the script reads from env.
if (child.stdin) child.stdin.end();
});
ps.on('error', reject);
});

try { fs.unlinkSync(tmpZip); } catch (e) { /* ignore */ }

// Defense in depth: walkFiles() throws on any symlink/junction. Also
// confirm every file resolves inside scratch.
const files = walkFiles(scratch);
for (let i = 0; i < files.length; i++) {
assertInsideRoot(scratch, files[i]);
}
} catch (err) {
rmrf(scratch);
throw err;
} finally {
rmrf(scriptDir);
}

return scratch;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of extractZipToScratch has a potential resource leak. If the second fs.mkdtempSync call (for scriptDir) fails, the scratch directory created just before it will not be cleaned up because the code hasn't entered the try block yet. Additionally, the finally block calls rmrf(scriptDir) without checking if scriptDir was successfully initialized, which could lead to a TypeError masking the original error. Moving the resource initialization inside the try block and adding a check in finally ensures robust cleanup.

  let scratch;
  let scriptDir;
  try {
    scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-zip-'));
    const tmpZip = path.join(scratch, '__archive.zip');
    scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-ps-'));
    const scriptPath = path.join(scriptDir, 'extract.ps1');

    fs.writeFileSync(tmpZip, buf);
    fs.writeFileSync(scriptPath, EXTRACT_ZIP_PS1, 'utf8');

    await new Promise(function(resolve, reject) {
      const child = cp.execFile(
        'powershell.exe',
        [
          '-NoProfile',
          '-NonInteractive',
          '-ExecutionPolicy', 'Bypass',
          '-File', scriptPath
        ],
        {
          windowsHide: true,
          env: Object.assign({}, process.env, {
            SRC_ZIP: tmpZip,
            DEST_DIR: scratch
          })
        },
        function(err, _stdout, stderr) {
          if (err) {
            reject(new Error('zip extraction failed: ' + (stderr || err.message)));
          } else {
            resolve();
          }
        }
      );
      // Do not write to stdin; the script reads from env.
      if (child.stdin) child.stdin.end();
    });

    try { fs.unlinkSync(tmpZip); } catch (e) { /* ignore */ }

    // Defense in depth: walkFiles() throws on any symlink/junction. Also
    // confirm every file resolves inside scratch.
    const files = walkFiles(scratch);
    for (let i = 0; i < files.length; i++) {
      assertInsideRoot(scratch, files[i]);
    }

    const result = scratch;
    scratch = null; // Prevent cleanup in catch
    return result;
  } catch (err) {
    if (scratch) rmrf(scratch);
    throw err;
  } finally {
    if (scriptDir) rmrf(scriptDir);
  }
}

@avifenesh

Copy link
Copy Markdown
Contributor Author

Superseded by fresh sync after agent-core upstreamed missing files (workflow-state.js full version + lib/repo-intel/queries.js). Close older PR to avoid conflicts.

@avifenesh avifenesh closed this Apr 26, 2026
@avifenesh
avifenesh deleted the chore/sync-core-sync-docs-20260426-130758 branch April 26, 2026 13:12

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 61d39b5. Configure here.

Comment thread lib/binary/index.js
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-zip-'));
const tmpZip = path.join(scratch, '__archive.zip');
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-ps-'));
const scriptPath = path.join(scriptDir, 'extract.ps1');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch directory leaks if scriptDir creation fails

Low Severity

In extractZipToScratch, the scratch directory is created at line 503 and scriptDir at line 505, both before the try block begins at line 508. If the second fs.mkdtempSync call throws (e.g. disk full), execution skips the try-catch-finally entirely, so scratch is never cleaned up. The caller's finally block also can't help because its scratch variable was never assigned. By contrast, extractTarGzToScratch correctly creates only one temp directory right before its try block.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 61d39b5. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant