Add exact runtime conformance lookup and agent-visible language limits
This commit is contained in:
+53
-22
@@ -21,6 +21,23 @@ export type QxHandler<C, O> = (context: C) => O | Promise<O>;
|
||||
export type QxDerived<C, O> = { kind: "derived"; get: QxHandler<C, O> };
|
||||
export type QxSession<C> = { id: string; run<T>(work: (context: C) => Promise<T>): Promise<T>; close(): Promise<void> };
|
||||
export type QxContextLifecycle<C> = { signal?: AbortSignal; openSession?: () => Promise<QxSession<C>> };
|
||||
declare const contractView: unique symbol;
|
||||
/** Generated exact closed contract. A descriptor is type evidence, never authority. */
|
||||
export type QxInterfaceContract<View> = {
|
||||
readonly interfaceRevisionId: string;
|
||||
readonly operations: Record<string, QxOperationSpec>;
|
||||
readonly [contractView]: (value: View) => View;
|
||||
};
|
||||
export const defineQxInterfaceContract = <View>(
|
||||
interfaceRevisionId: string,
|
||||
operations: Record<string, QxOperationSpec>,
|
||||
): QxInterfaceContract<View> => Object.freeze({ interfaceRevisionId, operations }) as QxInterfaceContract<View>;
|
||||
export type QxConformer = {
|
||||
tryConform<View>(
|
||||
object: import("./references.js").QxObjectRef,
|
||||
contract: QxInterfaceContract<View>,
|
||||
): Promise<View | undefined>;
|
||||
};
|
||||
export const qxDerived = <C, O>(get: QxHandler<C, O>): QxDerived<C, O> => ({ kind: "derived", get });
|
||||
|
||||
/** Versioned binding ABI. This mirrors the language-neutral value IR. */
|
||||
@@ -35,7 +52,7 @@ export type QxOperationSpec = { id: string; inputType: QxValueType; outputType:
|
||||
export type QxPortSpec =
|
||||
| { kind: "state"; id: string; valueType: QxValueType; primitives: string[] }
|
||||
| { kind: "edge"; id: string; primitives: string[] }
|
||||
| { kind: "interface"; id: string; operations: Record<string, QxOperationSpec> }
|
||||
| { kind: "interface"; id: string; interfaceRevisionId: string; operations: Record<string, QxOperationSpec> }
|
||||
| { kind: "constructor"; id: string; inputType: QxValueType };
|
||||
export type QxHandlerSpec = {
|
||||
receiver?: "none";
|
||||
@@ -46,6 +63,32 @@ export type QxHandlerSpec = {
|
||||
};
|
||||
export type QxMessages = Record<string, MessageBinding<any>>;
|
||||
|
||||
const bindInterfaceView = (
|
||||
target: import("./index.js").InterfacePort,
|
||||
contract: QxInterfaceContract<unknown>,
|
||||
messages: QxMessages,
|
||||
) => ({
|
||||
objectId: target.objectId,
|
||||
contract,
|
||||
live: Object.fromEntries(
|
||||
Object.entries(contract.operations).map(([name, operation]) => [
|
||||
name,
|
||||
(input: unknown) => target.live(operation.id, inputFields(operation.inputType, input, messages)),
|
||||
]),
|
||||
),
|
||||
...Object.fromEntries(
|
||||
Object.entries(contract.operations).map(([name, operation]) => [
|
||||
name,
|
||||
async (input: unknown) =>
|
||||
decodeQxValue(
|
||||
operation.outputType,
|
||||
(await target.live(operation.id, inputFields(operation.inputType, input, messages))).$quixosValue,
|
||||
messages,
|
||||
),
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
// Conversion belongs at the binding boundary. It does not add orchestrator validation.
|
||||
export const decodeQxValue = (type: QxValueType, value: Value | undefined, messages: QxMessages): any => {
|
||||
if (type.kind === "builtin" && type.name === "unit") return null;
|
||||
@@ -198,27 +241,7 @@ export const bindQxHandler = <C, O>(
|
||||
const target = raw.interface(port.id);
|
||||
return [
|
||||
name,
|
||||
{
|
||||
objectId: target.objectId,
|
||||
live: Object.fromEntries(
|
||||
Object.entries(port.operations).map(([name, operation]) => [
|
||||
name,
|
||||
(input: unknown) => target.live(operation.id, inputFields(operation.inputType, input, messages)),
|
||||
]),
|
||||
),
|
||||
...Object.fromEntries(
|
||||
Object.entries(port.operations).map(([name, operation]) => [
|
||||
name,
|
||||
async (input: unknown) =>
|
||||
decodeQxValue(
|
||||
operation.outputType,
|
||||
(await target.live(operation.id, inputFields(operation.inputType, input, messages)))
|
||||
.$quixosValue,
|
||||
messages,
|
||||
),
|
||||
]),
|
||||
),
|
||||
},
|
||||
bindInterfaceView(target, defineQxInterfaceContract(port.interfaceRevisionId, port.operations), messages),
|
||||
];
|
||||
}
|
||||
case "constructor":
|
||||
@@ -233,6 +256,14 @@ export const bindQxHandler = <C, O>(
|
||||
}),
|
||||
);
|
||||
return {
|
||||
conform: {
|
||||
async tryConform<View>(object: import("./references.js").QxObjectRef, contract: QxInterfaceContract<View>) {
|
||||
const target = await raw.tryConform(object, contract.interfaceRevisionId);
|
||||
return target
|
||||
? (bindInterfaceView(target, contract as QxInterfaceContract<unknown>, messages) as View)
|
||||
: undefined;
|
||||
},
|
||||
} satisfies QxConformer,
|
||||
...(spec.receiver === "none" ? {} : { objectId: raw.objectId }),
|
||||
signal: raw.signal,
|
||||
...(spec.receiver !== "none" && raw.openSession
|
||||
|
||||
+41
-39
@@ -42,7 +42,7 @@ import {
|
||||
PackageRuntime,
|
||||
WatchEventSchema,
|
||||
} from "./quixos/runtime_pb.js";
|
||||
import { CapabilityRefSchema } from "./quixos/refs_pb.js";
|
||||
import { CapabilityRefSchema, type ConformanceWitness } from "./quixos/refs_pb.js";
|
||||
|
||||
export type CaminoClient = Client<typeof CaminoService>;
|
||||
export type OrchClient = Client<typeof OrchestratorRuntime>;
|
||||
@@ -200,6 +200,7 @@ export type ConstructorPort = {
|
||||
export type RuntimePort = StatePort | EdgePort | InterfacePort | ConstructorPort;
|
||||
|
||||
export type RuntimeContext = {
|
||||
tryConform(object: QxObjectRef, interfaceRevisionId: string): Promise<InterfacePort | undefined>;
|
||||
/** Cooperative cancellation. Completion is acknowledged only after the handler returns. */
|
||||
signal?: AbortSignal;
|
||||
openSession?: () => Promise<RuntimeSession>;
|
||||
@@ -238,6 +239,37 @@ export const createRuntimeContext = (
|
||||
orch: OrchClient,
|
||||
request: RuntimeRequest,
|
||||
): RuntimeContext => {
|
||||
const acquiredPort = (
|
||||
objectId: string,
|
||||
interfaceRevisionId: string,
|
||||
conformance?: ConformanceWitness,
|
||||
): InterfacePort => {
|
||||
const invoke = async (operationId: string, input: Record<string, unknown> = {}) => {
|
||||
const response = await orch.invokeCapability({
|
||||
objectId,
|
||||
capability: create(CapabilityRefSchema, { interfaceRevisionId, operationId, conformance }),
|
||||
input: Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsToProtoValue(value)])),
|
||||
});
|
||||
if (!response.ok) throw new Error(response.error || "Capability invocation failed");
|
||||
for (const dependency of response.dependencies) {
|
||||
if (dependency.kind === "state" || dependency.kind === "edge")
|
||||
await recordDependency({
|
||||
kind: dependency.kind,
|
||||
objectId: dependency.objectId,
|
||||
attachmentId: dependency.attachmentId,
|
||||
...(dependency.kind === "edge" ? { projectionId: dependency.projectionId } : {}),
|
||||
} as RuntimeDependency);
|
||||
}
|
||||
if (!response.result) throw new Error("Capability returned no value");
|
||||
return response.result;
|
||||
};
|
||||
return {
|
||||
objectId: referenceFromWire(objectId),
|
||||
interfaceRevisionId,
|
||||
invoke: async (operation, input) => protoValueToJs(await invoke(operation, input)),
|
||||
live: async (operation, input) => liveValue(await invoke(operation, input)),
|
||||
};
|
||||
};
|
||||
const ports = new Map<string, RuntimePort>();
|
||||
for (const dependency of request.dependencies) {
|
||||
switch (dependency.binding.case) {
|
||||
@@ -346,44 +378,7 @@ export const createRuntimeContext = (
|
||||
case "interfaceRevisionId": {
|
||||
const interfaceRevisionId = dependency.binding.value;
|
||||
const dependencyObjectId = dependency.objectId || request.objectId;
|
||||
const invoke = async (operationId: string, input: Record<string, unknown>) => {
|
||||
const response = await orch.invokeCapability({
|
||||
capability: create(CapabilityRefSchema, { interfaceRevisionId, operationId }),
|
||||
objectId: dependencyObjectId,
|
||||
input: Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsToProtoValue(value)])),
|
||||
});
|
||||
if (!response.ok) throw new Error(response.error || `Capability ${operationId} failed`);
|
||||
for (const dependency of response.dependencies) {
|
||||
if (dependency.kind === "state") {
|
||||
await recordDependency({
|
||||
kind: "state",
|
||||
objectId: dependency.objectId,
|
||||
attachmentId: dependency.attachmentId,
|
||||
});
|
||||
} else if (dependency.kind === "edge") {
|
||||
await recordDependency({
|
||||
kind: "edge",
|
||||
objectId: dependency.objectId,
|
||||
attachmentId: dependency.attachmentId,
|
||||
projectionId: dependency.projectionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return response.result;
|
||||
};
|
||||
const capability: InterfacePort = {
|
||||
objectId: referenceFromWire(dependencyObjectId),
|
||||
interfaceRevisionId,
|
||||
async invoke(operationId, input = {}) {
|
||||
return protoValueToJs(await invoke(operationId, input));
|
||||
},
|
||||
async live(operationId, input = {}) {
|
||||
const value = await invoke(operationId, input);
|
||||
if (!value) throw new Error(`Capability ${operationId} returned no value`);
|
||||
return liveValue(value);
|
||||
},
|
||||
};
|
||||
ports.set(dependency.portId, capability);
|
||||
ports.set(dependency.portId, acquiredPort(dependencyObjectId, interfaceRevisionId));
|
||||
break;
|
||||
}
|
||||
case "constructorAtomId": {
|
||||
@@ -409,6 +404,13 @@ export const createRuntimeContext = (
|
||||
return port as T;
|
||||
};
|
||||
return {
|
||||
async tryConform(object, interfaceRevisionId) {
|
||||
const objectId = referenceToWire(object);
|
||||
const { conformance } = await orch.tryConform({ objectId, interfaceRevisionId });
|
||||
if (conformance && (conformance.objectId !== objectId || conformance.interfaceRevisionId !== interfaceRevisionId))
|
||||
throw new Error("Conformance response does not match the requested view");
|
||||
return conformance ? acquiredPort(objectId, interfaceRevisionId, conformance) : undefined;
|
||||
},
|
||||
get objectId() {
|
||||
return referenceFromWire(request.objectId);
|
||||
},
|
||||
|
||||
+79
-28
File diff suppressed because one or more lines are too long
+48
-4
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file quixos/refs.proto.
|
||||
*/
|
||||
export const file_quixos_refs: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zIkQKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCSJCChBQYWNrYWdlRXhwb3J0UmVmEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSEQoJZXhwb3J0X2lkGAIgASgJIsQBChJJbmplY3RlZERlcGVuZGVuY3kSDwoHcG9ydF9pZBgBIAEoCRIXCg1zdGF0ZV9zbG90X2lkGAIgASgJSAASJgoEZWRnZRgDIAEoCzIWLnF1aXhvcy5FZGdlRGVwZW5kZW5jeUgAEh8KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgEIAEoCUgAEh0KE2NvbnN0cnVjdG9yX2F0b21faWQYBSABKAlIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyI9Cg5FZGdlRGVwZW5kZW5jeRIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCWIGcHJvdG8z");
|
||||
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLEAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEQoJb2JqZWN0X2lkGAYgASgJQgkKB2JpbmRpbmciPQoORWRnZURlcGVuZGVuY3kSFAoMZWRnZV90eXBlX2lkGAEgASgJEhUKDXByb2plY3Rpb25faWQYAiABKAliBnByb3RvMw");
|
||||
|
||||
/**
|
||||
* @generated from message quixos.CapabilityRef
|
||||
@@ -25,6 +25,13 @@ export type CapabilityRef = Message<"quixos.CapabilityRef"> & {
|
||||
* @generated from field: string operation_id = 2;
|
||||
*/
|
||||
operationId: string;
|
||||
|
||||
/**
|
||||
* Optional fence for a view acquired through TryConform. Not an authority grant.
|
||||
*
|
||||
* @generated from field: quixos.ConformanceWitness conformance = 3;
|
||||
*/
|
||||
conformance?: ConformanceWitness | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,6 +41,43 @@ export type CapabilityRef = Message<"quixos.CapabilityRef"> & {
|
||||
export const CapabilityRefSchema: GenMessage<CapabilityRef> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 0);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.ConformanceWitness
|
||||
*/
|
||||
export type ConformanceWitness = Message<"quixos.ConformanceWitness"> & {
|
||||
/**
|
||||
* @generated from field: string object_id = 1;
|
||||
*/
|
||||
objectId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string interface_revision_id = 2;
|
||||
*/
|
||||
interfaceRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string conformance_id = 3;
|
||||
*/
|
||||
conformanceId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string workspace_revision_id = 4;
|
||||
*/
|
||||
workspaceRevisionId: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string workspace_epoch = 5;
|
||||
*/
|
||||
workspaceEpoch: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message quixos.ConformanceWitness.
|
||||
* Use `create(ConformanceWitnessSchema)` to create a new message.
|
||||
*/
|
||||
export const ConformanceWitnessSchema: GenMessage<ConformanceWitness> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 1);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.PackageExportRef
|
||||
*/
|
||||
@@ -54,7 +98,7 @@ export type PackageExportRef = Message<"quixos.PackageExportRef"> & {
|
||||
* Use `create(PackageExportRefSchema)` to create a new message.
|
||||
*/
|
||||
export const PackageExportRefSchema: GenMessage<PackageExportRef> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 1);
|
||||
messageDesc(file_quixos_refs, 2);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.InjectedDependency
|
||||
@@ -108,7 +152,7 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
|
||||
* Use `create(InjectedDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const InjectedDependencySchema: GenMessage<InjectedDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 2);
|
||||
messageDesc(file_quixos_refs, 3);
|
||||
|
||||
/**
|
||||
* @generated from message quixos.EdgeDependency
|
||||
@@ -130,5 +174,5 @@ export type EdgeDependency = Message<"quixos.EdgeDependency"> & {
|
||||
* Use `create(EdgeDependencySchema)` to create a new message.
|
||||
*/
|
||||
export const EdgeDependencySchema: GenMessage<EdgeDependency> = /*@__PURE__*/
|
||||
messageDesc(file_quixos_refs, 3);
|
||||
messageDesc(file_quixos_refs, 4);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user