Fix skill origin metadata and macOS source-importer portability

Point SKILL.md origin-repository/origin-path at the renamed
diagrams-drawio repository and workspace path.

openConfined() and the directory walker addressed opened files through
/proc/self/fd, which only exists on Linux, so every file/directory source
import failed on macOS (2 of 150 tests). Keep the procfs path where it is
available and fall back to realpath() plus a dev/inode identity check
against the open handle elsewhere.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-06 12:58:12 +03:00
co-authored by Claude Code
parent 9809cf73e9
commit 9b84db4b0a
2 changed files with 39 additions and 9 deletions
+2 -2
View File
@@ -6,8 +6,8 @@ metadata:
author: workspace-swiss-knife author: workspace-swiss-knife
version: "2.0" version: "2.0"
spec: agentskills.io/specification spec: agentskills.io/specification
origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/drawio-main.git origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/diagrams-drawio.git
origin-path: $HOME/projects-ibm/cognitive-architect/workspace-skills-code-agent/drawio-main origin-path: $HOME/projects-skills-code-agent/ws-skills-code-agent/diagrams-drawio
repository: https://gitea.lego-cloud.eu/home-v1-skills-code-agent/diagrams-drawio repository: https://gitea.lego-cloud.eu/home-v1-skills-code-agent/diagrams-drawio
compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments
--- ---
+37 -7
View File
@@ -1,5 +1,5 @@
import { constants } from "node:fs"; import { constants } from "node:fs";
import { open, readdir, realpath } from "node:fs/promises"; import { open, readdir, realpath, stat } from "node:fs/promises";
import type { FileHandle } from "node:fs/promises"; import type { FileHandle } from "node:fs/promises";
import { isAbsolute, relative, resolve } from "node:path"; import { isAbsolute, relative, resolve } from "node:path";
@@ -643,7 +643,7 @@ async function openConfined(candidate: string, root: string): Promise<{ handle:
throw error; throw error;
} }
try { try {
const canonical = await realpath(`/proc/self/fd/${handle.fd}`); const canonical = await canonicalPathOfOpenHandle(handle, candidate);
if (!isConfined(root, canonical)) throw new Error("Opened source escapes outside declared root"); if (!isConfined(root, canonical)) throw new Error("Opened source escapes outside declared root");
return { handle, canonical }; return { handle, canonical };
} catch (error) { } catch (error) {
@@ -652,6 +652,35 @@ async function openConfined(candidate: string, root: string): Promise<{ handle:
} }
} }
let procFdAvailable: boolean | undefined;
/** Path to address an open directory by: its `/proc/self/fd/N` entry on Linux, else its verified canonical path. */
async function descriptorPath(handle: FileHandle, canonical: string): Promise<string> {
if (procFdAvailable === undefined) {
try { await realpath(`/proc/self/fd/${handle.fd}`); procFdAvailable = true; } catch { procFdAvailable = false; }
}
return procFdAvailable ? `/proc/self/fd/${handle.fd}` : canonical;
}
/**
* Resolve the canonical path of an already-opened file. On Linux `/proc/self/fd/N` answers for the
* exact descriptor; on macOS/BSD (no procfs) fall back to resolving the candidate path and proving it
* names the same inode as the open handle, so a swap between open() and realpath() is still rejected.
*/
async function canonicalPathOfOpenHandle(handle: FileHandle, candidate: string): Promise<string> {
try {
return await realpath(`/proc/self/fd/${handle.fd}`);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const canonical = await realpath(candidate);
const [opened, resolved] = await Promise.all([handle.stat(), stat(canonical)]);
if (opened.dev !== resolved.dev || opened.ino !== resolved.ino) {
throw new Error("Opened source does not match its canonical path");
}
return canonical;
}
async function readBounded(handle: FileHandle, limit: number, directory: boolean): Promise<Buffer> { async function readBounded(handle: FileHandle, limit: number, directory: boolean): Promise<Buffer> {
const initial = await handle.stat(); const initial = await handle.stat();
if (!initial.isFile()) throw new Error("Source entry must be a regular file"); if (!initial.isFile()) throw new Error("Source entry must be a regular file");
@@ -711,12 +740,13 @@ export async function importSource(request: SourceImportRequest): Promise<Source
const results: SourceImportResult[] = []; const results: SourceImportResult[] = [];
let consumed = 0; let consumed = 0;
const walk = async (directory: FileHandle): Promise<void> => { const walk = async (directory: { handle: FileHandle; canonical: string }): Promise<void> => {
for (const name of (await readdir(`/proc/self/fd/${directory.fd}`)).sort(codePointCompare)) { const base = await descriptorPath(directory.handle, directory.canonical);
const opened = await openConfined(resolve(`/proc/self/fd/${directory.fd}`, name), rootPath); for (const name of (await readdir(base)).sort(codePointCompare)) {
const opened = await openConfined(resolve(base, name), rootPath);
try { try {
const entryInfo = await opened.handle.stat(); const entryInfo = await opened.handle.stat();
if (entryInfo.isDirectory()) await walk(opened.handle); if (entryInfo.isDirectory()) await walk(opened);
else if (entryInfo.isFile()) { else if (entryInfo.isFile()) {
const dot = name.lastIndexOf("."); const extension = dot >= 0 ? name.slice(dot).toLowerCase() : ""; const dot = name.lastIndexOf("."); const extension = dot >= 0 ? name.slice(dot).toLowerCase() : "";
if (extensions[request.sourceKind].has(extension)) { if (extensions[request.sourceKind].has(extension)) {
@@ -729,7 +759,7 @@ export async function importSource(request: SourceImportRequest): Promise<Source
} finally { await opened.handle.close(); } } finally { await opened.handle.close(); }
} }
}; };
try { await walk(source.handle); } try { await walk(source); }
finally { await source.handle.close(); } finally { await source.handle.close(); }
if (results.length === 0) throw new Error(`No supported ${request.sourceKind} source files found`); if (results.length === 0) throw new Error(`No supported ${request.sourceKind} source files found`);
const nodeById = new Map<string, DiagramNode>(); const edgeById = new Map<string, DiagramEdge>(); const diagnostics: ImportDiagnostic[] = []; const nodeById = new Map<string, DiagramNode>(); const edgeById = new Map<string, DiagramEdge>(); const diagnostics: ImportDiagnostic[] = [];