FN-9035: prevent static computer commands in shipped skills

Keep bundled computer-use guidance limited to runtime discovery so command syntax cannot drift.

- Scan all shipped skill markdown files, including plugin skills
- Reject static `fn computer` commands outside the computer-use discovery stub
- Assert the discovery stub exposes only its supported JSON probes

Files changed:
 packages/cli/src/commands/__tests__/computer-use-skill.test.ts | 50 ++++++++++++++++++++--
 1 file changed, 47 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-9035

Fusion-Task-Lineage: 5049fd29-1b3b-4968-b09e-1bb7be9d52ff

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-13 17:01:39 -07:00
parent 1788dc0506
commit 2dcead4c8f

View File

@@ -1,12 +1,42 @@
import { readFileSync } from "node:fs";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { dirname, join, relative } from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import { COMPUTER_COMMAND_SURFACE } from "../computer/contract.js";
const skill = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../../skill/computer-use/SKILL.md"), "utf8");
const testDir = dirname(fileURLToPath(import.meta.url));
const cliRoot = join(testDir, "../../..");
const repoRoot = join(cliRoot, "../..");
const computerUseSkillPath = join(cliRoot, "skill/computer-use/SKILL.md");
const skillRoots = [join(cliRoot, "skill"), join(repoRoot, "plugins")];
function findShippedSkillMarkdown(root: string): string[] {
if (!existsSync(root)) return [];
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
const path = join(root, entry.name);
if (entry.isDirectory()) return findShippedSkillMarkdown(path);
return entry.name === "SKILL.md" ? [path] : [];
});
}
/**
* FNXC:ComputerUseSkill 2026-08-13-23:35:
* The original stub-only ratchet let sibling shipped skills reintroduce static computer commands and
* flags that drift from the installed binary. Discover every bundled SKILL.md so the binary remains
* the only version-matched command guide, while preserving the three read-only fallback probes.
*/
function readShippedSkills(): Array<{ path: string; content: string }> {
return skillRoots
.flatMap(findShippedSkillMarkdown)
.filter((path) => path.includes("/skill/") || /\/plugins\/[^/]+\/src\/skills\//.test(path))
.sort()
.map((path) => ({ path, content: readFileSync(path, "utf8") }));
}
describe("computer-use shipped skill", () => {
it("has parseable frontmatter and keeps its anti-drift stub thin", () => {
const skill = readFileSync(computerUseSkillPath, "utf8");
const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/)?.[1];
expect(parse(frontmatter ?? "")).toMatchObject({ name: "computer-use" });
expect(skill).toContain("fn skills get computer-use");
@@ -16,5 +46,19 @@ describe("computer-use shipped skill", () => {
expect(COMPUTER_COMMAND_SURFACE[name as keyof typeof COMPUTER_COMMAND_SURFACE]).toBeDefined();
expect(tail.replace(/`$/, "")).toBe(" --json");
}
expect([...skill.matchAll(/--[a-z][a-z-]*/g)].map(([flag]) => flag)).toEqual(["--json", "--json", "--json"]);
});
it("keeps computer commands out of every other shipped skill", () => {
const skills = readShippedSkills();
const skillPaths = skills.map(({ path }) => relative(repoRoot, path));
expect(skillPaths).not.toHaveLength(0);
expect(skillPaths).toContain("packages/cli/skill/computer-use/SKILL.md");
expect(skillPaths).toContain("packages/cli/skill/fusion/SKILL.md");
for (const { path, content } of skills) {
if (path === computerUseSkillPath) continue;
expect(content, relative(repoRoot, path)).not.toMatch(/fn computer [a-z-]+/);
}
});
});