fix(skill): load verified packages through canonical runtime paths

This commit is contained in:
Jesse_Chen
2026-08-14 17:17:08 +08:00
parent ae4a6e4720
commit 26606412a2
4 changed files with 88 additions and 5 deletions
@@ -2,18 +2,24 @@ import { createHash } from "node:crypto";
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
realpathSync,
statSync,
symlinkSync,
} from "node:fs";
import {
basename,
isAbsolute,
join,
relative,
resolve,
sep,
win32,
} from "node:path";
import { tmpdir } from "node:os";
export type SkillPackageStatus = "active" | "deprecated" | "blocked";
@@ -65,6 +71,7 @@ const SOURCE_TREE_PROJECT_ROOT = process.env.JYOTISHA_PROJECT_ROOT
? process.cwd()
: resolve(process.cwd(), ".."));
const PACKAGE_HASH_DOMAIN = "jyotisha-skill-package-v1\0";
const RUNTIME_SKILL_ROOT = mkdtempSync(join(tmpdir(), "jyotisha-skill-runtime-"));
export class SkillPackageRegistryError extends Error {
constructor(message: string) {
@@ -582,6 +589,73 @@ export class SkillPackageRegistry {
}
}
/**
* Return a Mastra-compatible read-only alias for a verified package.
*
* Mastra validates that the configured skill directory basename matches the
* frontmatter name. Versioned registry packages intentionally live under a
* numeric directory (for example, `versions/6.9.14`), so loading that
* directory directly fails validation. The alias keeps the verified package
* bytes and identity unchanged while exposing the canonical skill name at the
* loader boundary.
*/
export function resolveSkillPackageRuntimePath(
skillPackage: ResolvedSkillPackageIdentity,
): string {
const packageDirectory = resolveExistingPath(
skillPackage.resolvedPath,
`Skill package directory ${skillPackage.packagePath}`,
);
assertDirectory(
packageDirectory,
`Skill package directory ${skillPackage.packagePath}`,
);
const actualHash = computeSkillPackageSha256(packageDirectory);
if (actualHash !== skillPackage.sha256) {
return fail(
`SHA-256 mismatch before runtime loading for ${skillPackage.name}@${skillPackage.version}: expected ${skillPackage.sha256}, got ${actualHash}`,
);
}
const runtimeParent = join(RUNTIME_SKILL_ROOT, skillPackage.sha256);
mkdirSync(runtimeParent, { recursive: true });
const runtimePath = join(runtimeParent, skillPackage.name);
if (basename(runtimePath) !== skillPackage.name) {
return fail(
`Runtime skill path must end with the canonical skill name ${skillPackage.name}`,
);
}
try {
const runtimeEntry = lstatSync(runtimePath);
if (!runtimeEntry.isSymbolicLink()) {
return fail(`Runtime skill path is not a symbolic-link alias: ${runtimePath}`);
}
const linkedTarget = resolveExistingPath(
runtimePath,
`Runtime skill alias for ${skillPackage.name}@${skillPackage.version}`,
);
if (linkedTarget !== packageDirectory) {
return fail(
`Runtime skill alias for ${skillPackage.name} points to an unexpected package`,
);
}
} catch (error) {
if (error instanceof SkillPackageRegistryError) throw error;
try {
symlinkSync(packageDirectory, runtimePath, "dir");
} catch (linkError) {
return fail(
`Runtime skill alias for ${skillPackage.name}@${skillPackage.version} could not be created: ${
linkError instanceof Error ? linkError.message : "unknown filesystem error"
}`,
);
}
}
return runtimePath;
}
export function createSkillPackageRegistry(
options: SkillPackageRegistryOptions = {},
): SkillPackageRegistry {
+3 -2
View File
@@ -2,6 +2,7 @@ import { Agent } from "@mastra/core/agent";
import type { ResolvedLanguageModel } from "./model";
import {
resolveActiveSkillPackage,
resolveSkillPackageRuntimePath,
type ResolvedSkillPackageIdentity,
} from "../lib/skill-package-registry.ts";
import {
@@ -88,7 +89,7 @@ export function getRectificationV9Agent(
name: "Birth Time Rectification V9",
model: model.model,
instructions: agenticRectificationInstructions,
skills: [skillPackage.resolvedPath],
skills: [resolveSkillPackageRuntimePath(skillPackage)],
tools: createRectificationV9Tools(ctx),
});
}
@@ -116,7 +117,7 @@ export function getRectificationV9RegenerationAgent(
name: "Jyotisha Rectification Reply Regenerator",
model: model.model,
instructions: regenerationInstructions,
skills: [skillPackage.resolvedPath],
skills: [resolveSkillPackageRuntimePath(skillPackage)],
tools: createRectificationV9ReadOnlyTools(ctx),
});
}
+5 -2
View File
@@ -4,13 +4,16 @@ import { createConsultationTools, type ConsultationAgentContext } from "./consul
import { toAgentConsultationContext } from "./consultation-workflow.ts";
import { evidenceDraftModelOutputSchema } from "../lib/birth-time-guide-agent.ts";
import type { ResolvedLanguageModel } from "./model";
import { resolveActiveSkillPackage } from "../lib/skill-package-registry.ts";
import {
resolveActiveSkillPackage,
resolveSkillPackageRuntimePath,
} from "../lib/skill-package-registry.ts";
export { consultationInputSchema, consultationWorkflowReceipt, consultationWorkflowResponseSchema, runConsultationWorkflow, toAgentConsultationContext } from "./consultation-workflow.ts";
export type { ConsultationInput } from "./consultation-workflow.ts";
const jyotishSkillPackage = resolveActiveSkillPackage("jyotish-vedic-astrology");
const jyotishSkillPath = jyotishSkillPackage.resolvedPath;
const jyotishSkillPath = resolveSkillPackageRuntimePath(jyotishSkillPackage);
const jyotishInstructions = `You are the guide for a conversational Vedic astrology product.
Write in concise Simplified Chinese as a natural conversation, not a report or fixed template. Use Markdown only when it improves scanning; tables are allowed only for genuinely comparative information.
+6 -1
View File
@@ -9,7 +9,7 @@ import {
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { test, type TestContext } from "node:test";
import {
@@ -17,6 +17,7 @@ import {
createSkillPackageRegistry,
resolveActiveSkillPackage,
resolveExactSkillPackage,
resolveSkillPackageRuntimePath,
resolveSkillPackageVersion,
verifyAllActiveSkillPackages,
type SkillPackageIdentity,
@@ -105,6 +106,10 @@ test("checked-in registry verifies both active packages and returns absolute dir
projectRoot,
});
assert.equal(active.version, "6.9.14");
const runtimePath = resolveSkillPackageRuntimePath(active);
assert.equal(basename(runtimePath), active.name);
assert.equal(realpathSync(runtimePath), active.resolvedPath);
assert.notEqual(runtimePath, active.resolvedPath);
assert.deepEqual(
resolveExactSkillPackage(active.name, active.version, active.sha256, {
projectRoot,