guanghu-ice-heart/tcs-core/personal-skill-brains/personal-skill-runtime.mjs

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