84 lines
3.9 KiB
JavaScript
84 lines
3.9 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const registryPath = path.join(here, "personal-skill-registry.json");
|
|
|
|
function requiredString(value, field) {
|
|
if (typeof value !== "string" || value.trim() === "") throw new TypeError(`${field} must be a non-empty string`);
|
|
}
|
|
|
|
export function loadPersonalSkillRegistry(file = registryPath) {
|
|
const registry = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
if (registry.schema !== "guanghu.personal-channel-skill-registry/v1") throw new TypeError("unsupported registry schema");
|
|
return registry;
|
|
}
|
|
|
|
export function resolvePersonalSkill(skillId, registry = loadPersonalSkillRegistry()) {
|
|
requiredString(skillId, "skill_id");
|
|
const entry = registry.skills.find((skill) => skill.skill_id === skillId);
|
|
if (!entry) return null;
|
|
const packPath = path.resolve(path.dirname(registryPath), entry.pack_path);
|
|
const pack = JSON.parse(fs.readFileSync(packPath, "utf8"));
|
|
return { entry, pack };
|
|
}
|
|
|
|
export function planPersonalSkillRun(event, registry = loadPersonalSkillRegistry()) {
|
|
if (!event || typeof event !== "object" || Array.isArray(event)) throw new TypeError("event must be an object");
|
|
for (const field of ["event_id", "skill_id", "channel_id", "human_actor_id", "persona_id"]) requiredString(event[field], field);
|
|
|
|
const resolved = resolvePersonalSkill(event.skill_id, registry);
|
|
if (!resolved) return receipt(event, "NO_MATCH", ["SKILL_NOT_REGISTERED"]);
|
|
const { pack } = resolved;
|
|
|
|
if (pack.visibility !== "PRIVATE" || pack.share_state !== "NOT_AUTHORIZED" || pack.marketplace_state !== "NOT_AUTHORIZED") {
|
|
return receipt(event, "BLOCK", ["PRIVATE_OWNERSHIP_INVARIANT_BROKEN"]);
|
|
}
|
|
const allowedChannels = Array.isArray(pack.channel_ids) ? pack.channel_ids : [pack.channel_id];
|
|
if (!allowedChannels.includes(event.channel_id)) return receipt(event, "BLOCK", ["PERSONAL_CHANNEL_MISMATCH"]);
|
|
if (event.persona_id !== pack.custodian_persona_id) return receipt(event, "BLOCK", ["CUSTODIAN_PERSONA_MISMATCH"]);
|
|
|
|
if (event.intent === "SHARE" || event.intent === "PUBLISH" || event.intent === "MARKETPLACE_LIST") {
|
|
const isOwner = event.human_actor_id === pack.owner_human_anchor;
|
|
const isDirect = event.source === "HUMAN_DIRECT_LANGUAGE";
|
|
return receipt(event, "AUTHORIZATION_REQUIRED", [
|
|
...(isOwner ? [] : ["ONLY_OWNER_HUMAN_MAY_DECIDE_RELEASE"]),
|
|
...(isDirect ? [] : ["DIRECT_OWNER_LANGUAGE_REQUIRED"]),
|
|
"NO_RELEASE_COMPILED_BY_THIS_PLANNER"
|
|
]);
|
|
}
|
|
|
|
const input = event.input ?? {};
|
|
const missing = (pack.input_schema.required ?? []).filter((field) => input[field] === undefined || input[field] === null || input[field] === "");
|
|
if (missing.length) return receipt(event, "NEEDS_INPUT", missing.map((field) => `MISSING_INPUT:${field}`));
|
|
|
|
const stopped = (pack.stop_conditions ?? []).filter((condition) => (event.active_stop_conditions ?? []).includes(condition));
|
|
if (stopped.length) return receipt(event, "BLOCK", stopped);
|
|
|
|
const semanticRequired = pack.optional_semantic.required_for?.some((capability) => !(event.semantic_candidates ?? {})[capability]);
|
|
if (semanticRequired) return receipt(event, "MODEL_REQUIRED", ["SEMANTIC_CANDIDATES_MISSING"]);
|
|
|
|
if (pack.persona_judgment.required === true && !event.persona_judgment) {
|
|
return receipt(event, "PERSONA_JUDGMENT_REQUIRED", ["PERSONA_COMMIT_OR_REFUSAL_MISSING"]);
|
|
}
|
|
|
|
return {
|
|
...receipt(event, "DETERMINISTIC_READY", []),
|
|
deterministic_steps: pack.deterministic_core.steps,
|
|
validators: pack.deterministic_core.validators,
|
|
output_schema: pack.output_schema,
|
|
external_execution_route: "GH-CHANNEL-RECEIPT-0001"
|
|
};
|
|
}
|
|
|
|
function receipt(event, status, reasons) {
|
|
return {
|
|
schema: "guanghu.personal-skill-plan-receipt/v1",
|
|
event_id: event.event_id,
|
|
skill_id: event.skill_id,
|
|
status,
|
|
reasons,
|
|
external_effect: "NONE"
|
|
};
|
|
}
|