"use strict"; const childProcess = require("node:child_process"); const crypto = require("node:crypto"); const fs = require("node:fs"); class HoloLakeKnowledgeProvider { constructor(options = {}) { this.repositoryId = String(options.repositoryId || ""); this.repositoryPath = String(options.repositoryPath || ""); this.ref = "refs/heads/main"; this.maxArchiveBytes = Math.max( 1024 * 1024, Number(options.maxArchiveBytes || 128 * 1024 * 1024), ); if (!/^[a-z0-9._-]+\/[a-z0-9._-]+$/.test(this.repositoryId)) { throw new Error("knowledge_repository_id_invalid"); } if (!this.repositoryPath || !fs.existsSync(this.repositoryPath)) { throw new Error("knowledge_repository_unavailable"); } } manifest() { const commit = this.git(["rev-parse", "--verify", `${this.ref}^{commit}`]) .toString("utf8") .trim(); if (!/^[0-9a-f]{40}$/.test(commit)) { throw new Error("knowledge_commit_invalid"); } const committedAt = Number( this.git(["show", "-s", "--format=%ct", commit]).toString("utf8").trim(), ); return { schema: "guanghu.hololake-knowledge-manifest/v1", repository: this.repositoryId, ref: this.ref, commit, committed_at: committedAt, archive_url: `/api/hololake/knowledge/archive?commit=${commit}`, }; } archive(commit) { const manifest = this.manifest(); if (!safeEqual(String(commit || ""), manifest.commit)) { throw new Error("knowledge_commit_not_current"); } const body = this.git( ["archive", "--format=zip", manifest.commit], this.maxArchiveBytes + 1, ); if (body.length > this.maxArchiveBytes) { throw new Error("knowledge_archive_too_large"); } return { schema: "guanghu.hololake-knowledge-archive/v1", repository: this.repositoryId, commit: manifest.commit, sha256: crypto.createHash("sha256").update(body).digest("hex"), content_type: "application/zip", body, }; } git(args, maxBuffer = 1024 * 1024) { return childProcess.execFileSync( "git", [`--git-dir=${this.repositoryPath}`, ...args], { encoding: "buffer", maxBuffer, stdio: ["ignore", "pipe", "pipe"], }, ); } } class HoloLakeAiGateway { constructor(options = {}) { this.fetchImpl = options.fetchImpl || fetch; this.maxMessages = Math.max(1, Number(options.maxMessages || 64)); this.maxInputCharacters = Math.max( 1, Number(options.maxInputCharacters || 120_000), ); this.maxOutputTokens = Math.max( 16, Number(options.maxOutputTokens || 16_384), ); this.maxResponseBytes = Math.max( 64 * 1024, Number(options.maxResponseBytes || 2 * 1024 * 1024), ); this.providers = normalizeProviders(options.providers || {}); } catalog() { return { schema: "guanghu.hololake-ai-catalog/v1", providers: Object.entries(this.providers).map(([id, provider]) => ({ id, name: provider.name || id, models: provider.models, })), }; } async execute(request = {}) { const providerId = String(request.provider || ""); const provider = this.providers[providerId]; if (!provider) throw new Error("ai_provider_not_registered"); const model = String(request.model || ""); if (!provider.models.includes(model)) { throw new Error("ai_model_not_registered"); } const messages = validateMessages( request.messages, this.maxMessages, this.maxInputCharacters, ); const maxTokens = Number(request.max_tokens || 1024); if ( !Number.isInteger(maxTokens) || maxTokens < 1 || maxTokens > this.maxOutputTokens ) { throw new Error("ai_output_limit_exceeded"); } const response = await this.fetchImpl(`${provider.baseUrl}/chat/completions`, { method: "POST", headers: { authorization: `Bearer ${provider.apiKey}`, "content-type": "application/json", }, body: JSON.stringify({ model, messages, max_tokens: maxTokens, stream: false, }), signal: AbortSignal.timeout(provider.timeoutMs), }); const text = await response.text(); if (Buffer.byteLength(text) > this.maxResponseBytes) { throw new Error("ai_response_too_large"); } if (!response.ok) { const error = new Error("ai_upstream_failed"); error.status = response.status; throw error; } let parsed; try { parsed = JSON.parse(text); } catch { throw new Error("ai_upstream_invalid_json"); } return { ok: true, response: parsed, receipt: { schema: "guanghu.hololake-ai-receipt/v1", state: "executed", provider: providerId, model, upstream_status: response.status, occurred_at: Date.now() / 1000, }, }; } } function loadAiProviders(file) { if (!file || !fs.existsSync(file)) return {}; const parsed = JSON.parse(fs.readFileSync(file, "utf8")); if (!parsed || parsed.schema !== "guanghu.hololake-ai-providers/v1") { throw new Error("ai_provider_registry_invalid"); } return parsed.providers || {}; } function normalizeProviders(providers) { const normalized = {}; for (const [id, value] of Object.entries(providers)) { if (!/^[a-z0-9._-]{1,64}$/.test(id) || !value) continue; const baseUrl = String(value.baseUrl || value.base_url || "").replace(/\/$/, ""); const apiKey = String(value.apiKey || value.api_key || ""); const models = Array.isArray(value.models) ? value.models.map(String).filter(model => /^[A-Za-z0-9._:/-]{1,160}$/.test(model)) : []; let parsed; try { parsed = new URL(baseUrl); } catch { continue; } if (parsed.protocol !== "https:" || !apiKey || models.length === 0) continue; normalized[id] = { baseUrl, apiKey, name: String(value.name || id).slice(0, 120), models, timeoutMs: Math.max(1_000, Number(value.timeoutMs || value.timeout_ms || 120_000)), }; } return normalized; } function validateMessages(messages, maxMessages, maxInputCharacters) { if (!Array.isArray(messages) || messages.length < 1) { throw new Error("ai_messages_required"); } if (messages.length > maxMessages) throw new Error("ai_input_too_large"); let characters = 0; const normalized = messages.map(message => { if ( !message || !["system", "user", "assistant"].includes(message.role) || typeof message.content !== "string" ) { throw new Error("ai_message_invalid"); } characters += message.content.length; return { role: message.role, content: message.content }; }); if (characters > maxInputCharacters) throw new Error("ai_input_too_large"); return normalized; } function safeEqual(left, right) { const a = Buffer.from(String(left)); const b = Buffer.from(String(right)); return a.length === b.length && crypto.timingSafeEqual(a, b); } module.exports = { HoloLakeAiGateway, HoloLakeKnowledgeProvider, loadAiProviders, };