guanghu-ice-heart/server-tools/lake-lamp-authz/hololake-capabilities.js

242 lines
7 KiB
JavaScript
Raw Normal View History

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