Host checked adapter-object controllers through bounded registries (WI-13)

This commit is contained in:
Timothy J. Aveni
2026-09-21 18:00:27 -07:00
parent 1e08ccd937
commit d824634859
14 changed files with 235 additions and 7 deletions
+5 -1
View File
@@ -257,7 +257,11 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
} }
if (controllerText !== undefined) 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; let catalogText: string | undefined;
try { try {
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8"); 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 entry = { id: spec.id, ...spec.controller };
const fields = ["id", "subject", "snapshot", "remote", "input"]; const fields = ["id", "subject", "snapshot", "remote", "input"];
if ( 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") || fields.some((key) => typeof (entry as Record<string, unknown>)[key] !== "string") ||
!["unit", "value"].includes(entry.input ?? "") !["unit", "value"].includes(entry.input ?? "")
) )
+1 -1
View File
@@ -15,7 +15,7 @@ export type ScaffoldRecipe = {
declaration?: string; declaration?: string;
queryDocument?: string; queryDocument?: string;
initialFiles?: Record<string, 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 }; tools?: { quixos: Source; protocol: Source; helpers: Source; sdk?: Source };
}; };
const source = (value: Source): GitSource => { const source = (value: Source): GitSource => {
+46 -4
View File
@@ -6,6 +6,7 @@ export function validateControllers(
controllers: readonly PackageController[], controllers: readonly PackageController[],
interfaces: readonly InterfaceRevision[], interfaces: readonly InterfaceRevision[],
prefix: string, prefix: string,
queries: readonly import("../query/types.js").CheckedQuery[] = [],
): CapabilityValidationIssue[] { ): CapabilityValidationIssue[] {
const issues: CapabilityValidationIssue[] = [], const issues: CapabilityValidationIssue[] = [],
ids = new Set<string>(); ids = new Set<string>();
@@ -15,6 +16,37 @@ export function validateControllers(
if (!controller.id.trim() || ids.has(controller.id)) if (!controller.id.trim() || ids.has(controller.id))
fail(`${path}.id`, "Controller identities must be nonempty and unique within the package"); fail(`${path}.id`, "Controller identities must be nonempty and unique within the package");
ids.add(controller.id); 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); const iface = interfaces.find((i) => i.revisionId === controller.subject && !i.template);
if (!iface) { if (!iface) {
fail(`${path}.subject`, "Controller subject must be an imported closed interface"); fail(`${path}.subject`, "Controller subject must be an imported closed interface");
@@ -54,11 +86,20 @@ export function validateControllers(
} }
export function validateWorkspaceControllers(workspace: WorkspaceRevision): CapabilityValidationIssue[] { export function validateWorkspaceControllers(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
return workspace.packageImports.flatMap((pkg, index) => 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. */ /** 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"); if (Buffer.byteLength(text) > 1048576) throw new Error("Controller catalog exceeds 1 MiB");
const value: unknown = JSON.parse(text); const value: unknown = JSON.parse(text);
const record = (value: unknown): value is Record<string, unknown> => 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) for (const entry of value.controllers)
if ( if (
!record(entry) || !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") fields.some((key) => typeof entry[key] !== "string")
) )
throw new Error("Invalid controller entry: expected exactly id, subject, snapshot, remote, input strings"); throw new Error("Invalid controller entry: expected exactly id, subject, snapshot, remote, input strings");
const controllers = value.controllers as PackageController[]; 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")); if (issues.length) throw new Error(issues.map((i) => `${i.path}: ${i.message}`).join("\n"));
return controllers; return controllers;
} }
+2
View File
@@ -326,6 +326,8 @@ export interface PackageRevision {
/** Checked adapter registration. Implementations remain ordinary object capabilities. */ /** Checked adapter registration. Implementations remain ordinary object capabilities. */
export interface PackageController { export interface PackageController {
/** Optional opt-in discovery: a bounded checked workspace query in this package. */
registry?: string;
id: string; id: string;
subject: InterfaceRevisionId; subject: InterfaceRevisionId;
snapshot: OperationId; snapshot: OperationId;
+50
View File
@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import { compileQuery } from "../src/query/compile.js";
import { parseControllerCatalog } from "../src/capability-model/controllers.js";
const source = { repository: "https://example.test/device.git", commit: "a".repeat(40) };
test("controller discovery requires complete bounded accepted-state membership", async () => {
const result = compileCapabilityResourceSource(
await readFile("test/fixtures/controller-device/DeviceController.qx", "utf8"),
{ source },
);
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
const iface = result.resource.revision;
const pkg = compileCapabilityResourceSource(
'import interface DeviceController;package Driver id "driver" revision "driver@1" {query Enabled id "enabled" root workspace DeviceController document "enabled.graphql" operation "Enabled" {max rows 100;}}',
{ source, environment: { interfaces: new Map([["DeviceController", iface]]), interfaceClosure: [iface] } },
);
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
const declaration = pkg.resource.revision.queries![0]!;
const metadata = JSON.stringify({
schemaVersion: 1,
controllers: [
{
id: "driver",
registry: "enabled",
subject: "device-controller@1",
snapshot: "device:snapshot:call",
remote: "device:apply:call",
input: "value",
},
],
});
const check = async (document: string) =>
parseControllerCatalog(metadata, [iface], [await compileQuery(declaration, [iface], async () => document)]);
assert.equal(
(await check("query Enabled {root {objects(all:64,where:{enabled:{eq:true}}) {entries {node {_qx {ref}}}}}}"))
.length,
1,
);
for (const document of [
"query Enabled {root {objects(first:64) {entries {node {_qx {ref}}}}}}",
"query Enabled {root {objects(all:65) {entries {node {_qx {ref}}}}}}",
"query Enabled {root {devices:objects(all:64) {entries {node {_qx {ref}}}}}}",
"query Enabled {root {objects(all:64) {entries {node {enabled}}}}}",
"query Enabled($enabled:Boolean!) {root {objects(all:64,where:{enabled:{eq:$enabled}}) {entries {node {_qx {ref}}}}}}",
])
await assert.rejects(check(document), /registry|Controller/);
assert.throws(() => parseControllerCatalog(metadata, [iface], []), /registry/);
});
+68
View File
@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import { readFile, mkdir, writeFile } from "node:fs/promises";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/index.js";
import { compileQuery } from "../src/query/compile.js";
import { parseControllerCatalog } from "../src/capability-model/controllers.js";
import {
compileExecutionPlan,
generateInterfaceClientBindings,
generatePackageQueryClientBindings,
} from "../src/execution-world/index.js";
const read = (file: string) => readFile("test/fixtures/controller-device/" + file, "utf8");
const source = { repository: "https://example.test/device.git", commit: "a".repeat(40) };
const device = compileCapabilityResourceSource(await read("Device.qx"), { source });
assert.ok(device.ok && device.resource.kind === "interface", JSON.stringify(device.diagnostics));
const control = compileCapabilityResourceSource(await read("DeviceController.qx"), { source });
assert.ok(control.ok && control.resource.kind === "interface", JSON.stringify(control.diagnostics));
const iface = device.resource.revision,
controllerInterface = control.resource.revision,
environment = {
interfaces: new Map([
["Device", iface],
["DeviceController", controllerInterface],
]),
interfaceClosure: [iface, controllerInterface],
};
const driver = compileCapabilityResourceSource(await read("Driver.qx"), { source, environment });
assert.ok(driver.ok && driver.resource.kind === "package", JSON.stringify(driver.diagnostics));
const pkg = driver.resource.revision;
pkg.checkedQueries = await Promise.all(
pkg.queries!.map((query) => compileQuery(query, [iface, controllerInterface], read)),
);
pkg.controllers = parseControllerCatalog(
JSON.stringify({
schemaVersion: 1,
controllers: [
{
id: "device-driver",
registry: "enabled",
subject: "device-controller@1",
snapshot: "device:snapshot:call",
remote: "device:apply:call",
input: "value",
},
],
}),
[iface, controllerInterface],
pkg.checkedQueries,
);
for (const variant of ["controller", "controller-next"]) {
const extra = variant.endsWith("next") ? 'atom Dashboard id "dashboard";' : "";
const result = compileCapabilitySource((await read("workspace.qx")).replace(/}\s*$/, extra + "\n}"), "workspace.qx", {
...environment,
packages: new Map([["Driver", pkg]]),
});
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const plan = compileExecutionPlan(result.workspace),
root = "dist/managed/" + variant;
await mkdir(root, { recursive: true });
await writeFile(root + "/client.ts", generateInterfaceClientBindings({ Device: iface }));
await writeFile(root + "/queries.ts", generatePackageQueryClientBindings(pkg));
for (const [file, value] of Object.entries({
"candidate.json": result.workspace,
"execution-plan.json": plan,
"entry.json": null,
"source.json": { kind: "workspace", ...source },
}))
await writeFile(root + "/" + file, JSON.stringify(value));
}
+2
View File
@@ -169,3 +169,5 @@ package Owner id "package:owner" revision "package:owner@1" {
})) }))
await writeFile(`${root}/${file}`, JSON.stringify(value)); await writeFile(`${root}/${file}`, JSON.stringify(value));
} }
await import("./emit-controllers.js");
+11
View File
@@ -0,0 +1,11 @@
external atom Display id "device:display";
type Request=record {url:string;text:string;version:int32;};
interface Device id "device" revision "device@1" {
static operation create id "device:create" : string -> atom-ref<Display> {call id "device:create:call" execution transaction targets [browser,server];}
queryable value enabled id "device:enabled" : bool {get id "device:enabled:get" execution snapshot targets [browser,server];set id "device:enabled:set" execution transaction targets [browser,server];}
queryable value requested id "device:requested" : Request {get id "device:requested:get" execution snapshot targets [browser,server];}
queryable value observed id "device:observed" : optional<Request> {get id "device:observed:get" execution snapshot targets [browser,server];}
operation requestText id "device:request" : string -> unit {call id "device:request:call" execution transaction targets [browser,server];}
operation recordObservation id "device:record" : Request -> unit {call id "device:record:call" execution transaction targets [server];}
operation remove id "device:remove" : unit -> unit {call id "device:remove:call" execution transaction targets [server];}
}
+6
View File
@@ -0,0 +1,6 @@
type Request=record {url:string;text:string;version:int32;};
interface DeviceController id "device-controller" revision "device-controller@1" {
queryable portable value enabled id "controller:enabled" : bool {get id "controller:enabled:get" execution snapshot targets [server];}
operation snapshot id "device:snapshot" : unit -> optional<Request> {call id "device:snapshot:call" execution snapshot targets [server];}
operation apply id "device:apply" : optional<Request> -> unit {call id "device:apply:call" execution remote;}
}
+15
View File
@@ -0,0 +1,15 @@
import interface Device;
import interface DeviceController;
external atom Display id "device:display";
type Request=record {url:string;text:string;version:int32;};
package Driver id "driver" revision "driver@1" {
function create id "driver:create" : string -> atom-ref<Display> execution transaction targets [browser,server] requires {creation device id "device" : Display input record {enabled:bool;requested:Request;observed:optional<Request>;};};
operation request id "driver:request" : string -> unit mode call execution transaction targets [browser,server] receiver any requires {state requested id "requested" : Request [read,write];};
operation snapshot id "driver:snapshot" : unit -> optional<Request> mode call execution snapshot targets [server] receiver any requires {interface data id "data" : Device using [enabled.get,requested.get,observed.get];};
operation recordObservation id "driver:record" : Request -> unit mode call execution transaction targets [server] receiver any requires {state requested id "requested" : Request [read];state observed id "observed" : optional<Request> [write];};
operation apply id "driver:apply" : optional<Request> -> unit mode call execution remote receiver any requires {interface device id "device" : Device using [recordObservation.call];};
operation enabled id "driver:enabled" : unit -> bool mode call execution snapshot targets [server] receiver any requires {interface data id "data" : Device using [enabled.get];};
query Enabled id "enabled" root workspace DeviceController document "enabled.graphql" operation "Enabled" {max rows 64;}
query Inputs id "inputs" root Device document "inputs.graphql" operation "Inputs" {replica;}
query View id "view" root Device document "inputs.graphql" operation "Inputs" {local from Inputs;watch;}
}
+1
View File
@@ -0,0 +1 @@
query Enabled {root {objects(all:64, where:{enabled:{eq:true}}) {entries {node {_qx {ref}}}}}}
+1
View File
@@ -0,0 +1 @@
query Inputs {root {enabled requested {url text version} observed {url text version}}}
+25
View File
@@ -0,0 +1,25 @@
workspace Controllers id "workspace:controllers" revision "workspace:controllers@1" commit "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" {
import interface Device;
import interface DeviceController;
import package Driver;
atom Display id "device:display";
conform Display as Device id "display:device" {
private state Enabled id "display:enabled" on Display : bool policy register;
private state Requested id "display:requested" on Display : record {url:string;text:string;version:int32;} policy register;
private state Observed id "display:observed" on Display : optional<record {url:string;text:string;version:int32;}> policy register;
initialize {enabled to state Enabled;requested to state Requested;observed to state Observed;};
bind create.call to package Driver.create with {device to creation Display with {Device from input;};};
bind enabled.get to state Enabled.read;
bind enabled.set to state Enabled.write;
bind requested.get to state Requested.read;
bind observed.get to state Observed.read;
bind requestText.call to package Driver.request with {requested to state Requested;};
bind recordObservation.call to package Driver.recordObservation with {requested to state Requested;observed to state Observed;};
bind remove.call to lifecycle.delete;
}
conform Display as DeviceController id "display:controller" {
bind enabled.get to package Driver.enabled with {data to interface Device;};
bind snapshot.call to package Driver.snapshot with {data to interface Device;};
bind apply.call to package Driver.apply with {device to interface Device;};
}
}