Host checked adapter-object controllers through bounded registries (WI-13)
This commit is contained in:
@@ -257,7 +257,11 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
if (controllerText !== undefined)
|
||||
compiled.resource.revision.controllers = parseControllerCatalog(controllerText, queryInterfaces);
|
||||
compiled.resource.revision.controllers = parseControllerCatalog(
|
||||
controllerText,
|
||||
queryInterfaces,
|
||||
compiled.resource.revision.checkedQueries,
|
||||
);
|
||||
let catalogText: string | undefined;
|
||||
try {
|
||||
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8");
|
||||
|
||||
@@ -10,7 +10,8 @@ export async function scaffoldController(
|
||||
const entry = { id: spec.id, ...spec.controller };
|
||||
const fields = ["id", "subject", "snapshot", "remote", "input"];
|
||||
if (
|
||||
Object.keys(entry).length !== fields.length ||
|
||||
Object.keys(entry).some((key) => ![...fields, "registry"].includes(key)) ||
|
||||
(entry.registry !== undefined && typeof entry.registry !== "string") ||
|
||||
fields.some((key) => typeof (entry as Record<string, unknown>)[key] !== "string") ||
|
||||
!["unit", "value"].includes(entry.input ?? "")
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ export type ScaffoldRecipe = {
|
||||
declaration?: string;
|
||||
queryDocument?: string;
|
||||
initialFiles?: Record<string, string>;
|
||||
controller?: { subject: string; snapshot: string; remote: string; input: "unit" | "value" };
|
||||
controller?: { registry?: string; subject: string; snapshot: string; remote: string; input: "unit" | "value" };
|
||||
tools?: { quixos: Source; protocol: Source; helpers: Source; sdk?: Source };
|
||||
};
|
||||
const source = (value: Source): GitSource => {
|
||||
|
||||
@@ -6,6 +6,7 @@ export function validateControllers(
|
||||
controllers: readonly PackageController[],
|
||||
interfaces: readonly InterfaceRevision[],
|
||||
prefix: string,
|
||||
queries: readonly import("../query/types.js").CheckedQuery[] = [],
|
||||
): CapabilityValidationIssue[] {
|
||||
const issues: CapabilityValidationIssue[] = [],
|
||||
ids = new Set<string>();
|
||||
@@ -15,6 +16,37 @@ export function validateControllers(
|
||||
if (!controller.id.trim() || ids.has(controller.id))
|
||||
fail(`${path}.id`, "Controller identities must be nonempty and unique within the package");
|
||||
ids.add(controller.id);
|
||||
if (controller.registry !== undefined) {
|
||||
const query = queries.find((query) => query.declaration.id === controller.registry);
|
||||
const selected = (entries: import("../query/types.js").QuerySelection[] | undefined, name: string) =>
|
||||
entries?.find((entry) => entry.name === name && entry.key === name && !entry.conditions.length);
|
||||
const root = selected(query?.selection, "root"),
|
||||
objects = selected(root?.selection, "objects"),
|
||||
entries = selected(objects?.selection, "entries"),
|
||||
node = selected(entries?.selection, "node"),
|
||||
qx = selected(node?.selection, "_qx"),
|
||||
ref = selected(qx?.selection, "ref");
|
||||
const all = objects?.arguments.all;
|
||||
if (
|
||||
!query ||
|
||||
query.declaration.root.kind !== "workspace-interface" ||
|
||||
query.declaration.root.interface !== controller.subject ||
|
||||
query.declaration.placement?.kind === "local" ||
|
||||
query.effects.some((effect) => effect.execution === "rpc-permitted") ||
|
||||
query.variables.kind !== "record" ||
|
||||
Object.keys(query.variables.fields).length ||
|
||||
!ref ||
|
||||
all?.kind !== "literal" ||
|
||||
!/^\d+$/.test(String(all.value)) ||
|
||||
!Number.isSafeInteger(Number(all.value)) ||
|
||||
Number(all.value) < 1 ||
|
||||
Number(all.value) > 64
|
||||
)
|
||||
fail(
|
||||
`${path}.registry`,
|
||||
"Controller registry requires a native/portable workspace query over its subject, no variables, and root.objects(all: 1..64).entries.node._qx.ref without aliases or conditional selections",
|
||||
);
|
||||
}
|
||||
const iface = interfaces.find((i) => i.revisionId === controller.subject && !i.template);
|
||||
if (!iface) {
|
||||
fail(`${path}.subject`, "Controller subject must be an imported closed interface");
|
||||
@@ -54,11 +86,20 @@ export function validateControllers(
|
||||
}
|
||||
export function validateWorkspaceControllers(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
|
||||
return workspace.packageImports.flatMap((pkg, index) =>
|
||||
validateControllers(pkg.controllers ?? [], workspace.interfaceImports, `packageImports[${index}].controllers`),
|
||||
validateControllers(
|
||||
pkg.controllers ?? [],
|
||||
workspace.interfaceImports,
|
||||
`packageImports[${index}].controllers`,
|
||||
pkg.checkedQueries,
|
||||
),
|
||||
);
|
||||
}
|
||||
/** The file is declarative metadata, not an executable registration callback. */
|
||||
export function parseControllerCatalog(text: string, interfaces: readonly InterfaceRevision[]): PackageController[] {
|
||||
export function parseControllerCatalog(
|
||||
text: string,
|
||||
interfaces: readonly InterfaceRevision[],
|
||||
queries: readonly import("../query/types.js").CheckedQuery[] = [],
|
||||
): PackageController[] {
|
||||
if (Buffer.byteLength(text) > 1048576) throw new Error("Controller catalog exceeds 1 MiB");
|
||||
const value: unknown = JSON.parse(text);
|
||||
const record = (value: unknown): value is Record<string, unknown> =>
|
||||
@@ -75,12 +116,13 @@ export function parseControllerCatalog(text: string, interfaces: readonly Interf
|
||||
for (const entry of value.controllers)
|
||||
if (
|
||||
!record(entry) ||
|
||||
Object.keys(entry).length !== fields.length ||
|
||||
Object.keys(entry).some((key) => ![...fields, "registry"].includes(key)) ||
|
||||
(entry.registry !== undefined && typeof entry.registry !== "string") ||
|
||||
fields.some((key) => typeof entry[key] !== "string")
|
||||
)
|
||||
throw new Error("Invalid controller entry: expected exactly id, subject, snapshot, remote, input strings");
|
||||
const controllers = value.controllers as PackageController[];
|
||||
const issues = validateControllers(controllers, interfaces, "controllers");
|
||||
const issues = validateControllers(controllers, interfaces, "controllers", queries);
|
||||
if (issues.length) throw new Error(issues.map((i) => `${i.path}: ${i.message}`).join("\n"));
|
||||
return controllers;
|
||||
}
|
||||
|
||||
@@ -326,6 +326,8 @@ export interface PackageRevision {
|
||||
|
||||
/** Checked adapter registration. Implementations remain ordinary object capabilities. */
|
||||
export interface PackageController {
|
||||
/** Optional opt-in discovery: a bounded checked workspace query in this package. */
|
||||
registry?: string;
|
||||
id: string;
|
||||
subject: InterfaceRevisionId;
|
||||
snapshot: OperationId;
|
||||
|
||||
Reference in New Issue
Block a user