mindspace: version runtime contracts and audit packages

This commit is contained in:
john
2026-07-27 17:43:38 +08:00
parent e94052ff24
commit 70264a4c4c
23 changed files with 1448 additions and 20 deletions
+144 -1
View File
@@ -1,6 +1,9 @@
import {
MINDSPACE_SERVER_ADAPTER_BINDINGS,
MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION,
MINDSPACE_SERVER_ADAPTER_REQUIRED_BINDINGS,
MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES,
} from './mindspace-server-adapter-contract.mjs';
function trimTrailingSlash(value) {
@@ -27,6 +30,15 @@ function buildRemoteOperationUrl(endpoint, bindingKey, method, operationBasePath
return `${base}${normalizedBasePath}/${encodeURIComponent(bindingKey)}/${encodeURIComponent(method)}`;
}
function buildRemoteContractUrl(endpoint, contractPath = '/mindspace/v1/contract') {
const base = resolveRemoteAdapterEndpoint(endpoint);
if (!base) {
throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL');
}
const normalizedPath = normalizeOperationBasePath(contractPath);
return `${base}${normalizedPath}`;
}
function throwRemoteOperationError(response, bindingKey, method, bodyText, payload) {
const error = new Error(
payload?.message ??
@@ -88,6 +100,114 @@ async function invokeRemoteOperation({
}
}
function formatContractIssue(issue) {
if (typeof issue === 'string') return issue;
return JSON.stringify(issue);
}
export function validateMindSpaceRemoteContract(
contract,
{
minContractVersion = MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION,
requiredBindings = MINDSPACE_SERVER_ADAPTER_REQUIRED_BINDINGS,
requiredCapabilities = MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES,
} = {},
) {
const issues = [];
const version = Number(contract?.contractVersion ?? 0);
if (!Number.isFinite(version) || version < minContractVersion) {
issues.push(
`contractVersion ${contract?.contractVersion ?? '(missing)'} is below required ${minContractVersion}`,
);
}
const bindings = contract?.bindings ?? {};
for (const [bindingKey, methods] of Object.entries(requiredBindings ?? {})) {
const exposed = bindings[bindingKey];
if (!Array.isArray(exposed)) {
issues.push(`missing binding ${bindingKey}`);
continue;
}
for (const method of methods) {
if (!exposed.includes(method)) {
issues.push(`missing method ${bindingKey}.${method}`);
}
}
}
const capabilities = new Set(
Array.isArray(contract?.requiredCapabilities)
? contract.requiredCapabilities
: [],
);
for (const capability of requiredCapabilities ?? []) {
if (!capabilities.has(capability)) {
issues.push(`missing capability ${capability}`);
}
}
if (issues.length > 0) {
const error = new Error(
`MindSpace remote contract is incompatible: ${issues
.map(formatContractIssue)
.join('; ')}`,
);
error.code = 'mindspace_contract_incompatible';
error.details = {
issues,
contractVersion: contract?.contractVersion ?? null,
buildId: contract?.buildId ?? null,
gitSha: contract?.gitSha ?? null,
};
throw error;
}
return contract;
}
async function fetchRemoteContract({
endpoint,
contractPath,
fetchFn,
authToken,
timeoutMs,
}) {
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timeoutId =
controller && Number.isFinite(timeoutMs) && timeoutMs > 0
? setTimeout(() => controller.abort(new Error('MindSpace remote contract request timed out')), timeoutMs)
: null;
try {
const response = await fetchFn(buildRemoteContractUrl(endpoint, contractPath), {
method: 'GET',
headers: {
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
},
signal: controller?.signal,
});
const bodyText = await response.text().catch(() => '');
let payload = null;
if (bodyText) {
try {
payload = JSON.parse(bodyText);
} catch {
payload = null;
}
}
if (!response.ok) {
const error = new Error(
`MindSpace remote contract check failed with ${response.status}${bodyText ? `: ${bodyText}` : ''}`,
);
error.code = 'mindspace_contract_unavailable';
throw error;
}
if (!payload) {
const error = new Error('MindSpace remote contract response must be valid JSON');
error.code = 'mindspace_contract_invalid_json';
throw error;
}
return payload;
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
function createRemoteBindingProxy({
endpoint,
bindingKey,
@@ -128,6 +248,10 @@ export function createMindSpaceRemoteServerAdapter({
endpoint = '',
authToken = '',
operationBasePath = '/mindspace/v1/adapter',
contractPath = '/mindspace/v1/contract',
minContractVersion = MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION,
requiredBindings = MINDSPACE_SERVER_ADAPTER_REQUIRED_BINDINGS,
requiredCapabilities = MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES,
timeoutMs = 15_000,
fetchFn = globalThis.fetch?.bind(globalThis),
} = {}) {
@@ -157,15 +281,31 @@ export function createMindSpaceRemoteServerAdapter({
implementationStatus: 'scaffold',
endpoint: endpointUrl,
operationBasePath: normalizedOperationBasePath,
contractPath: normalizeOperationBasePath(contractPath),
timeoutMs,
storageRoot: null,
publicBaseUrl: null,
storageAdapter: null,
...bindings,
assertReady() {
async assertReady() {
if (!endpointUrl) {
throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL');
}
const contract = await fetchRemoteContract({
endpoint: endpointUrl,
contractPath,
fetchFn: invoke,
authToken,
timeoutMs,
});
validateMindSpaceRemoteContract(contract, {
minContractVersion,
requiredBindings,
requiredCapabilities,
});
logger.log?.(
`MindSpace remote contract ok (version=${contract.contractVersion}, build=${contract.buildId ?? 'unknown'}, git=${contract.gitSha ?? 'unknown'})`,
);
return true;
},
startBackgroundJobs() {
@@ -182,7 +322,10 @@ export function createMindSpaceRemoteServerAdapter({
}
export const mindspaceRemoteServerAdapterInternals = {
buildRemoteContractUrl,
buildRemoteOperationUrl,
fetchRemoteContract,
invokeRemoteOperation,
normalizeOperationBasePath,
validateMindSpaceRemoteContract,
};