mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(skills): restore executable bit on bundled whisper script + release-time check (#41351)
* Enforce executable shell scripts in bundled skills * fix: format CONTRIBUTING.md (oxfmt trailing whitespace) * fix: skip shell script executable check on Windows Windows does not support Unix permission bits — chmod is a no-op and statSync().mode never reports execute bits. Skip the runtime check and the corresponding tests on win32. * style: restore contributing formatting * chore(ci): refresh detect-secrets baseline * fix(skills): mark video-frames frame script executable * fix: revert unrelated CI/secrets changes from whisper chmod PR * chore(ci): retrigger full PR checks * test: annotate executable-bit regression suite * test(tts): mock resolveModelAsync in summarizeText tests * test(whatsapp): make append history test use stale timestamp * test(models): tolerate registry loader option expansion * docs: add changelog for bundled skill executable fix * fix(config): allow partial Codex web search location * Drop unrelated formatting from PR 41351 * Fix bundled plugin bridge source expectation * test: restore bundled plugin bridge npm expectation --------- Co-authored-by: xaeon2026 <xaeon2026@gmail.com> Co-authored-by: Jackal Xin <jackal092927@users.noreply.github.com> Co-authored-by: xaeon2026 <xaeon2026@users.noreply.github.com>
This commit is contained in:
@@ -3133,6 +3133,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Skills/OpenAI Whisper: restore executable bits for bundled Whisper and video-frame shell helpers and add a release check for non-executable bundled skill shell scripts, so packaged installs no longer fail with permission-denied errors. Fixes #9303. Thanks @nikolasdehor.
|
||||||
- Agents/tools: skip unavailable media generation and PDF tool factories from the live reply path when Gateway metadata and the active auth store prove no configured provider can back them, while keeping explicit config and auth-backed providers on the normal factory path. Thanks @shakkernerd.
|
- Agents/tools: skip unavailable media generation and PDF tool factories from the live reply path when Gateway metadata and the active auth store prove no configured provider can back them, while keeping explicit config and auth-backed providers on the normal factory path. Thanks @shakkernerd.
|
||||||
- Agents/runtime: reuse the Gateway metadata startup plan when ensuring reply runtime plugins are loaded, so live agent turns do not broad-load plugin runtimes after the Gateway already scoped startup activation. Thanks @shakkernerd.
|
- Agents/runtime: reuse the Gateway metadata startup plan when ensuring reply runtime plugins are loaded, so live agent turns do not broad-load plugin runtimes after the Gateway already scoped startup activation. Thanks @shakkernerd.
|
||||||
- Agents/runtime: delegate scoped reply runtime registry reuse to the plugin loader cache-key compatibility checks, so config changes with the same startup plugin ids cannot keep stale runtime hooks or tools active. Thanks @shakkernerd.
|
- Agents/runtime: delegate scoped reply runtime registry reuse to the plugin loader cache-key compatibility checks, so config changes with the same startup plugin ids cannot keep stale runtime hooks or tools active. Thanks @shakkernerd.
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import {
|
|||||||
readdirSync,
|
readdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
rmSync,
|
rmSync,
|
||||||
|
statSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
|
import type { Dirent } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
@@ -144,6 +146,47 @@ export const PACKED_COMPLETION_SMOKE_ARGS = [
|
|||||||
"zsh",
|
"zsh",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export function collectSkillShellScriptExecutableErrors(rootDir = resolve(".")): string[] {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillsDir = join(rootDir, "skills");
|
||||||
|
const errors: string[] = [];
|
||||||
|
let entries: Dirent[];
|
||||||
|
try {
|
||||||
|
entries = readdirSync(skillsDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const scriptsDir = join(skillsDir, entry.name, "scripts");
|
||||||
|
let scriptEntries: Dirent[];
|
||||||
|
try {
|
||||||
|
scriptEntries = readdirSync(scriptsDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const scriptEntry of scriptEntries) {
|
||||||
|
if (!scriptEntry.isFile() || !scriptEntry.name.endsWith(".sh")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const scriptPath = join(scriptsDir, scriptEntry.name);
|
||||||
|
if ((statSync(scriptPath).mode & 0o111) === 0) {
|
||||||
|
errors.push(
|
||||||
|
`skill shell script is not executable: skills/${entry.name}/scripts/${scriptEntry.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
function collectBundledExtensions(): BundledExtension[] {
|
function collectBundledExtensions(): BundledExtension[] {
|
||||||
const extensionsDir = resolve("extensions");
|
const extensionsDir = resolve("extensions");
|
||||||
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
|
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
|
||||||
@@ -189,6 +232,17 @@ function checkBundledExtensionMetadata() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkSkillShellScriptsExecutable() {
|
||||||
|
const errors = collectSkillShellScriptExecutableErrors();
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error("release-check: skill shell script permission validation failed:");
|
||||||
|
for (const error of errors) {
|
||||||
|
console.error(` - ${error}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function runPackDry(): PackResult[] {
|
function runPackDry(): PackResult[] {
|
||||||
const raw = execSync("npm pack --dry-run --json --ignore-scripts", {
|
const raw = execSync("npm pack --dry-run --json --ignore-scripts", {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
@@ -865,6 +919,7 @@ function runCriticalPluginSdkEntrypointImportSmoke() {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
checkAppcastSparkleVersions();
|
checkAppcastSparkleVersions();
|
||||||
|
checkSkillShellScriptsExecutable();
|
||||||
checkCliBootstrapExternalImports({
|
checkCliBootstrapExternalImports({
|
||||||
logger: {
|
logger: {
|
||||||
error: (message: string) => console.error(`release-check: ${message}`),
|
error: (message: string) => console.error(`release-check: ${message}`),
|
||||||
|
|||||||
0
skills/openai-whisper-api/scripts/transcribe.sh
Normal file → Executable file
0
skills/openai-whisper-api/scripts/transcribe.sh
Normal file → Executable file
0
skills/video-frames/scripts/frame.sh
Normal file → Executable file
0
skills/video-frames/scripts/frame.sh
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { bundledDistPluginFile, bundledPluginFile } from "openclaw/plugin-sdk/test-fixtures";
|
import { bundledDistPluginFile, bundledPluginFile } from "openclaw/plugin-sdk/test-fixtures";
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
collectForbiddenPackContentPaths,
|
collectForbiddenPackContentPaths,
|
||||||
collectForbiddenPackPaths,
|
collectForbiddenPackPaths,
|
||||||
collectMissingPackPaths,
|
collectMissingPackPaths,
|
||||||
|
collectSkillShellScriptExecutableErrors,
|
||||||
collectPackUnpackedSizeErrors,
|
collectPackUnpackedSizeErrors,
|
||||||
collectPackedInstalledPackageVerificationErrors,
|
collectPackedInstalledPackageVerificationErrors,
|
||||||
createPackedCompletionSmokeEnv,
|
createPackedCompletionSmokeEnv,
|
||||||
@@ -338,6 +339,41 @@ describe("bundled plugin package dependency checks", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This suite exists both as regression coverage and as an intentional CI touchpoint for executable-bit fixes.
|
||||||
|
// Windows doesn't support Unix permission bits; chmod 0o755 is a no-op and
|
||||||
|
// statSync().mode never reports execute bits, so these tests are meaningless there.
|
||||||
|
describe.skipIf(process.platform === "win32")("collectSkillShellScriptExecutableErrors", () => {
|
||||||
|
it("flags non-executable shell scripts under skills/*/scripts", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-"));
|
||||||
|
const scriptPath = join(root, "skills", "openai-whisper-api", "scripts", "transcribe.sh");
|
||||||
|
mkdirSync(join(root, "skills", "openai-whisper-api", "scripts"), { recursive: true });
|
||||||
|
writeFileSync(scriptPath, "#!/usr/bin/env bash\necho test\n", "utf8");
|
||||||
|
chmodSync(scriptPath, 0o644);
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(collectSkillShellScriptExecutableErrors(root)).toEqual([
|
||||||
|
"skill shell script is not executable: skills/openai-whisper-api/scripts/transcribe.sh",
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts executable shell scripts", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-"));
|
||||||
|
const scriptPath = join(root, "skills", "openai-whisper-api", "scripts", "transcribe.sh");
|
||||||
|
mkdirSync(join(root, "skills", "openai-whisper-api", "scripts"), { recursive: true });
|
||||||
|
writeFileSync(scriptPath, "#!/usr/bin/env bash\necho test\n", "utf8");
|
||||||
|
chmodSync(scriptPath, 0o755);
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(collectSkillShellScriptExecutableErrors(root)).toEqual([]);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("collectForbiddenPackPaths", () => {
|
describe("collectForbiddenPackPaths", () => {
|
||||||
it("blocks all packaged node_modules payloads", () => {
|
it("blocks all packaged node_modules payloads", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user