Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ function bundle(providerId: string): RuntimeProviderBundle {
supported: true,
operations: ["rebuild"],
},
stateMutation: unsupported(providerId),
bootstrap: unsupported(providerId),
snapshot: unsupported(providerId),
recovery: unsupported(providerId),
Expand Down
5 changes: 5 additions & 0 deletions src/lib/onboard/runtime-provider/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export type {
RuntimeProviderChannelStopTransport,
RuntimeProviderGatewayLauncher,
RuntimeProviderManagedImageSupport,
RuntimeProviderPreparedStateMutationPlan,
RuntimeProviderStateMutationPlan,
RuntimeProviderStateMutationSelector,
RuntimeProviderStateMutationSurface,
RuntimeProviderWorkloadCleanupPlan,
RuntimeProviderWorkloadCleanupResult,
RuntimeProviderWorkloadProfile,
Expand All @@ -26,3 +30,4 @@ export {
resolveRuntimeProviderBundle,
runtimeProviderContainerEngineIdentity,
} from "./registry";
export { prepareRuntimeProviderStateMutationPlan } from "./state-mutation";
77 changes: 77 additions & 0 deletions src/lib/onboard/runtime-provider/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type { ManagedImageSelectionPolicy } from "../workload/source";
export const RUNTIME_PROVIDER_BUNDLE_CONTRACT_VERSION = 1 as const;
export const RUNTIME_PROVIDER_SNAPSHOT_CONTRACT_VERSION = 1 as const;
export const RUNTIME_PROVIDER_SNAPSHOT_PREFLIGHT_SCHEMA_VERSION = 1 as const;
export const RUNTIME_PROVIDER_STATE_MUTATION_CONTRACT_VERSION = 1 as const;
export const RUNTIME_PROVIDER_STATE_MUTATION_PLAN_SCHEMA_VERSION = 1 as const;

export type RuntimeProviderGatewayLauncher = "nemoclaw" | "openshell";
export type RuntimeProviderLifecycleAction = "start" | "stop";
Expand Down Expand Up @@ -201,6 +203,56 @@ export interface RuntimeProviderManagedProfileRestoreAuthority {
readonly profileFingerprint: string;
}

export type RuntimeProviderStateMutationSelector =
| { readonly kind: "path"; readonly path: string }
| { readonly kind: "prefix"; readonly prefix: string };

/** One bounded state scope. Providers never accept commands or callbacks here. */
export interface RuntimeProviderStateMutationPlan {
readonly schemaVersion: typeof RUNTIME_PROVIDER_STATE_MUTATION_PLAN_SCHEMA_VERSION;
readonly intent: "protection-transition" | "restore";
readonly stateRoot: string;
readonly selectors: readonly RuntimeProviderStateMutationSelector[];
/** Digest of the complete projection produced by the selected AgentDefinition. */
readonly projectionSha256: string;
}

export interface RuntimeProviderPreparedStateMutationPlan {
readonly plan: RuntimeProviderStateMutationPlan;
readonly planSha256: string;
readonly projectionSha256: string;
}

export interface RuntimeProviderStateMutationContext {
readonly environment: NodeJS.ProcessEnv;
readonly sandbox: SandboxEntry;
readonly sandboxName: string;
}

/** Opaque provider proof for one durable, exact-runtime active fence. */
export interface RuntimeProviderStateMutationFence {
readonly schemaVersion: 1;
readonly providerId: string;
readonly sandboxName: string;
readonly lifecycleGeneration: string;
readonly stateRoot: string;
readonly planSha256: string;
readonly projectionSha256: string;
readonly nonce: string;
readonly providerHandle: string;
}

/** Fresh service evidence required before an active fence may be retired. */
export interface RuntimeProviderStateMutationActivationProof {
readonly schemaVersion: 1;
readonly providerId: string;
readonly sandboxName: string;
readonly lifecycleGeneration: string;
readonly configurationGeneration: string;
readonly listenerIdentity: string;
readonly healthSha256: string;
}

/**
* Complete normalized source state supplied to the owning restore facet.
* `providerHandle` binds the lifecycle generation and full runtime receipt.
Expand Down Expand Up @@ -265,6 +317,30 @@ export type RuntimeProviderMutationAuthoritySurface =
}>
| RuntimeProviderUnsupportedSurface;

export type RuntimeProviderStateMutationSurface =
| RuntimeProviderSupportedSurface<{
readonly contractVersion: typeof RUNTIME_PROVIDER_STATE_MUTATION_CONTRACT_VERSION;
acquire(
input: RuntimeProviderStateMutationContext & {
/** Frozen, digested output of prepareRuntimeProviderStateMutationPlan. */
readonly plan: RuntimeProviderPreparedStateMutationPlan;
},
): Promise<RuntimeProviderStateMutationFence>;
assertFenced(
input: RuntimeProviderStateMutationContext,
fence: RuntimeProviderStateMutationFence,
): Promise<void>;
activate(
input: RuntimeProviderStateMutationContext,
fence: RuntimeProviderStateMutationFence,
proof: RuntimeProviderStateMutationActivationProof,
): Promise<void>;
recover(
input: RuntimeProviderStateMutationContext,
): Promise<RuntimeProviderStateMutationFence | null>;
}>
| RuntimeProviderUnsupportedSurface;

export type RuntimeProviderBootstrapSurface =
| RuntimeProviderSupportedSurface<{
createLifecycle(
Expand Down Expand Up @@ -360,6 +436,7 @@ export interface RuntimeProviderBundle {
readonly workload: RuntimeProviderWorkloadSurface;
readonly lifecycle: RuntimeProviderLifecycleSurface;
readonly mutationAuthority: RuntimeProviderMutationAuthoritySurface;
readonly stateMutation: RuntimeProviderStateMutationSurface;
readonly bootstrap: RuntimeProviderBootstrapSurface;
readonly snapshot: RuntimeProviderSnapshotSurface;
readonly recovery: RuntimeProviderRecoverySurface;
Expand Down
8 changes: 8 additions & 0 deletions src/lib/onboard/runtime-provider/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ export function createDockerRuntimeProviderBundle(
"workload-cleanup",
],
},
stateMutation: unsupported(
providerId,
"Exact-runtime state mutation requires durable writer exclusion and fresh activation proof.",
),
bootstrap: unsupported(providerId, futureReason),
snapshot: createDockerRuntimeProviderSnapshotSurface(providerId, {
captureHostCommand: deps.captureHostCommand,
Expand Down Expand Up @@ -440,6 +444,10 @@ export function createKubernetesRuntimeProviderBundle(
"workload-cleanup",
],
},
stateMutation: unsupported(
providerId,
"Exact-runtime state mutation is unavailable for the Kubernetes provider.",
),
bootstrap: unsupported(providerId, futureReason),
snapshot: unsupported(providerId, futureReason),
recovery: unsupported(providerId, futureReason),
Expand Down
15 changes: 15 additions & 0 deletions src/lib/onboard/runtime-provider/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
RUNTIME_PROVIDER_BUNDLE_CONTRACT_VERSION,
RUNTIME_PROVIDER_SNAPSHOT_CONTRACT_VERSION,
RUNTIME_PROVIDER_SNAPSHOT_PREFLIGHT_SCHEMA_VERSION,
RUNTIME_PROVIDER_STATE_MUTATION_CONTRACT_VERSION,
type RuntimeProviderBundle,
type RuntimeProviderBundleRegistry,
type RuntimeProviderChannelStopTransport,
Expand All @@ -29,6 +30,7 @@ const BUNDLE_SURFACES = [
"workload",
"lifecycle",
"mutationAuthority",
"stateMutation",
"bootstrap",
"snapshot",
"recovery",
Expand Down Expand Up @@ -335,6 +337,18 @@ function validateMutationAuthoritySurface(
}
}

function validateStateMutationSurface(providerId: string, surface: Record<string, unknown>): void {
if (surface.supported !== true) return;
if (surface.contractVersion !== RUNTIME_PROVIDER_STATE_MUTATION_CONTRACT_VERSION) {
throw new RuntimeProviderRegistrationError(
`stateMutation for '${providerId}' has an unsupported contract version`,
);
}
for (const operation of ["acquire", "assertFenced", "activate", "recover"] as const) {
requireFunction(surface, operation, "stateMutation");
}
}

function validateBootstrapSurface(surface: Record<string, unknown>): void {
if (surface.supported === true) {
requireFunction(surface, "createLifecycle", "bootstrap");
Expand Down Expand Up @@ -429,6 +443,7 @@ function validateSupportedSurfaceSchemas(
validateWorkloadSurface(providerId, surfaces.workload);
validateLifecycleSurface(providerId, surfaces.lifecycle);
validateMutationAuthoritySurface(providerId, surfaces.mutationAuthority);
validateStateMutationSurface(providerId, surfaces.stateMutation);
validateBootstrapSurface(surfaces.bootstrap);
validateSnapshotSurface(providerId, surfaces.snapshot);
validateRecoverySurface(surfaces.recovery);
Expand Down
41 changes: 38 additions & 3 deletions src/lib/onboard/runtime-provider/runtime-provider-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ describe("RuntimeProviderBundle registry contract", () => {
"workload",
"lifecycle",
"mutationAuthority",
"stateMutation",
"bootstrap",
"snapshot",
"recovery",
Expand All @@ -130,6 +131,7 @@ describe("RuntimeProviderBundle registry contract", () => {
expect(bundle[surface].providerId, `${providerId}.${surface}`).toBe(providerId);
}
expect(bundle.bootstrap).toMatchObject({ supported: false });
expect(bundle.stateMutation).toMatchObject({ supported: false });
expect(bundle.snapshot).toMatchObject(
providerId === "docker"
? {
Expand Down Expand Up @@ -287,10 +289,10 @@ describe("RuntimeProviderBundle registry contract", () => {

it("rejects a missing surface and every surface identity mismatch", () => {
const bundle = mxcBundle();
const { cleanup: _cleanup, ...missingCleanup } = bundle;
const { stateMutation: _stateMutation, ...missingStateMutation } = bundle;
expect(() =>
createRuntimeProviderBundleRegistry([["mxc", missingCleanup as RuntimeProviderBundle]]),
).toThrow(/missing cleanup surface/u);
createRuntimeProviderBundleRegistry([["mxc", missingStateMutation as RuntimeProviderBundle]]),
).toThrow(/missing stateMutation surface/u);

for (const surface of [
"plan",
Expand All @@ -300,6 +302,7 @@ describe("RuntimeProviderBundle registry contract", () => {
"workload",
"lifecycle",
"mutationAuthority",
"stateMutation",
"bootstrap",
"snapshot",
"recovery",
Expand Down Expand Up @@ -370,6 +373,19 @@ describe("RuntimeProviderBundle registry contract", () => {
operations: ["not-an-operation"],
}),
],
[
"stateMutation",
(bundle: RuntimeProviderBundle) => ({
...bundle.stateMutation,
supported: true,
reason: undefined,
contractVersion: 2,
acquire: vi.fn(),
assertFenced: vi.fn(),
activate: vi.fn(),
recover: vi.fn(),
}),
],
[
"bootstrap",
(bundle: RuntimeProviderBundle) => ({
Expand Down Expand Up @@ -434,6 +450,25 @@ describe("RuntimeProviderBundle registry contract", () => {
).toThrow(/lifecycle\.verifyStarted must be a function/u);
});

it("rejects supported state mutation without the complete durable-fence protocol", () => {
const bundle = mxcBundle();
expect(() =>
createRuntimeProviderBundleRegistry([
[
"mxc",
replaceSurface(bundle, "stateMutation", {
providerId: "mxc",
supported: true,
contractVersion: 1,
acquire: vi.fn(),
assertFenced: vi.fn(),
activate: vi.fn(),
}),
],
]),
).toThrow(/stateMutation\.recover must be a function/u);
});

it("rejects cleanup without a side-effect-free ownership plan", () => {
const bundle = mxcBundle();
expectSupportedSurface(bundle.cleanup);
Expand Down
Loading
Loading