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" }; }