From 9b84db4b0a5216e0d60af00d159ae9cd7b2b8394 Mon Sep 17 00:00:00 2001 From: Oleg Lukasonok Date: Sun, 6 Sep 2026 12:58:12 +0300 Subject: [PATCH] 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 --- SKILL.md | 4 +- .../src/services/source-importers/index.ts | 44 ++++++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/SKILL.md b/SKILL.md index ede552f..5f90a7c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -6,8 +6,8 @@ metadata: author: workspace-swiss-knife version: "2.0" spec: agentskills.io/specification - origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/drawio-main.git - origin-path: $HOME/projects-ibm/cognitive-architect/workspace-skills-code-agent/drawio-main + origin-repository: git@github.ibm.com:CTOTools-skills-code-agent/diagrams-drawio.git + 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 compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments --- diff --git a/scripts/src/services/source-importers/index.ts b/scripts/src/services/source-importers/index.ts index 5232af9..31dfb39 100644 --- a/scripts/src/services/source-importers/index.ts +++ b/scripts/src/services/source-importers/index.ts @@ -1,5 +1,5 @@ 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 { isAbsolute, relative, resolve } from "node:path"; @@ -643,7 +643,7 @@ async function openConfined(candidate: string, root: string): Promise<{ handle: throw error; } 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"); return { handle, canonical }; } 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 { + 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 { + 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 { const initial = await handle.stat(); if (!initial.isFile()) throw new Error("Source entry must be a regular file"); @@ -711,12 +740,13 @@ export async function importSource(request: SourceImportRequest): Promise => { - for (const name of (await readdir(`/proc/self/fd/${directory.fd}`)).sort(codePointCompare)) { - const opened = await openConfined(resolve(`/proc/self/fd/${directory.fd}`, name), rootPath); + const walk = async (directory: { handle: FileHandle; canonical: string }): Promise => { + const base = await descriptorPath(directory.handle, directory.canonical); + for (const name of (await readdir(base)).sort(codePointCompare)) { + const opened = await openConfined(resolve(base, name), rootPath); try { const entryInfo = await opened.handle.stat(); - if (entryInfo.isDirectory()) await walk(opened.handle); + if (entryInfo.isDirectory()) await walk(opened); else if (entryInfo.isFile()) { const dot = name.lastIndexOf("."); const extension = dot >= 0 ? name.slice(dot).toLowerCase() : ""; if (extensions[request.sourceKind].has(extension)) { @@ -729,7 +759,7 @@ export async function importSource(request: SourceImportRequest): Promise(); const edgeById = new Map(); const diagnostics: ImportDiagnostic[] = [];