Implement workspace evolution, migrations, and runtime continuity

Enable evolution by default for source-backed workspaces. Add stable
conformance ownership, semantic-major review, candidate typechecking,
and durable fenced cutover with explicit migrations and forward recovery.

Independently supervise package runtimes so unchanged resource owners keep
their processes and connections across cutover. Add scoped invocation
authority, resource sessions, and typed callback rebinding.

Wire opaque object references through generated bindings and RPCs. Add
canonical relationship sets, keyed maps, and ordered lists with scoped
transactional mutations, revision checks, and inverse consistency. Support
planned cascade deletion, protection, tombstones, and lifecycle foundations.

Add journaled structural edits, package/function/migration scaffolding,
managed repository creation, and resumable bottom-up dependency pin
publication. Document lifetime boundaries, revision pinning, prototype
compatibility policy, commands, and deferred work.

Validate with 210 tests, user-systemd process/connection continuity,
generated-package TypeScript checks, and Nix host/protocol checks.
TTL handoff, physical reclamation, general multi-step migrations, and
root-systemd migration isolation acceptance remain deferred.
This commit is contained in:
Timothy J. Aveni
2026-09-10 18:27:41 -07:00
parent 549c4539d5
commit ce6ae8f662
47 changed files with 1837 additions and 244 deletions
+35 -8
View File
@@ -3,14 +3,16 @@ import { ValueSchema, ObjectValueSchema, type Value } from "./camino/api_pb.js";
import { derived, jsToProtoValue, liveValue, protoValueToJs,
type RuntimeContext, type RuntimeHandler, type DerivedHandler } from "./index.js";
declare const referenceBrand: unique symbol;
export type QxObjectRef<Identity extends string> = string & { readonly [referenceBrand]: { readonly [K in Identity]: true } };
export type { QxObjectRef } from "./references.js";
import { assertReferenceFree, referenceToWire } from "./references.js";
declare const watchBrand: unique symbol;
export type QxWatchHandle = string & { readonly [watchBrand]: true };
export type MessageBinding<T> = { encode(value: T): Value; decode(value: Value): T };
export type BindingValue<B> = B extends MessageBinding<infer T> ? T : never;
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>>};
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. */
@@ -41,7 +43,17 @@ export const decodeQxValue = (type: QxValueType, value: Value | undefined, messa
if (value.kind.case !== "listValue") throw new Error("Expected QX list");
return value.kind.value.values.map((entry) => decodeQxValue(type.value, entry, messages));
}
if (type.kind === "message") return requireMessage(messages, type.descriptorId).decode(value);
if (type.kind === "message") {
assertReferenceFree(protoValueToJs(value));
const decoded = requireMessage(messages, type.descriptorId).decode(value);
assertReferenceFree(decoded);
return decoded;
}
if (type.kind === "object-ref") {
if (value.kind.case !== "refValue") throw new Error("Expected a declared RPC object reference");
return protoValueToJs(value);
}
if (value.kind.case === "refValue") throw new Error("Reference supplied to a non-reference value");
if (type.kind === "scalar") {
if (type.name === "int64" || type.name === "uint64") {
if (value.kind.case !== "integerValue") throw new Error("Expected QX integer");
@@ -64,8 +76,13 @@ export const encodeQxValue = (type: QxValueType, value: any, messages: QxMessage
if (type.kind === "builtin" && type.name === "unit") return jsToProtoValue(null);
if (type.kind === "optional") return value === null ? jsToProtoValue(null) : encodeQxValue(type.value, value, messages);
if (type.kind === "list") return jsToProtoValue(value.map((entry: unknown) => liveValue(encodeQxValue(type.value, entry, messages))));
if (type.kind === "message") return requireMessage(messages, type.descriptorId).encode(value);
if (type.kind === "object-ref") return jsToProtoValue({ $quixosRef: value });
if (type.kind === "object-ref") { referenceToWire(value); return jsToProtoValue(value); }
assertReferenceFree(value);
if (type.kind === "message") {
const encoded = requireMessage(messages, type.descriptorId).encode(value);
assertReferenceFree(protoValueToJs(encoded));
return encoded;
}
return jsToProtoValue(value);
};
@@ -88,7 +105,7 @@ const inputFields = (type: QxValueType, value: unknown, messages: QxMessages): R
export const bindQxHandler = <C, O>(
spec: QxHandlerSpec, handler: QxHandler<C, O> | QxDerived<C, O>, messages: QxMessages,
): RuntimeHandler | DerivedHandler => {
const execute = async (raw: RuntimeContext) => {
const bindContext = (raw: RuntimeContext): C => {
const ports = Object.fromEntries(Object.entries(spec.ports).map(([name, port]) => {
switch (port.kind) {
case "state": {
@@ -100,7 +117,9 @@ export const bindQxHandler = <C, O>(
}
case "edge": {
const edge = raw.edge(port.id);
return [name, Object.fromEntries(port.primitives.map((primitive) => [primitive, edge[primitive as "resolve" | "connect" | "disconnect"]]))];
return [name, {...Object.fromEntries(port.primitives.map((primitive) => [primitive, edge[primitive as "resolve" | "connect" | "disconnect"]])),
...(port.primitives.includes("resolve") ? {collection: edge.collection} : {}),
...(port.primitives.includes("resolve") && port.primitives.includes("connect") && port.primitives.includes("disconnect") ? {replace: edge.replace} : {})}];
}
case "interface": {
const target = raw.interface(port.id);
@@ -112,8 +131,16 @@ export const bindQxHandler = <C, O>(
case "constructor": return [name, { construct: (input: unknown) => raw.constructor(port.id).construct(inputFields(port.inputType, input, messages)) }];
}
}));
const context = { objectId: raw.objectId,
return { objectId: raw.objectId, signal: raw.signal,
...(raw.openSession ? {openSession: async () => {
const session = await raw.openSession!();
return {id: session.id, close: () => session.close(),
run: <T>(work: (context: C) => Promise<T>) => session.run((next) => work(bindContext(next)))};
}} : {}),
input: decodeQxValue(spec.inputType, inputValue(raw, spec.inputType), messages), ports } as C;
};
const execute = async (raw: RuntimeContext) => {
const context = bindContext(raw);
const value = await (typeof handler === "function" ? handler(context) : handler.get(context));
return liveValue(encodeQxValue(spec.eventType ?? spec.outputType, value, messages));
};
+130 -9
View File
File diff suppressed because one or more lines are too long
+43 -2
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
// @generated from file camino/schema.proto (package camino, syntax proto3)
/* eslint-disable */
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file camino/schema.proto.
*/
export const file_camino_schema: GenFile = /*@__PURE__*/
fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiQQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJIqQBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIqYBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCCKHAQoORWRnZUF0dGFjaG1lbnQSFAoMZWRnZV90eXBlX2lkGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIjCgVmaXJzdBgDIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSJAoGc2Vjb25kGAQgASgLMhQuY2FtaW5vLkVkZ2VFbmRwb2ludCLsAQoPUGVyc2lzdGVuY2VQbGFuEhQKDHdvcmtzcGFjZV9pZBgBIAEoCRIdChV3b3Jrc3BhY2VfcmV2aXNpb25faWQYAiABKAkSJQoFYXRvbXMYAyADKAsyFi5jYW1pbm8uQXRvbURlZmluaXRpb24SLQoMY29uZm9ybWFuY2VzGAQgAygLMhcuY2FtaW5vLkF0b21Db25mb3JtYW5jZRInCgZzdGF0ZXMYBSADKAsyFy5jYW1pbm8uU3RhdGVBdHRhY2htZW50EiUKBWVkZ2VzGAYgAygLMhYuY2FtaW5vLkVkZ2VBdHRhY2htZW50KmgKC0NhcmRpbmFsaXR5EhsKF0NBUkRJTkFMSVRZX1VOU1BFQ0lGSUVEEAASEAoMT1BUSU9OQUxfT05FEAESDwoLRVhBQ1RMWV9PTkUQAhIICgRNQU5ZEAMSDwoLTUFOWV9VTklRVUUQBGIGcHJvdG8z");
fileDesc("ChNjYW1pbm8vc2NoZW1hLnByb3RvEgZjYW1pbm8iNwoOQXRvbURlZmluaXRpb24SDwoHYXRvbV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkiWQoPQXRvbUNvbmZvcm1hbmNlEg8KB2F0b21faWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJIsIBCg9TdGF0ZUF0dGFjaG1lbnQSDwoHc2xvdF9pZBgBIAEoCRIYChBhdHRhY2hlZF9hdG9tX2lkGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRIXCg92YWx1ZV90eXBlX2pzb24YBCABKAkSGwoTc3RvcmFnZV9wb2xpY3lfanNvbhgFIAEoCRIaChJkZWZhdWx0X3ZhbHVlX2pzb24YBiABKAkSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYByABKAkiUAoSRW5kcG9pbnRDb25zdHJhaW50EhEKB2F0b21faWQYASABKAlIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYAiABKAlIAEIGCgRraW5kIvsBCgxFZGdlRW5kcG9pbnQSFQoNcHJvamVjdGlvbl9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSLgoKY29uc3RyYWludBgDIAEoCzIaLmNhbWluby5FbmRwb2ludENvbnN0cmFpbnQSKAoLY2FyZGluYWxpdHkYBCABKA4yEy5jYW1pbm8uQ2FyZGluYWxpdHkSDwoHb3JkZXJlZBgFIAEoCBIRCglvbl9kZWxldGUYBiABKAkSFAoMcmV0YWluX290aGVyGAcgASgIEhAKCGtleV90eXBlGAggASgJEhgKEHB1YmxpY190cmF2ZXJzYWwYCSABKAgipQEKDkVkZ2VBdHRhY2htZW50EhQKDGVkZ2VfdHlwZV9pZBgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSIwoFZmlyc3QYAyABKAsyFC5jYW1pbm8uRWRnZUVuZHBvaW50EiQKBnNlY29uZBgEIAEoCzIULmNhbWluby5FZGdlRW5kcG9pbnQSHAoUb3duZXJfY29uZm9ybWFuY2VfaWQYBSABKAki7AEKD1BlcnNpc3RlbmNlUGxhbhIUCgx3b3Jrc3BhY2VfaWQYASABKAkSHQoVd29ya3NwYWNlX3JldmlzaW9uX2lkGAIgASgJEiUKBWF0b21zGAMgAygLMhYuY2FtaW5vLkF0b21EZWZpbml0aW9uEi0KDGNvbmZvcm1hbmNlcxgEIAMoCzIXLmNhbWluby5BdG9tQ29uZm9ybWFuY2USJwoGc3RhdGVzGAUgAygLMhcuY2FtaW5vLlN0YXRlQXR0YWNobWVudBIlCgVlZGdlcxgGIAMoCzIWLmNhbWluby5FZGdlQXR0YWNobWVudCpoCgtDYXJkaW5hbGl0eRIbChdDQVJESU5BTElUWV9VTlNQRUNJRklFRBAAEhAKDE9QVElPTkFMX09ORRABEg8KC0VYQUNUTFlfT05FEAISCAoETUFOWRADEg8KC01BTllfVU5JUVVFEARiBnByb3RvMw");
/**
* @generated from message camino.AtomDefinition
@@ -47,6 +47,11 @@ export type AtomConformance = Message<"camino.AtomConformance"> & {
* @generated from field: string interface_revision_id = 2;
*/
interfaceRevisionId: string;
/**
* @generated from field: string conformance_id = 3;
*/
conformanceId: string;
};
/**
@@ -89,6 +94,11 @@ export type StateAttachment = Message<"camino.StateAttachment"> & {
* @generated from field: string default_value_json = 6;
*/
defaultValueJson: string;
/**
* @generated from field: string owner_conformance_id = 7;
*/
ownerConformanceId: string;
};
/**
@@ -155,6 +165,32 @@ export type EdgeEndpoint = Message<"camino.EdgeEndpoint"> & {
* @generated from field: bool ordered = 5;
*/
ordered: boolean;
/**
* Empty means restrict. Direction is the endpoint being deleted.
*
* @generated from field: string on_delete = 6;
*/
onDelete: string;
/**
* @generated from field: bool retain_other = 7;
*/
retainOther: boolean;
/**
* Empty for sets/lists, otherwise string, boolean, or int64 map keys.
*
* @generated from field: string key_type = 8;
*/
keyType: string;
/**
* Explicit read-only dependency injection traversal, not mutation authority.
*
* @generated from field: bool public_traversal = 9;
*/
publicTraversal: boolean;
};
/**
@@ -187,6 +223,11 @@ export type EdgeAttachment = Message<"camino.EdgeAttachment"> & {
* @generated from field: camino.EdgeEndpoint second = 4;
*/
second?: EdgeEndpoint | undefined;
/**
* @generated from field: string owner_conformance_id = 5;
*/
ownerConformanceId: string;
};
/**
+170 -32
View File
@@ -1,7 +1,12 @@
import http from "node:http";
import { readFileSync } from "node:fs";
import { createInvocationRegistry } from "./invocations.js";
import { isObjectReference, referenceFromWire, referenceToWire, assertReferenceFree, type QxObjectRef } from "./references.js";
export * from "./bindings.js";
export {relationshipMap, relationshipList, relationshipSet} from "./relationships.js";
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
export {createMigrationContext, migrationObjectId, serveMigration, type MigrationContext, type MigrationInput, type MigrationOutput, type MigrationEdge} from "./migration.js";
import { create, equals } from "@bufbuild/protobuf";
import { Code, ConnectError, createClient, type Client, type ConnectRouter } from "@connectrpc/connect";
import { connectNodeAdapter, createConnectTransport } from "@connectrpc/connect-node";
@@ -61,10 +66,11 @@ const isWrappedValue = (value: unknown): value is { $quixosValue: Value } =>
isRecord(value) && "$quixosValue" in value &&
isRecord(value.$quixosValue) && value.$quixosValue.$typeName === "camino.Value";
export const objectRef = (objectId: string) => ({ $quixosRef: objectId });
export const objectRef = (reference: QxObjectRef) => { referenceToWire(reference); return reference; };
export const liveValue = (value: Value) => ({ $quixosValue: value });
export const jsToProtoValue = (value: unknown): Value => {
if (isObjectReference(value)) return create(ValueSchema, {kind: {case: "refValue", value: create(RefValueSchema, {objectId: referenceToWire(value)})}});
if (isWrappedValue(value)) return value.$quixosValue;
if (value === null || value === undefined) {
return create(ValueSchema, { kind: { case: "nullValue", value: create(NullValueSchema, {}) } });
@@ -79,11 +85,7 @@ export const jsToProtoValue = (value: unknown): Value => {
kind: { case: "listValue", value: create(ListValueSchema, { values: value.map(jsToProtoValue) }) },
});
}
if (isRecord(value) && typeof value.$quixosRef === "string") {
return create(ValueSchema, {
kind: { case: "refValue", value: create(RefValueSchema, { objectId: value.$quixosRef }) },
});
}
if (isRecord(value) && "$quixosRef" in value) throw new Error("Raw ID wrappers are not object references");
if (isRecord(value) && typeof value.$quixosCrdtType === "string" &&
typeof value.$quixosCrdtPayload === "string") {
return create(ValueSchema, {
@@ -111,7 +113,7 @@ export const protoValueToJs = (value: Value | undefined): unknown => {
case "stringValue":
case "integerValue": return value.kind.value;
case "bytesValue": return bytesToBase64(value.kind.value);
case "refValue": return value.kind.value.objectId;
case "refValue": return referenceFromWire(value.kind.value.objectId);
case "listValue": return value.kind.value.values.map(protoValueToJs);
case "objectValue": return Object.fromEntries(
Object.entries(value.kind.value.fields).map(([key, entry]) => [key, protoValueToJs(entry)]),
@@ -136,24 +138,31 @@ export type StatePort<T = unknown> = {
export type EdgePort = {
edgeTypeId: string;
projectionId: string;
resolve(): Promise<string[]>;
connect(targetObjectId: string): Promise<void>;
disconnect(targetObjectId: string): Promise<void>;
resolve(): Promise<QxObjectRef[]>;
connect(target: QxObjectRef): Promise<void>;
disconnect(target: QxObjectRef): Promise<void>;
collection(): Promise<RelationshipCollection>;
replace(entries: RelationshipEntry[], expectedRevision: bigint): Promise<RelationshipCollection>;
};
export type RelationshipEntry<T extends QxObjectRef = QxObjectRef> = {edgeId?: string; target: T; key?: string | boolean | bigint};
export type RelationshipCollection<T extends QxObjectRef = QxObjectRef> = {revision: bigint; entries: RelationshipEntry<T>[]};
export type InterfacePort = {
objectId: string;
objectId: QxObjectRef;
interfaceRevisionId: string;
invoke(operationId: string, input?: Record<string, unknown>): Promise<unknown>;
live(operationId: string, input?: Record<string, unknown>): Promise<ReturnType<typeof liveValue>>;
};
export type ConstructorPort = {
atomId: string;
construct(input?: Record<string, unknown>): Promise<string>;
construct(input?: Record<string, unknown>): Promise<QxObjectRef>;
};
export type RuntimePort = StatePort | EdgePort | InterfacePort | ConstructorPort;
export type RuntimeContext = {
objectId: string;
/** Cooperative cancellation. Completion is acknowledged only after the handler returns. */
signal?: AbortSignal;
openSession?: () => Promise<RuntimeSession>;
objectId: QxObjectRef;
input: Record<string, unknown>;
inputProto: Record<string, Value>;
ports: ReadonlyMap<string, RuntimePort>;
@@ -163,6 +172,17 @@ export type RuntimeContext = {
constructor(portId: string): ConstructorPort;
};
export type RuntimeSession = {
id: string;
run<T>(work: (context: RuntimeContext) => Promise<T>): Promise<T>;
/** Close the external resource first, then close its host retention/session. */
close(): Promise<void>;
};
export class RuntimeAuthorityError extends Error {
readonly retryable: boolean;
constructor(message: string) { super(message); this.name = "RuntimeAuthorityError"; this.retryable = /WORKSPACE_FENCED|STALE_EPOCH/.test(message); }
}
const targetForEdge = (
edge: { firstObjectId: string; secondObjectId: string; firstProjectionId: string },
projectionId: string,
@@ -192,6 +212,7 @@ export const createRuntimeContext = (
return liveValue(value.value);
},
async set(value) {
assertReferenceFree(isWrappedValue(value) ? protoValueToJs(value.$quixosValue) : value);
await camino.writeState({ objectId: dependencyObjectId, slotId, value: jsToProtoValue(value) });
},
};
@@ -201,18 +222,34 @@ export const createRuntimeContext = (
case "edge": {
const { edgeTypeId, projectionId } = dependency.binding.value;
const dependencyObjectId = dependency.objectId || request.objectId;
const collectionResult = (response: {revision: bigint; entries: {edgeId: string; targetObjectId: string; key?: Value}[]}): RelationshipCollection => ({revision: response.revision,
entries: response.entries.map((entry) => ({edgeId: entry.edgeId, target: referenceFromWire(entry.targetObjectId),
...(entry.key ? {key: entry.key.kind.case === "integerValue" ? BigInt(entry.key.kind.value) : protoValueToJs(entry.key) as string | boolean} : {})}))});
const edge: EdgePort = {
edgeTypeId,
projectionId,
async collection() {
await recordDependency({kind: "edge", objectId: dependencyObjectId, attachmentId: edgeTypeId, projectionId});
return collectionResult(await camino.readCollection({objectId: dependencyObjectId, edgeTypeId, projectionId}));
},
async replace(entries, expectedRevision) {
return collectionResult(await camino.replaceCollection({objectId: dependencyObjectId, edgeTypeId, projectionId, expectedRevision,
entries: entries.map((entry) => {
assertReferenceFree(entry.key);
return {edgeId: entry.edgeId ?? "", targetObjectId: referenceToWire(entry.target), key: entry.key === undefined ? undefined : jsToProtoValue(entry.key)};
})}));
},
async resolve() {
await recordDependency({ kind: "edge", objectId: dependencyObjectId, attachmentId: edgeTypeId, projectionId });
const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId });
return result.edges.map((entry) => targetForEdge(entry, projectionId));
return result.edges.map((entry) => referenceFromWire(targetForEdge(entry, projectionId)));
},
async connect(targetObjectId) {
async connect(target) {
const targetObjectId = referenceToWire(target);
await camino.connectEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId, targetObjectId });
},
async disconnect(targetObjectId) {
async disconnect(target) {
const targetObjectId = referenceToWire(target);
const result = await camino.resolveEdge({ objectId: dependencyObjectId, edgeTypeId, projectionId });
for (const entry of result.edges) {
if (targetForEdge(entry, projectionId) === targetObjectId) await camino.disconnectEdge({ edgeId: entry.id });
@@ -251,7 +288,7 @@ export const createRuntimeContext = (
return response.result;
};
const capability: InterfacePort = {
objectId: dependencyObjectId,
objectId: referenceFromWire(dependencyObjectId),
interfaceRevisionId,
async invoke(operationId, input = {}) {
return protoValueToJs(await invoke(operationId, input));
@@ -275,7 +312,7 @@ export const createRuntimeContext = (
input: Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsToProtoValue(value)])),
});
if (!response.object) throw new Error(`Constructor ${atomId} returned no object`);
return response.object.id;
return referenceFromWire(response.object.id);
},
};
ports.set(dependency.portId, constructor);
@@ -288,7 +325,7 @@ export const createRuntimeContext = (
return port as T;
};
return {
objectId: request.objectId,
objectId: referenceFromWire(request.objectId),
input: protoFieldsToJs(request.input),
inputProto: request.input,
ports,
@@ -332,9 +369,12 @@ export const createPackageRuntimeRoutes = (config: {
caminoUrl?: string;
orchUrl?: string;
}) => {
const invocations = createInvocationRegistry();
const headers: Record<string, string> = {};
if (process.env.CAMINO_RUNTIME_AUTH_TOKEN) {
headers["x-camino-runtime-token"] = process.env.CAMINO_RUNTIME_AUTH_TOKEN;
const processToken = process.env.CAMINO_RUNTIME_AUTH_TOKEN ?? (process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE
? readFileSync(process.env.CAMINO_RUNTIME_AUTH_TOKEN_FILE, "utf8").trim() : "");
if (processToken) {
headers["x-camino-runtime-token"] = processToken;
} else if (process.env.CAMINO_RUNTIME_AUTH_REQUIRED === "1") {
throw new Error("CAMINO_RUNTIME_AUTH_TOKEN is required");
}
@@ -353,24 +393,114 @@ export const createPackageRuntimeRoutes = (config: {
httpVersion: "1.1",
}));
const authenticateInstance = (header: Headers) => {
if (!process.env.QUIXOS_RUNTIME_INSTANCE_ID) return; // standalone development ABI
const supplied = Buffer.from(header.get("x-quixos-instance-token") ?? "");
const expected = Buffer.from(processToken);
if (!expected.length || supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) {
throw new ConnectError("Invalid runtime instance credential", Code.Unauthenticated);
}
};
const clientsFor = (request: { context?: { grant: string; instanceId: string; workspaceEpoch: string } }) => {
const context = request.context;
if (process.env.QUIXOS_RUNTIME_INSTANCE_ID && (!context?.grant || context.instanceId !== process.env.QUIXOS_RUNTIME_INSTANCE_ID || !context.workspaceEpoch)) {
throw new ConnectError("Managed invocation requires an exact instance and epoch grant", Code.Unauthenticated);
}
if (!context?.grant) return { camino, orch };
const transport = (url: string) => createConnectTransport({ baseUrl: url, httpVersion: "1.1", interceptors: [(next) => async (call) => {
call.header.set("x-quixos-invocation-grant", context.grant);
call.header.set("x-camino-runtime-token", processToken);
return next(call);
}] });
return {
camino: createClient(CaminoService, transport(config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310")),
orch: createClient(OrchestratorRuntime, transport(config.orchUrl ?? process.env.QUIXOS_ORCH_URL ?? "http://127.0.0.1:7311")),
};
};
const runtimeControl = async <T>(operation: string, input: unknown): Promise<T> => {
const response = await fetch(`${config.caminoUrl ?? process.env.CAMINO_URL ?? "http://127.0.0.1:7310"}/__runtime/${operation}`, {
method: "POST", headers: { "content-type": "application/json", "x-camino-runtime-token": processToken }, body: JSON.stringify(input), signal: AbortSignal.timeout(10_000),
});
const value = await response.json() as T & {error?: string};
if (!response.ok) throw new RuntimeAuthorityError(value.error ?? "Runtime authority request failed");
return value;
};
const attachSessions = (runtimeContext: RuntimeContext, request: RuntimeRequest & {context?: {grant: string; instanceId: string; workspaceEpoch: string; ownerConformanceId?: string}}) => {
if (!request.context?.grant || !request.context.ownerConformanceId) return;
runtimeContext.openSession = async () => {
const ownerId = request.context!.ownerConformanceId!;
const registration = { grant: request.context!.grant, objectId: request.objectId, ownerId,
sessionId: `session:${randomBytes(16).toString("hex")}`, token: randomBytes(32).toString("base64url") };
const register = () => runtimeControl<{sessionId: string; token: string}>("register-session", registration);
const registered = await register().catch((error) => {
// Retry a transport/lost-response failure with exactly the same identity.
// Admission/authority errors are definitive and must not be retried here.
if (error instanceof RuntimeAuthorityError) throw error;
return register();
});
let closed = false;
return {
id: registered.sessionId,
async run(work) {
if (closed) throw new RuntimeAuthorityError("SESSION_CLOSED");
// Acquisition happens before user code. A fence failure can be retried
// by the caller without replaying a side-effecting callback.
const grant = await runtimeControl<{grant: string; epoch: string; instanceId: string; invocationId: string; bindingDigest: string}>("acquire-session", registered);
const execution = invocations.begin(grant.invocationId);
const sessionRequest = { ...request, context: { grant: grant.grant, instanceId: grant.instanceId, workspaceEpoch: grant.epoch } };
const clients = clientsFor(sessionRequest);
const context = createRuntimeContext(clients.camino, clients.orch, sessionRequest);
context.signal = execution.signal;
try { return await work(context); }
finally {
execution.finish();
await runtimeControl("complete-invocation", { invocationId: grant.invocationId }).catch((error) => console.error("Session completion will be reconciled by the host", error));
}
},
async close() { await runtimeControl("close-session", registered); closed = true; },
};
};
};
return (router: ConnectRouter) => router.service(PackageRuntime, {
handshake: () => create(HandshakeResponseSchema, {
handshake: (request) => create(HandshakeResponseSchema, {
packageRevisionId: config.packageRevisionId,
runtimeProtocolVersion: "quixos-capabilities-v1",
exportIds: Object.keys(config.exports),
capabilities: ["invocation-completion-v1", "instance-authentication-v1", "epoch-grants-v1"],
instanceId: process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "",
authenticationProof: request.nonce && processToken ? createHmac("sha256", processToken)
.update(JSON.stringify([request.nonce, process.env.QUIXOS_RUNTIME_INSTANCE_ID ?? "", config.packageRevisionId]))
.digest("hex") : "",
}),
invoke: async (request) => {
getInvocationStatus: (request, context) => {
authenticateInstance(context.requestHeader);
return invocations.status(request.invocationId);
},
cancelInvocation: (request, context) => {
authenticateInstance(context.requestHeader);
return invocations.cancel(request.invocationId);
},
invoke: async (request, context) => {
authenticateInstance(context.requestHeader);
const { camino, orch } = clientsFor(request);
const exportId = request.export?.exportId;
const handler = exportId ? config.exports[exportId] : undefined;
if (!handler) throw new ConnectError(`Unknown export ${exportId ?? ""}`, Code.NotFound);
const execution = invocations.begin(request.invocationId || (process.env.QUIXOS_RUNTIME_INSTANCE_ID ? "" : randomUUID()));
try {
const result = await evaluate(handler, createRuntimeContext(camino, orch, request));
const runtimeContext = createRuntimeContext(camino, orch, request);
attachSessions(runtimeContext, request);
runtimeContext.signal = AbortSignal.any([context.signal, execution.signal]);
const result = await evaluate(handler, runtimeContext);
execution.finish();
return create(InvokeResponseSchema, {
ok: true,
result: result.value,
dependencies: protoDependencies(result.dependencies),
});
} catch (error) {
execution.finish(true);
return create(InvokeResponseSchema, {
ok: false,
error: error instanceof Error ? error.message : String(error),
@@ -378,13 +508,20 @@ export const createPackageRuntimeRoutes = (config: {
}
},
watch: async function* (request, context) {
authenticateInstance(context.requestHeader);
const { camino, orch } = clientsFor(request);
const exportId = request.export?.exportId;
const handler = exportId ? config.exports[exportId] : undefined;
if (!handler || !isDerived(handler)) {
throw new ConnectError(`Export ${exportId ?? ""} is not derived`, Code.FailedPrecondition);
}
const execution = invocations.begin(request.invocationId || (process.env.QUIXOS_RUNTIME_INSTANCE_ID ? "" : randomUUID()));
const signal = AbortSignal.any([context.signal, execution.signal]);
try {
const watchId = `watch:${randomUUID()}`;
const runtimeContext = createRuntimeContext(camino, orch, request);
attachSessions(runtimeContext, request);
runtimeContext.signal = signal;
type WatchOutcome = { key: string; done: boolean; error?: unknown };
type Subscription = {
dependency: RuntimeDependency;
@@ -404,7 +541,7 @@ export const createPackageRuntimeRoutes = (config: {
const establish = (async () => {
const controller = new AbortController();
const stream = camino.watchObject(
{ objectId: dependency.objectId, includeSnapshot: true },
{ objectId: dependency.objectId, includeSnapshot: true, attachmentIds: request.context?.grant ? [dependency.attachmentId] : [] },
{ signal: controller.signal },
)[Symbol.asyncIterator]();
try {
@@ -443,7 +580,7 @@ export const createPackageRuntimeRoutes = (config: {
subscription.controller.abort();
}
};
context.signal.addEventListener("abort", abortAll, { once: true });
signal.addEventListener("abort", abortAll, { once: true });
const evaluateWithStableSubscriptions = async () => {
// A direct state/edge port records its dependency before reading it,
@@ -459,6 +596,7 @@ export const createPackageRuntimeRoutes = (config: {
throw new Error("Derived dependency discovery did not stabilize after 32 passes");
};
try {
let current = await evaluateWithStableSubscriptions();
yield create(WatchEventSchema, {
watchId,
@@ -468,11 +606,10 @@ export const createPackageRuntimeRoutes = (config: {
});
const abort = new Promise<"abort">((resolve) => {
if (context.signal.aborted) resolve("abort");
else context.signal.addEventListener("abort", () => resolve("abort"), { once: true });
if (signal.aborted) resolve("abort");
else signal.addEventListener("abort", () => resolve("abort"), { once: true });
});
try {
while (!context.signal.aborted) {
while (!signal.aborted) {
if (subscriptions.size === 0) {
await abort;
break;
@@ -506,9 +643,10 @@ export const createPackageRuntimeRoutes = (config: {
current = updated;
}
} finally {
context.signal.removeEventListener("abort", abortAll);
signal.removeEventListener("abort", abortAll);
abortAll();
}
} finally { execution.finish(); }
},
});
};
+24
View File
@@ -0,0 +1,24 @@
/** Execution completion, not HTTP disconnection, is the drain boundary. */
export const createInvocationRegistry = () => {
const entries = new Map<string, { state: string; controller: AbortController }>();
return {
begin(id: string) {
if (!id || entries.has(id)) throw new Error("Invocation ID is missing or already used; invocations are never replayed implicitly");
// Completed identities remain until process retirement. A bounded process
// may reject new work; it must not evict and accidentally replay a call.
if (entries.size >= 100_000) throw new Error("Invocation registry full; explicit runtime retirement required");
const entry = { state: "running", controller: new AbortController() };
entries.set(id, entry);
return { signal: entry.controller.signal, finish(failed = false) { entry.state = failed ? "failed" : "completed"; } };
},
status(id: string) { return { invocationId: id, state: entries.get(id)?.state ?? "unknown" }; },
cancel(id: string) {
const entry = entries.get(id);
if (entry && ["running", "cancellation-requested"].includes(entry.state)) {
entry.state = "cancellation-requested";
entry.controller.abort();
}
return this.status(id);
},
};
};
+95
View File
@@ -0,0 +1,95 @@
import {createHash} from "node:crypto";
export type MigrationInput = {
schemaVersion: 1; executionId: string; exportId: string;
ports: {name: string; binding: string; view: "old" | "new"; access: ("read" | "write" | "create" | "edge")[]; atomId?: string;
attachedAtomId?: string; defaultValue?: unknown;
states?: {objectId: string; value: unknown}[]; edges?: MigrationEdge[]}[];
};
export type MigrationEdge = {id: string; edgeTypeId: string; firstObjectId: string; secondObjectId: string; firstProjectionId: string; secondProjectionId: string; firstOrdinal?: number; secondOrdinal?: number; firstKeyJson?: string; secondKeyJson?: string};
export type MigrationOutput = {schemaVersion: 1; executionId: string;
writes: {port: string; objectId: string; value: unknown}[];
creates: {port: string; logicalKey: string; objectId: string}[];
edgeReplacements: {port: string; edges: MigrationEdge[]}[]};
export type MigrationContext = {
enumerate(port: string): {objectId: string; value: unknown}[];
read(port: string, objectId: string): unknown;
write(port: string, objectId: string, value: unknown): void;
create(port: string, logicalKey: string): string;
edges(port: string): MigrationEdge[];
replaceEdges(port: string, edges: MigrationEdge[]): void;
};
export const migrationObjectId = (executionId: string, port: string, logicalKey: string) =>
`obj:migration:${createHash("sha256").update(JSON.stringify([executionId, port, logicalKey])).digest("hex")}`;
/** No ordinary RuntimeContext or network/database clients are supplied here.
* Process isolation belongs to the host, not this convenience API. */
export const createMigrationContext = (input: MigrationInput) => {
if (input.schemaVersion !== 1 || !input.executionId || new Set(input.ports.map((entry) => entry.name)).size !== input.ports.length) throw new Error("Invalid migration input");
const output: MigrationOutput = {schemaVersion: 1, executionId: input.executionId, writes: [], creates: [], edgeReplacements: []};
const port = (name: string, access: string) => {
const selected = input.ports.find((entry) => entry.name === name);
if (!selected?.access.includes(access as "read") || (selected.view === "old" && access !== "read")) throw new Error(`Migration port ${name} does not grant ${access}`);
return selected;
};
const context: MigrationContext = {
enumerate(name) {
const selected = port(name, "read"), states = structuredClone(selected.states ?? []);
if (selected.view === "new" && Object.hasOwn(selected, "defaultValue")) for (const helper of output.creates) {
if (input.ports.find((entry) => entry.name === helper.port)?.atomId === selected.attachedAtomId && !states.some((entry) => entry.objectId === helper.objectId)) states.push({objectId: helper.objectId, value: structuredClone(selected.defaultValue)});
}
if (selected.view === "new") for (const write of output.writes) {
if (input.ports.find((entry) => entry.name === write.port)?.binding !== selected.binding) continue;
const existing = states.findIndex((entry) => entry.objectId === write.objectId), entry = {objectId: write.objectId, value: structuredClone(write.value)};
if (existing < 0) states.push(entry); else states[existing] = entry;
}
return states.sort((a, b) => a.objectId < b.objectId ? -1 : a.objectId > b.objectId ? 1 : 0);
},
read(name, objectId) {return context.enumerate(name).find((entry) => entry.objectId === objectId)?.value;},
write(name, objectId, value) {
port(name, "write");
const previous = output.writes.findIndex((entry) => entry.port === name && entry.objectId === objectId);
const entry = {port: name, objectId, value: structuredClone(value)};
if (previous < 0) output.writes.push(entry); else output.writes[previous] = entry;
},
create(name, logicalKey) {
port(name, "create");
if (!logicalKey || logicalKey.length > 1024) throw new Error("Migration creation requires a bounded stable logical key");
const objectId = migrationObjectId(input.executionId, name, logicalKey);
if (!output.creates.some((entry) => entry.objectId === objectId)) output.creates.push({port: name, logicalKey, objectId});
return objectId;
},
edges(name) {
const selected = port(name, "read");
const replacement = selected.view === "new" ? output.edgeReplacements.find((entry) => input.ports.find((candidate) => candidate.name === entry.port)?.binding === selected.binding) : undefined;
return structuredClone(replacement?.edges ?? selected.edges ?? []);
},
replaceEdges(name, edges) {
port(name, "edge");
const previous = output.edgeReplacements.findIndex((entry) => entry.port === name);
const entry = {port: name, edges: structuredClone(edges)};
if (previous < 0) output.edgeReplacements.push(entry); else output.edgeReplacements[previous] = entry;
},
};
return {context, result: () => structuredClone(output)};
};
/** Entrypoint for an immutable package's dedicated bin/migrate executable.
* stdout is protocol-only; send diagnostics to stderr. The host independently
* validates every write, helper identity, contract, and completion receipt. */
export const serveMigration = async (exports: Record<string, (context: MigrationContext) => void | Promise<void>>) => {
const chunks: Buffer[] = []; let bytes = 0;
for await (const chunk of process.stdin) {
bytes += chunk.length;
if (bytes > 16 * 1024 * 1024) throw new Error("Migration input exceeds 16 MiB");
chunks.push(Buffer.from(chunk));
}
const input = JSON.parse(Buffer.concat(chunks).toString("utf8")) as MigrationInput;
const implementation = Object.hasOwn(exports, input.exportId) ? exports[input.exportId] : undefined;
if (!implementation) throw new Error("Unknown migration export");
const execution = createMigrationContext(input);
await implementation(execution.context);
const result = JSON.stringify(execution.result());
if (Buffer.byteLength(result) > 16 * 1024 * 1024) throw new Error("Migration output exceeds 16 MiB");
process.stdout.write(`${result}\n`);
};
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
// @generated from file quixos/orch.proto (package quixos.orch, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
// @generated from file quixos/package.proto (package quixos, syntax proto3)
/* eslint-disable */
+1 -1
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
// @generated from file quixos/refs.proto (package quixos, syntax proto3)
/* eslint-disable */
+138 -7
View File
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts,import_extension=js"
// @generated by protoc-gen-es v2.14.1 with parameter "target=ts,import_extension=js"
// @generated from file quixos/runtime.proto (package quixos.runtime, syntax proto3)
/* eslint-disable */
@@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file quixos/runtime.proto.
*/
export const file_quixos_runtime: GenFile = /*@__PURE__*/
fileDesc("ChRxdWl4b3MvcnVudGltZS5wcm90bxIOcXVpeG9zLnJ1bnRpbWUiMQoQSGFuZHNoYWtlUmVxdWVzdBIdChVvcmNoX3Byb3RvY29sX3ZlcnNpb24YASABKAkiZgoRSGFuZHNoYWtlUmVzcG9uc2USGwoTcGFja2FnZV9yZXZpc2lvbl9pZBgBIAEoCRIgChhydW50aW1lX3Byb3RvY29sX3ZlcnNpb24YAiABKAkSEgoKZXhwb3J0X2lkcxgDIAMoCSKLAgoNSW52b2tlUmVxdWVzdBIVCg1pbnZvY2F0aW9uX2lkGAEgASgJEigKBmV4cG9ydBgCIAEoCzIYLnF1aXhvcy5QYWNrYWdlRXhwb3J0UmVmEhEKCW9iamVjdF9pZBgDIAEoCRI3CgVpbnB1dBgEIAMoCzIoLnF1aXhvcy5ydW50aW1lLkludm9rZVJlcXVlc3QuSW5wdXRFbnRyeRIwCgxkZXBlbmRlbmNpZXMYBSADKAsyGi5xdWl4b3MuSW5qZWN0ZWREZXBlbmRlbmN5GjsKCklucHV0RW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ASKDAQoOSW52b2tlUmVzcG9uc2USCgoCb2sYASABKAgSHQoGcmVzdWx0GAIgASgLMg0uY2FtaW5vLlZhbHVlEg0KBWVycm9yGAMgASgJEjcKDGRlcGVuZGVuY2llcxgEIAMoCzIhLnF1aXhvcy5ydW50aW1lLkRlcml2ZWREZXBlbmRlbmN5IokCCgxXYXRjaFJlcXVlc3QSFQoNaW52b2NhdGlvbl9pZBgBIAEoCRIoCgZleHBvcnQYAiABKAsyGC5xdWl4b3MuUGFja2FnZUV4cG9ydFJlZhIRCglvYmplY3RfaWQYAyABKAkSNgoFaW5wdXQYBCADKAsyJy5xdWl4b3MucnVudGltZS5XYXRjaFJlcXVlc3QuSW5wdXRFbnRyeRIwCgxkZXBlbmRlbmNpZXMYBSADKAsyGi5xdWl4b3MuSW5qZWN0ZWREZXBlbmRlbmN5GjsKCklucHV0RW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ASJiChFEZXJpdmVkRGVwZW5kZW5jeRIMCgRraW5kGAEgASgJEhEKCW9iamVjdF9pZBgCIAEoCRIVCg1hdHRhY2htZW50X2lkGAMgASgJEhUKDXByb2plY3Rpb25faWQYBCABKAkilQEKCldhdGNoRXZlbnQSEAoId2F0Y2hfaWQYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWUSNwoMZGVwZW5kZW5jaWVzGAMgAygLMiEucXVpeG9zLnJ1bnRpbWUuRGVyaXZlZERlcGVuZGVuY3kSDQoFZXJyb3IYBCABKAkSDwoHaW5pdGlhbBgFIAEoCDLwAQoOUGFja2FnZVJ1bnRpbWUSUAoJSGFuZHNoYWtlEiAucXVpeG9zLnJ1bnRpbWUuSGFuZHNoYWtlUmVxdWVzdBohLnF1aXhvcy5ydW50aW1lLkhhbmRzaGFrZVJlc3BvbnNlEkcKBkludm9rZRIdLnF1aXhvcy5ydW50aW1lLkludm9rZVJlcXVlc3QaHi5xdWl4b3MucnVudGltZS5JbnZva2VSZXNwb25zZRJDCgVXYXRjaBIcLnF1aXhvcy5ydW50aW1lLldhdGNoUmVxdWVzdBoaLnF1aXhvcy5ydW50aW1lLldhdGNoRXZlbnQwAWIGcHJvdG8z", [file_camino_api, file_quixos_refs]);
fileDesc("ChRxdWl4b3MvcnVudGltZS5wcm90bxIOcXVpeG9zLnJ1bnRpbWUiQAoQSGFuZHNoYWtlUmVxdWVzdBIdChVvcmNoX3Byb3RvY29sX3ZlcnNpb24YASABKAkSDQoFbm9uY2UYAiABKAkirwEKEUhhbmRzaGFrZVJlc3BvbnNlEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSIAoYcnVudGltZV9wcm90b2NvbF92ZXJzaW9uGAIgASgJEhIKCmV4cG9ydF9pZHMYAyADKAkSEwoLaW5zdGFuY2VfaWQYBCABKAkSHAoUYXV0aGVudGljYXRpb25fcHJvb2YYBSABKAkSFAoMY2FwYWJpbGl0aWVzGAYgAygJIpoBChFJbnZvY2F0aW9uQ29udGV4dBIXCg93b3Jrc3BhY2VfZXBvY2gYASABKAkSEwoLaW5zdGFuY2VfaWQYAiABKAkSFgoOYmluZGluZ19kaWdlc3QYAyABKAkSDQoFZ3JhbnQYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIcChRvd25lcl9jb25mb3JtYW5jZV9pZBgGIAEoCSIxChhJbnZvY2F0aW9uQ29udHJvbFJlcXVlc3QSFQoNaW52b2NhdGlvbl9pZBgBIAEoCSI4ChBJbnZvY2F0aW9uU3RhdHVzEhUKDWludm9jYXRpb25faWQYASABKAkSDQoFc3RhdGUYAiABKAkivwIKDUludm9rZVJlcXVlc3QSFQoNaW52b2NhdGlvbl9pZBgBIAEoCRIoCgZleHBvcnQYAiABKAsyGC5xdWl4b3MuUGFja2FnZUV4cG9ydFJlZhIRCglvYmplY3RfaWQYAyABKAkSNwoFaW5wdXQYBCADKAsyKC5xdWl4b3MucnVudGltZS5JbnZva2VSZXF1ZXN0LklucHV0RW50cnkSMAoMZGVwZW5kZW5jaWVzGAUgAygLMhoucXVpeG9zLkluamVjdGVkRGVwZW5kZW5jeRIyCgdjb250ZXh0GAYgASgLMiEucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvbkNvbnRleHQaOwoKSW5wdXRFbnRyeRILCgNrZXkYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWU6AjgBIoMBCg5JbnZva2VSZXNwb25zZRIKCgJvaxgBIAEoCBIdCgZyZXN1bHQYAiABKAsyDS5jYW1pbm8uVmFsdWUSDQoFZXJyb3IYAyABKAkSNwoMZGVwZW5kZW5jaWVzGAQgAygLMiEucXVpeG9zLnJ1bnRpbWUuRGVyaXZlZERlcGVuZGVuY3kivQIKDFdhdGNoUmVxdWVzdBIVCg1pbnZvY2F0aW9uX2lkGAEgASgJEigKBmV4cG9ydBgCIAEoCzIYLnF1aXhvcy5QYWNrYWdlRXhwb3J0UmVmEhEKCW9iamVjdF9pZBgDIAEoCRI2CgVpbnB1dBgEIAMoCzInLnF1aXhvcy5ydW50aW1lLldhdGNoUmVxdWVzdC5JbnB1dEVudHJ5EjAKDGRlcGVuZGVuY2llcxgFIAMoCzIaLnF1aXhvcy5JbmplY3RlZERlcGVuZGVuY3kSMgoHY29udGV4dBgGIAEoCzIhLnF1aXhvcy5ydW50aW1lLkludm9jYXRpb25Db250ZXh0GjsKCklucHV0RW50cnkSCwoDa2V5GAEgASgJEhwKBXZhbHVlGAIgASgLMg0uY2FtaW5vLlZhbHVlOgI4ASJiChFEZXJpdmVkRGVwZW5kZW5jeRIMCgRraW5kGAEgASgJEhEKCW9iamVjdF9pZBgCIAEoCRIVCg1hdHRhY2htZW50X2lkGAMgASgJEhUKDXByb2plY3Rpb25faWQYBCABKAkilQEKCldhdGNoRXZlbnQSEAoId2F0Y2hfaWQYASABKAkSHAoFdmFsdWUYAiABKAsyDS5jYW1pbm8uVmFsdWUSNwoMZGVwZW5kZW5jaWVzGAMgAygLMiEucXVpeG9zLnJ1bnRpbWUuRGVyaXZlZERlcGVuZGVuY3kSDQoFZXJyb3IYBCABKAkSDwoHaW5pdGlhbBgFIAEoCDKzAwoOUGFja2FnZVJ1bnRpbWUSUAoJSGFuZHNoYWtlEiAucXVpeG9zLnJ1bnRpbWUuSGFuZHNoYWtlUmVxdWVzdBohLnF1aXhvcy5ydW50aW1lLkhhbmRzaGFrZVJlc3BvbnNlEkcKBkludm9rZRIdLnF1aXhvcy5ydW50aW1lLkludm9rZVJlcXVlc3QaHi5xdWl4b3MucnVudGltZS5JbnZva2VSZXNwb25zZRJDCgVXYXRjaBIcLnF1aXhvcy5ydW50aW1lLldhdGNoUmVxdWVzdBoaLnF1aXhvcy5ydW50aW1lLldhdGNoRXZlbnQwARJhChNHZXRJbnZvY2F0aW9uU3RhdHVzEigucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvbkNvbnRyb2xSZXF1ZXN0GiAucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvblN0YXR1cxJeChBDYW5jZWxJbnZvY2F0aW9uEigucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvbkNvbnRyb2xSZXF1ZXN0GiAucXVpeG9zLnJ1bnRpbWUuSW52b2NhdGlvblN0YXR1c2IGcHJvdG8z", [file_camino_api, file_quixos_refs]);
/**
* @generated from message quixos.runtime.HandshakeRequest
@@ -24,6 +24,11 @@ export type HandshakeRequest = Message<"quixos.runtime.HandshakeRequest"> & {
* @generated from field: string orch_protocol_version = 1;
*/
orchProtocolVersion: string;
/**
* @generated from field: string nonce = 2;
*/
nonce: string;
};
/**
@@ -51,6 +56,21 @@ export type HandshakeResponse = Message<"quixos.runtime.HandshakeResponse"> & {
* @generated from field: repeated string export_ids = 3;
*/
exportIds: string[];
/**
* @generated from field: string instance_id = 4;
*/
instanceId: string;
/**
* @generated from field: string authentication_proof = 5;
*/
authenticationProof: string;
/**
* @generated from field: repeated string capabilities = 6;
*/
capabilities: string[];
};
/**
@@ -60,6 +80,91 @@ export type HandshakeResponse = Message<"quixos.runtime.HandshakeResponse"> & {
export const HandshakeResponseSchema: GenMessage<HandshakeResponse> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 1);
/**
* @generated from message quixos.runtime.InvocationContext
*/
export type InvocationContext = Message<"quixos.runtime.InvocationContext"> & {
/**
* @generated from field: string workspace_epoch = 1;
*/
workspaceEpoch: string;
/**
* @generated from field: string instance_id = 2;
*/
instanceId: string;
/**
* @generated from field: string binding_digest = 3;
*/
bindingDigest: string;
/**
* @generated from field: string grant = 4;
*/
grant: string;
/**
* @generated from field: string session_id = 5;
*/
sessionId: string;
/**
* Host-selected owner; packages must not invent workspace-local ownership.
*
* @generated from field: string owner_conformance_id = 6;
*/
ownerConformanceId: string;
};
/**
* Describes the message quixos.runtime.InvocationContext.
* Use `create(InvocationContextSchema)` to create a new message.
*/
export const InvocationContextSchema: GenMessage<InvocationContext> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 2);
/**
* @generated from message quixos.runtime.InvocationControlRequest
*/
export type InvocationControlRequest = Message<"quixos.runtime.InvocationControlRequest"> & {
/**
* @generated from field: string invocation_id = 1;
*/
invocationId: string;
};
/**
* Describes the message quixos.runtime.InvocationControlRequest.
* Use `create(InvocationControlRequestSchema)` to create a new message.
*/
export const InvocationControlRequestSchema: GenMessage<InvocationControlRequest> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 3);
/**
* @generated from message quixos.runtime.InvocationStatus
*/
export type InvocationStatus = Message<"quixos.runtime.InvocationStatus"> & {
/**
* @generated from field: string invocation_id = 1;
*/
invocationId: string;
/**
* unknown, running, cancellation-requested, completed, failed
*
* @generated from field: string state = 2;
*/
state: string;
};
/**
* Describes the message quixos.runtime.InvocationStatus.
* Use `create(InvocationStatusSchema)` to create a new message.
*/
export const InvocationStatusSchema: GenMessage<InvocationStatus> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 4);
/**
* @generated from message quixos.runtime.InvokeRequest
*/
@@ -88,6 +193,11 @@ export type InvokeRequest = Message<"quixos.runtime.InvokeRequest"> & {
* @generated from field: repeated quixos.InjectedDependency dependencies = 5;
*/
dependencies: InjectedDependency[];
/**
* @generated from field: quixos.runtime.InvocationContext context = 6;
*/
context?: InvocationContext | undefined;
};
/**
@@ -95,7 +205,7 @@ export type InvokeRequest = Message<"quixos.runtime.InvokeRequest"> & {
* Use `create(InvokeRequestSchema)` to create a new message.
*/
export const InvokeRequestSchema: GenMessage<InvokeRequest> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 2);
messageDesc(file_quixos_runtime, 5);
/**
* @generated from message quixos.runtime.InvokeResponse
@@ -127,7 +237,7 @@ export type InvokeResponse = Message<"quixos.runtime.InvokeResponse"> & {
* Use `create(InvokeResponseSchema)` to create a new message.
*/
export const InvokeResponseSchema: GenMessage<InvokeResponse> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 3);
messageDesc(file_quixos_runtime, 6);
/**
* @generated from message quixos.runtime.WatchRequest
@@ -157,6 +267,11 @@ export type WatchRequest = Message<"quixos.runtime.WatchRequest"> & {
* @generated from field: repeated quixos.InjectedDependency dependencies = 5;
*/
dependencies: InjectedDependency[];
/**
* @generated from field: quixos.runtime.InvocationContext context = 6;
*/
context?: InvocationContext | undefined;
};
/**
@@ -164,7 +279,7 @@ export type WatchRequest = Message<"quixos.runtime.WatchRequest"> & {
* Use `create(WatchRequestSchema)` to create a new message.
*/
export const WatchRequestSchema: GenMessage<WatchRequest> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 4);
messageDesc(file_quixos_runtime, 7);
/**
* @generated from message quixos.runtime.DerivedDependency
@@ -196,7 +311,7 @@ export type DerivedDependency = Message<"quixos.runtime.DerivedDependency"> & {
* Use `create(DerivedDependencySchema)` to create a new message.
*/
export const DerivedDependencySchema: GenMessage<DerivedDependency> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 5);
messageDesc(file_quixos_runtime, 8);
/**
* @generated from message quixos.runtime.WatchEvent
@@ -233,7 +348,7 @@ export type WatchEvent = Message<"quixos.runtime.WatchEvent"> & {
* Use `create(WatchEventSchema)` to create a new message.
*/
export const WatchEventSchema: GenMessage<WatchEvent> = /*@__PURE__*/
messageDesc(file_quixos_runtime, 6);
messageDesc(file_quixos_runtime, 9);
/**
* @generated from service quixos.runtime.PackageRuntime
@@ -263,6 +378,22 @@ export const PackageRuntime: GenService<{
input: typeof WatchRequestSchema;
output: typeof WatchEventSchema;
},
/**
* @generated from rpc quixos.runtime.PackageRuntime.GetInvocationStatus
*/
getInvocationStatus: {
methodKind: "unary";
input: typeof InvocationControlRequestSchema;
output: typeof InvocationStatusSchema;
},
/**
* @generated from rpc quixos.runtime.PackageRuntime.CancelInvocation
*/
cancelInvocation: {
methodKind: "unary";
input: typeof InvocationControlRequestSchema;
output: typeof InvocationStatusSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_quixos_runtime, 0);
+35
View File
@@ -0,0 +1,35 @@
/** Opaque runtime identity. The wire codec, never ordinary package state, owns
* the raw ID. These handles do not themselves confer authority or a lease. */
const identities = new WeakMap<object, string>();
declare const referenceBrand: unique symbol;
export interface QxObjectRef<Identity extends string = string> {
readonly [referenceBrand]: {readonly [K in Identity]: true};
equals(other: QxObjectRef<string>): boolean;
}
class Reference {
constructor(id: string) { identities.set(this, id); Object.freeze(this); }
equals(other: unknown) { return isObjectReference(other) && identities.get(this) === identities.get(other); }
toJSON(): never { throw new Error("Object references cannot be serialized into ordinary data"); }
toString(): never { throw new Error("Object references cannot be coerced to strings"); }
[Symbol.toPrimitive](): never { throw new Error("Object references cannot be coerced to scalar values"); }
}
export const isObjectReference = (value: unknown): value is QxObjectRef =>
typeof value === "object" && value !== null && identities.has(value);
/** Internal transport boundary; intentionally not exported from the SDK entry. */
export const referenceFromWire = (id: string): QxObjectRef => {
if (typeof id !== "string" || !id) throw new Error("Missing object reference identity");
return new Reference(id) as unknown as QxObjectRef;
};
export const referenceToWire = (value: unknown): string => {
if (!isObjectReference(value)) throw new Error("Expected an opaque object reference, not a raw ID");
return identities.get(value)!;
};
export const assertReferenceFree = (value: unknown, seen = new Set<object>()): void => {
if (!value || typeof value !== "object") return;
if (isObjectReference(value)) throw new Error("Managed references belong in declared RPC references or graph relationships, not ordinary state/messages");
if (seen.has(value)) throw new Error("Cyclic ordinary data");
seen.add(value);
if (!(value instanceof Uint8Array)) for (const child of Object.values(value)) assertReferenceFree(child, seen);
seen.delete(value);
};
+59
View File
@@ -0,0 +1,59 @@
import type {QxObjectRef} from "./references.js";
import type {RelationshipCollection, RelationshipEntry} from "./index.js";
type Key = string | boolean | bigint;
type Port<T extends QxObjectRef> = {collection(): Promise<RelationshipCollection<T>>; replace(entries: RelationshipEntry<T>[], expectedRevision: bigint): Promise<RelationshipCollection<T>>};
const checked = async <T extends QxObjectRef>(port: Port<T>, revision: bigint) => {
const snapshot = await port.collection();
if (snapshot.revision !== revision) throw new Error("STALE_COLLECTION_REVISION");
return snapshot;
};
/** Helpers never retry a failed CAS or silently overwrite concurrent edits. */
export const relationshipMap = <T extends QxObjectRef, K extends Key = Key>(port: Port<T>) => ({
read: () => port.collection(),
async get(key: K) {const snapshot = await port.collection(); return {revision: snapshot.revision, value: snapshot.entries.find((entry) => entry.key === key)?.target};},
async set(key: K, target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision);
const entries = snapshot.entries.filter((entry) => entry.key !== key);
const existing = snapshot.entries.find((entry) => entry.key === key && entry.target.equals(target));
entries.push(existing ?? {key, target});
return port.replace(entries, expectedRevision);
},
async delete(key: K, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision);
return port.replace(snapshot.entries.filter((entry) => entry.key !== key), expectedRevision);
},
});
export const relationshipList = <T extends QxObjectRef>(port: Port<T>) => ({
read: () => port.collection(),
async insert(index: number, target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision);
if (!Number.isSafeInteger(index) || index < 0 || index > snapshot.entries.length) throw new Error("List index out of bounds");
snapshot.entries.splice(index, 0, {target});
return port.replace(snapshot.entries, expectedRevision);
},
async move(edgeId: string, index: number, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision);
const prior = snapshot.entries.findIndex((entry) => entry.edgeId === edgeId);
if (prior < 0 || !Number.isSafeInteger(index) || index < 0 || index >= snapshot.entries.length) throw new Error("Unknown list entry or invalid index");
const [entry] = snapshot.entries.splice(prior, 1);
snapshot.entries.splice(index, 0, entry);
return port.replace(snapshot.entries, expectedRevision);
},
async delete(edgeId: string, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision);
if (!snapshot.entries.some((entry) => entry.edgeId === edgeId)) throw new Error("Unknown list entry");
return port.replace(snapshot.entries.filter((entry) => entry.edgeId !== edgeId), expectedRevision);
},
});
export const relationshipSet = <T extends QxObjectRef>(port: Port<T>) => ({
read: () => port.collection(),
async add(target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision);
if (snapshot.entries.some((entry) => entry.target.equals(target))) return snapshot;
return port.replace([...snapshot.entries, {target}], expectedRevision);
},
async delete(target: T, expectedRevision: bigint) {
const snapshot = await checked(port, expectedRevision);
return port.replace(snapshot.entries.filter((entry) => !entry.target.equals(target)), expectedRevision);
},
});