fix(skill): load verified packages through canonical runtime paths
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user