138 lines
7.2 KiB
TypeScript
138 lines
7.2 KiB
TypeScript
import type BetterSqlite3 from "better-sqlite3";
|
|
|
|
import { ModelConfigurationService, modelIds, type ModelConfigCandidate } from "./model-configuration.js";
|
|
|
|
const safeRefPattern = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/;
|
|
const requiredRatios = ["3:4", "1:1", "4:3", "9:16"];
|
|
const requiredAdapterErrors = [
|
|
"upstream_timeout", "upstream_failed", "safety_rejected", "gateway_balance_insufficient",
|
|
"gateway_contract_invalid", "reference_invalid", "unknown_retryable", "unknown_non_retryable",
|
|
];
|
|
const forbiddenKeyFragments = ["credential", "password", "prompt_text", "raw", "secret", "token"];
|
|
|
|
export interface ModelContractEvidenceInput {
|
|
evidence_hash: string;
|
|
evidence_ref: string;
|
|
matrix: unknown;
|
|
model_id: string;
|
|
verified_at: string;
|
|
verifier_ref: string;
|
|
}
|
|
|
|
function hasForbiddenEvidenceKey(value: unknown, depth = 0): boolean {
|
|
if (depth > 8 || !value || typeof value !== "object") return false;
|
|
if (Array.isArray(value)) return value.some((entry) => hasForbiddenEvidenceKey(entry, depth + 1));
|
|
return Object.entries(value).some(([key, entry]) => (
|
|
forbiddenKeyFragments.some((fragment) => key.toLowerCase().includes(fragment))
|
|
|| hasForbiddenEvidenceKey(entry, depth + 1)
|
|
));
|
|
}
|
|
|
|
function validateEvidence(input: ModelContractEvidenceInput) {
|
|
if (!modelIds.includes(input.model_id as typeof modelIds[number])) throw new Error("contract_evidence_model_invalid");
|
|
if (!safeRefPattern.test(input.evidence_hash) || !safeRefPattern.test(input.evidence_ref) || !safeRefPattern.test(input.verifier_ref)) {
|
|
throw new Error("contract_evidence_reference_invalid");
|
|
}
|
|
const verifiedAt = Date.parse(input.verified_at);
|
|
if (!Number.isFinite(verifiedAt) || new Date(verifiedAt).toISOString() !== input.verified_at) throw new Error("contract_evidence_time_invalid");
|
|
if (hasForbiddenEvidenceKey(input.matrix)) throw new Error("contract_evidence_sensitive_field");
|
|
if (!input.matrix || typeof input.matrix !== "object") throw new Error("contract_evidence_matrix_incomplete");
|
|
const matrix = input.matrix as Record<string, unknown>;
|
|
const passedSingleOutput = (value: unknown) => Boolean(value && typeof value === "object"
|
|
&& "status" in value && value.status === "passed" && "outputs" in value && value.outputs === 1);
|
|
const ratios = Array.isArray(matrix.ratios) ? matrix.ratios as Array<Record<string, unknown>> : [];
|
|
const ratioNames = ratios.filter(passedSingleOutput).map((entry) => entry.ratio).toSorted();
|
|
const executionModes = Array.isArray(matrix.execution_modes) ? matrix.execution_modes : [];
|
|
const errorMapping = Array.isArray(matrix.error_mapping) ? matrix.error_mapping : [];
|
|
const executionComplete = executionModes.includes("sync") || (executionModes.includes("async") && executionModes.includes("poll"));
|
|
if (matrix.model_id !== input.model_id || !passedSingleOutput(matrix.pure_text) || !passedSingleOutput(matrix.reference_image)
|
|
|| JSON.stringify(ratioNames) !== JSON.stringify([...requiredRatios].toSorted()) || !executionComplete
|
|
|| !requiredAdapterErrors.every((category) => errorMapping.includes(category))) {
|
|
throw new Error("contract_evidence_matrix_incomplete");
|
|
}
|
|
return verifiedAt;
|
|
}
|
|
|
|
function editableCandidates(models: ReturnType<ModelConfigurationService["read"]>["models"]): ModelConfigCandidate[] {
|
|
return models.map(({ config_version: _configVersion, runtime_availability: _runtime, ...candidate }) => structuredClone(candidate));
|
|
}
|
|
|
|
export class ModelContractEvidenceService {
|
|
readonly database: BetterSqlite3.Database;
|
|
private readonly clock: () => number;
|
|
private readonly models: ModelConfigurationService;
|
|
|
|
constructor(input: { clock?: () => number; database: BetterSqlite3.Database; models: ModelConfigurationService }) {
|
|
this.clock = input.clock ?? Date.now;
|
|
this.database = input.database;
|
|
this.models = input.models;
|
|
this.migrate();
|
|
}
|
|
|
|
recordVerified(input: {
|
|
actorId: string;
|
|
evidence: ModelContractEvidenceInput;
|
|
expectedConfigSetVersion: number;
|
|
idempotencyKey: string;
|
|
}) {
|
|
const verifiedAt = validateEvidence(input.evidence);
|
|
return this.database.transaction(() => {
|
|
const current = this.models.read();
|
|
if (current.config_set_version !== input.expectedConfigSetVersion) throw new Error("contract_evidence_config_set_conflict");
|
|
const existingHash = this.database.prepare("SELECT model_id FROM model_contract_evidence WHERE evidence_hash = ?").get(input.evidence.evidence_hash) as { model_id: string } | undefined;
|
|
if (existingHash && existingHash.model_id !== input.evidence.model_id) throw new Error("contract_evidence_shared_between_models");
|
|
const candidates = editableCandidates(current.models);
|
|
const target = candidates.find((model) => model.model_id === input.evidence.model_id)!;
|
|
target.contract_validation_status = "verified";
|
|
target.contract_evidence_ref = input.evidence.evidence_ref;
|
|
const configuration = this.models.replace({
|
|
actorId: input.actorId,
|
|
expectedConfigSetVersion: input.expectedConfigSetVersion,
|
|
idempotencyKey: input.idempotencyKey,
|
|
models: candidates,
|
|
});
|
|
const model = configuration.models.find((candidate) => candidate.model_id === input.evidence.model_id)!;
|
|
this.database.prepare(`
|
|
INSERT OR IGNORE INTO model_contract_evidence (
|
|
model_id, config_version, evidence_hash, evidence_ref, verifier_ref, verified_at, evidence_json, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
input.evidence.model_id, model.config_version, input.evidence.evidence_hash, input.evidence.evidence_ref,
|
|
input.evidence.verifier_ref, verifiedAt, JSON.stringify(input.evidence.matrix), this.clock(),
|
|
);
|
|
return { configuration, evidence: structuredClone(input.evidence), model };
|
|
}).immediate();
|
|
}
|
|
|
|
read(modelId: string, configVersion: number) {
|
|
return this.database.prepare(`
|
|
SELECT model_id, config_version, evidence_hash, evidence_ref, verifier_ref, verified_at, evidence_json
|
|
FROM model_contract_evidence WHERE model_id = ? AND config_version = ?
|
|
`).get(modelId, configVersion) as {
|
|
config_version: number; evidence_hash: string; evidence_json: string; evidence_ref: string;
|
|
model_id: string; verified_at: number; verifier_ref: string;
|
|
} | undefined;
|
|
}
|
|
|
|
private migrate() {
|
|
this.database.exec(`
|
|
CREATE TABLE IF NOT EXISTS model_contract_evidence (
|
|
model_id TEXT NOT NULL,
|
|
config_version INTEGER NOT NULL,
|
|
evidence_hash TEXT NOT NULL UNIQUE,
|
|
evidence_ref TEXT NOT NULL,
|
|
verifier_ref TEXT NOT NULL,
|
|
verified_at INTEGER NOT NULL,
|
|
evidence_json TEXT NOT NULL CHECK (json_valid(evidence_json)),
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY (model_id, config_version),
|
|
FOREIGN KEY (model_id, config_version) REFERENCES model_config_versions(model_id, config_version)
|
|
);
|
|
CREATE TRIGGER IF NOT EXISTS model_contract_evidence_no_update BEFORE UPDATE ON model_contract_evidence
|
|
BEGIN SELECT RAISE(ABORT, 'model_contract_evidence_immutable'); END;
|
|
CREATE TRIGGER IF NOT EXISTS model_contract_evidence_no_delete BEFORE DELETE ON model_contract_evidence
|
|
BEGIN SELECT RAISE(ABORT, 'model_contract_evidence_immutable'); END;
|
|
`);
|
|
}
|
|
}
|