mirror of
https://github.com/sickn33/agentic-awesome-skills.git
synced 2026-09-14 18:06:35 +08:00
fix: ship reviewed CLI file safety and security release notes (#1409)
# Pull Request Description Integrate the reviewed CLI JSON-read hardening from #1398 and prepare the 16.9.1 security release notes. The original contributor commits are retained in this branch. The fork PR cannot be approved by the protected runtime-path policy; this owner-authored PR uses the exact-head maintainer review path without changing that policy. Validation and reads now use the same file descriptor, reject a replaced file or raced FIFO, and enforce the byte limit even when the file grows. The input error contract and ordinary CLI behavior remain unchanged. No new runtime imports are introduced. Validation: 25 focused CLI tests and all 126 repository test files pass, plus validation, reference and documentation-security checks. Packed Linux/Windows verification is required before release. Co-authored-by: Beverly621 <205182701+Beverly621@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [16.9.1] - 2026-09-06 - "Security Fixes and Safer Local Files"
|
||||
|
||||
A corrective release for AAS users on Claude Code, Cursor, Codex CLI and Gemini CLI.
|
||||
It distributes the dependency and local-file fixes integrated after 16.9.0,
|
||||
without changing agent-owned skill selection or the 2,113-skill catalog.
|
||||
|
||||
### Start here
|
||||
|
||||
- Install: `npx agentic-awesome-skills@16.9.1`
|
||||
- [Choose your tool](README.md#choose-your-tool)
|
||||
- [Best skills by tool](README.md#best-skills-by-tool)
|
||||
- [Bundles](docs/users/bundles.md)
|
||||
- [Workflows](docs/users/workflows.md)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bind CLI JSON validation and bounded reads to the same file descriptor;
|
||||
reject file replacement, raced FIFOs and growth beyond the input budget.
|
||||
- Preserve private local state and reject unsafe linked files in VideoDB,
|
||||
profile export and Instagram/NotebookLM configuration; avoid blocking on
|
||||
non-regular files during type inventory.
|
||||
- Update Pillow, SoupSieve and setuptools requirements. Use manifest-local
|
||||
Python settings so Snyk resolves the supported dependency versions.
|
||||
- Lock Telegram and WhatsApp Node templates to patched qs dependencies and
|
||||
synchronize the corresponding plugin copies.
|
||||
|
||||
Thanks to @Beverly621 for the CLI JSON-read hardening in #1398.
|
||||
|
||||
### Validation scope
|
||||
|
||||
All 66 dependency findings in the reconciled Snyk group export were resolved,
|
||||
including SpendWise findings addressed in its separate repository. Contextual
|
||||
Code and license-policy findings remain visible; this release does not claim
|
||||
zero static-analysis findings or Windows ACL verification.
|
||||
|
||||
## [16.9.0] - 2026-09-06 - "From Agent Selection to Verified Installation"
|
||||
|
||||
> Connects exact agent-selected skills to a reviewable installation command,
|
||||
|
||||
@@ -106,13 +106,42 @@ function boundedInteger(value, field, minimum, maximum, fallback) {
|
||||
|
||||
function readJsonFile(filePath, maximumBytes = 4 * 1024 * 1024) {
|
||||
const absolute = path.resolve(filePath);
|
||||
const stat = fs.lstatSync(absolute);
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > maximumBytes) {
|
||||
throw cliError("AAS_CLI_JSON_FILE_UNSAFE", "invalidInput", {});
|
||||
const unsafe = () => cliError("AAS_CLI_JSON_FILE_UNSAFE", "invalidInput", {});
|
||||
const prior = fs.lstatSync(absolute);
|
||||
if (!prior.isFile() || prior.isSymbolicLink() || prior.nlink !== 1 || prior.size > maximumBytes) {
|
||||
throw unsafe();
|
||||
}
|
||||
let descriptor;
|
||||
try {
|
||||
// Nonblocking open prevents a raced FIFO from stalling before fstat can reject it.
|
||||
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0);
|
||||
descriptor = fs.openSync(absolute, flags);
|
||||
const before = fs.fstatSync(descriptor);
|
||||
if (!before.isFile() || before.nlink !== 1 || before.dev !== prior.dev || before.ino !== prior.ino
|
||||
|| before.size !== prior.size || before.size > maximumBytes) throw unsafe();
|
||||
const bytes = Buffer.alloc(before.size);
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
|
||||
if (!count) throw unsafe();
|
||||
offset += count;
|
||||
}
|
||||
const after = fs.fstatSync(descriptor);
|
||||
const current = fs.lstatSync(absolute);
|
||||
if (!after.isFile() || after.nlink !== 1 || after.dev !== before.dev
|
||||
|| after.ino !== before.ino || after.size !== before.size
|
||||
|| after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs
|
||||
|| !current.isFile() || current.isSymbolicLink() || current.nlink !== 1
|
||||
|| current.dev !== before.dev || current.ino !== before.ino || current.size !== before.size) throw unsafe();
|
||||
const text = bytes.toString("utf8");
|
||||
scanJson(text, 64);
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
if (error.code === "ELOOP" || (descriptor !== undefined && error.code === "ENOENT")) throw unsafe();
|
||||
throw error;
|
||||
} finally {
|
||||
if (descriptor !== undefined) fs.closeSync(descriptor);
|
||||
}
|
||||
const text = fs.readFileSync(absolute, "utf8");
|
||||
scanJson(text, 64);
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
function writeNewJson(filePath, value, { previewWindowsOutput = false } = {}) {
|
||||
|
||||
@@ -10,6 +10,7 @@ const core = require("../../lib/aas-v1");
|
||||
const {
|
||||
execute,
|
||||
main,
|
||||
readJsonFile,
|
||||
windowsOutputDurabilityDetails,
|
||||
writeNewStackArtifactDirectory,
|
||||
} = require("../../lib/aas-v1/cli/main");
|
||||
@@ -317,6 +318,95 @@ test("CLI normalizes native file errors without throwing or exposing paths", asy
|
||||
}
|
||||
});
|
||||
|
||||
test("CLI JSON reads reject a file replaced between inspection and open", (context) => {
|
||||
const item = fixture();
|
||||
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
|
||||
const input = path.join(item.root, "input.json");
|
||||
const replacement = path.join(item.root, "replacement.json");
|
||||
fs.writeFileSync(input, '{"value":"original"}\n');
|
||||
fs.writeFileSync(replacement, '{"value":"replacement"}\n');
|
||||
const originalLstatSync = fs.lstatSync;
|
||||
context.after(() => { fs.lstatSync = originalLstatSync; });
|
||||
let replaced = false;
|
||||
fs.lstatSync = function lstatAndReplace(candidate, ...args) {
|
||||
const stat = originalLstatSync(candidate, ...args);
|
||||
if (candidate === input && !replaced) {
|
||||
replaced = true;
|
||||
fs.renameSync(input, path.join(item.root, "inspected.json"));
|
||||
fs.renameSync(replacement, input);
|
||||
}
|
||||
return stat;
|
||||
};
|
||||
|
||||
assert.throws(() => readJsonFile(input), { code: "AAS_CLI_JSON_FILE_UNSAFE" });
|
||||
});
|
||||
|
||||
test("CLI JSON reads do not block when the inspected file becomes a FIFO", (context) => {
|
||||
if (process.platform === "win32") {
|
||||
context.skip("POSIX FIFO behavior");
|
||||
return;
|
||||
}
|
||||
const item = fixture();
|
||||
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
|
||||
const input = path.join(item.root, "input.json");
|
||||
const modulePath = path.join(ROOT, "tools/lib/aas-v1/cli/main.js");
|
||||
const probe = `
|
||||
const fs = require("node:fs");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const [input, modulePath] = process.argv.slice(1);
|
||||
const { readJsonFile } = require(modulePath);
|
||||
fs.writeFileSync(input, "{}\\n");
|
||||
const originalLstatSync = fs.lstatSync;
|
||||
let replaced = false;
|
||||
fs.lstatSync = function lstatAndReplace(candidate, ...args) {
|
||||
const stat = originalLstatSync(candidate, ...args);
|
||||
if (candidate === input && !replaced) {
|
||||
replaced = true;
|
||||
fs.renameSync(input, input + ".original");
|
||||
const result = spawnSync("mkfifo", [input], { encoding: "utf8" });
|
||||
if (result.status !== 0) throw new Error(result.stderr || "mkfifo failed");
|
||||
}
|
||||
return stat;
|
||||
};
|
||||
try {
|
||||
readJsonFile(input);
|
||||
process.exitCode = 2;
|
||||
} catch (error) {
|
||||
if (error.code !== "AAS_CLI_JSON_FILE_UNSAFE") {
|
||||
console.error(error);
|
||||
process.exitCode = 3;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = spawnSync(process.execPath, ["-e", probe, input, modulePath], {
|
||||
encoding: "utf8",
|
||||
timeout: 2_000,
|
||||
});
|
||||
assert.notEqual(result.error?.code, "ETIMEDOUT", "opening the raced FIFO must not block");
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
});
|
||||
|
||||
test("CLI JSON reads enforce the byte limit when a file grows after inspection", (context) => {
|
||||
const item = fixture();
|
||||
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
|
||||
const input = path.join(item.root, "input.json");
|
||||
fs.writeFileSync(input, "{}\n");
|
||||
const originalLstatSync = fs.lstatSync;
|
||||
context.after(() => { fs.lstatSync = originalLstatSync; });
|
||||
let grown = false;
|
||||
fs.lstatSync = function lstatAndGrow(candidate, ...args) {
|
||||
const stat = originalLstatSync(candidate, ...args);
|
||||
if (candidate === input && !grown) {
|
||||
grown = true;
|
||||
fs.writeFileSync(input, `${JSON.stringify({ padding: "x".repeat(4096) })}\n`);
|
||||
}
|
||||
return stat;
|
||||
};
|
||||
|
||||
assert.throws(() => readJsonFile(input, 64), { code: "AAS_CLI_JSON_FILE_UNSAFE" });
|
||||
});
|
||||
|
||||
test("CLI reports an invalid stack manifest as invalid input", async (context) => {
|
||||
const item = fixture();
|
||||
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
|
||||
|
||||
@@ -680,6 +680,10 @@ The packed preview matrix now includes Windows with PowerShell execution and rea
|
||||
|
||||
The Windows packed verification now repeats the complete installation lifecycle under Windows PowerShell 5.1, alongside PowerShell 7, with version-bound receipts and fail-closed aggregation. The 16.9.0 changelog covers the accumulated agent-owned CLI handoff, MCP fixes and Specialized Plugin refresh. Native Windows app interaction remains outside the observed coverage.
|
||||
|
||||
## Atomic CLI JSON input reads — 2026-09-06
|
||||
|
||||
CLI JSON inputs are now opened without following leaf symlinks or blocking on raced FIFOs, checked against the pre-open file identity, read through the same descriptor with a fixed byte budget, and rechecked before parsing. Focused regressions replace the file after inspection, substitute a FIFO and grow it past the configured limit; all cases fail closed without changing normal manifest, plan, selection or evidence behavior.
|
||||
|
||||
## README Star History embeds
|
||||
|
||||
Added the official theme-aware Star History chart and live global-rank badge to the existing [Star History section](README.md#star-history), retaining the direct chart link. Rank is fetched from Star History rather than hardcoded.
|
||||
|
||||
Reference in New Issue
Block a user