Define plugin sidecar ABI contract

This commit is contained in:
Meta Vens 2026-07-09 02:24:29 +01:00
commit f505b7e86c
16 changed files with 304 additions and 23 deletions

View file

@ -304,13 +304,16 @@ Recently cut:
Unity UI Toolkit release/test/capture statuses plus the tagged UPM release,
batchmode runner, and capture artifact pending proofs from the exported
runtime record.
- Plugin manifests and advertisements now carry a runtime-independent sidecar
ABI boundary. Sai, Norn, and TeX declare `invocationModel:
- Plugin manifests and advertisements now carry a typed runtime-independent
sidecar ABI boundary. Sai, Norn, and TeX declare `invocationModel:
executable-sidecar`, `gamecult.eve.plugin_abi.v1`, CultMesh/stdio transports,
renderer independence, no provider-state mutation, and provider-owned command
acceptance. The parity harness validates manifest/advertisement agreement,
exports the runtime boundary, and the plugin-owner consumer smoke asserts the
Sai/Norn sidecar contract from the conformance pack.
renderer independence, no provider-state mutation, provider-owned command
acceptance, and `runtime.sidecar` fields for process kind, protocol,
request/response schemas, operations, command envelope, receipt schema, and
state authority. The parity harness validates manifest/advertisement
agreement, checks sidecar operations against ABI fixture operations, exports
the runtime boundary, and the plugin-owner consumer smoke asserts the Sai/Norn
sidecar contract from the conformance pack.
- Conformance export now carries first-class `capabilityGaps[]` records derived
from plugin errors, provider advertisement/scenario errors, runtime missing
evidence, declared unsupported plugin projection, capture gaps, and split

View file

@ -164,8 +164,9 @@ manifests, advertisements, ABI fixtures, move-set paths, contract inputs, and
external proofs. It also checks that the manifest and advertisement agree on
the runtime-independent executable sidecar boundary: `gamecult.eve.plugin_abi.v1`,
CultMesh/stdio transport, renderer independence, no provider-state mutation,
and provider-owned command acceptance. The plugin-owner conformance smoke also
asserts those handoff paths and runtime boundary claims from the conformance
provider-owned command acceptance, and `runtime.sidecar` process/protocol/schema
fields. The plugin-owner conformance smoke also asserts those handoff paths,
runtime boundary claims, and exported sidecar fields from the conformance
export.
Aetheria provider handoff smoke:

View file

@ -118,9 +118,12 @@ Plugin manifests and advertisements declare a `runtime` block. Its normal
incubating shape is `invocationModel: executable-sidecar`,
`contract: gamecult.eve.plugin_abi.v1`, CultMesh plus stdio transports, and
authority limits such as `renderer-independent`, `no-provider-state-mutation`,
and `provider-accepts-or-denies-commands`. Runtimes consume that advertisement
as a capability boundary. A Unity or web projection adapter may render an
advertised capability; it does not become the plugin runtime.
and `provider-accepts-or-denies-commands`. The nested `runtime.sidecar` block
names the sidecar process kind, protocol, request/response schemas, ABI
operations, command envelope, receipt schema, and state authority rule. Runtimes
consume that advertisement as a capability boundary. A Unity or web projection
adapter may render an advertised capability; it does not become the plugin
runtime.
The ownership rule is semantic, not packaging ceremony: the plugin owns the
capability and representation shape, while providers own live state and
@ -167,6 +170,12 @@ runtime can return a proposed next state, diagnostics, measurements, or rendered
assets. It cannot decide whether a provider command is allowed, cannot persist
app truth, and cannot write receipts except for its own runtime diagnostics.
The incubating sidecar contract is checked in three places: the plugin manifest,
the plugin advertisement, and the ABI fixture. The parity harness compares
manifest and advertisement `runtime.sidecar` fields, verifies that sidecar
operations cover the ABI fixture operations, and exports the sidecar boundary
through the conformance pack for Sai, Norn, and EvePlugins to consume.
Example ABI request:
```json

View file

@ -12,7 +12,17 @@
"renderer-independent",
"no-provider-state-mutation",
"provider-accepts-or-denies-commands"
]
],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.command_receipt.v1",
"stateAuthority": "proposes-plugin-state-only-provider-accepts"
}
},
"schemas": [
"norn.graph.document.v1",

View file

@ -32,7 +32,17 @@
"renderer-independent",
"no-provider-state-mutation",
"provider-accepts-or-denies-commands"
]
],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.command_receipt.v1",
"stateAuthority": "proposes-plugin-state-only-provider-accepts"
}
},
"stateSchemas": [
"norn.graph.document.v1",

View file

@ -12,7 +12,17 @@
"renderer-independent",
"no-provider-state-mutation",
"provider-accepts-or-denies-commands"
]
],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.command_receipt.v1",
"stateAuthority": "proposes-plugin-state-only-provider-accepts"
}
},
"schemas": [
"sai.vn.story_session.v1",

View file

@ -40,7 +40,17 @@
"renderer-independent",
"no-provider-state-mutation",
"provider-accepts-or-denies-commands"
]
],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.command_receipt.v1",
"stateAuthority": "proposes-plugin-state-only-provider-accepts"
}
},
"stateSchemas": [
"sai.vn.story_session.v1",

View file

@ -12,7 +12,17 @@
"renderer-independent",
"no-provider-state-mutation",
"provider-accepts-or-denies-commands"
]
],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
"stateAuthority": "emits-render-results-only-provider-owns-source-state"
}
},
"schemas": [
"tex.math.source.v1",

View file

@ -21,7 +21,17 @@
"renderer-independent",
"no-provider-state-mutation",
"provider-accepts-or-denies-commands"
]
],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
"stateAuthority": "emits-render-results-only-provider-owns-source-state"
}
},
"stateSchemas": [
"tex.math.source.v1",

View file

@ -99,7 +99,8 @@
"invocationModel",
"contract",
"transports",
"authority"
"authority",
"sidecar"
],
"properties": {
"invocationModel": {
@ -123,6 +124,57 @@
"type": "string",
"minLength": 1
}
},
"sidecar": {
"type": "object",
"required": [
"processKind",
"protocol",
"requestSchema",
"responseSchema",
"operations",
"commandEnvelope",
"receiptSchema",
"stateAuthority"
],
"properties": {
"processKind": {
"type": "string",
"minLength": 1
},
"protocol": {
"type": "string",
"minLength": 1
},
"requestSchema": {
"type": "string",
"minLength": 1
},
"responseSchema": {
"type": "string",
"minLength": 1
},
"operations": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"commandEnvelope": {
"type": "string",
"minLength": 1
},
"receiptSchema": {
"type": "string",
"minLength": 1
},
"stateAuthority": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
}
},
"additionalProperties": true

View file

@ -41,7 +41,8 @@
"invocationModel",
"contract",
"transports",
"authority"
"authority",
"sidecar"
],
"properties": {
"invocationModel": {
@ -65,6 +66,57 @@
"type": "string",
"minLength": 1
}
},
"sidecar": {
"type": "object",
"required": [
"processKind",
"protocol",
"requestSchema",
"responseSchema",
"operations",
"commandEnvelope",
"receiptSchema",
"stateAuthority"
],
"properties": {
"processKind": {
"type": "string",
"minLength": 1
},
"protocol": {
"type": "string",
"minLength": 1
},
"requestSchema": {
"type": "string",
"minLength": 1
},
"responseSchema": {
"type": "string",
"minLength": 1
},
"operations": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"commandEnvelope": {
"type": "string",
"minLength": 1
},
"receiptSchema": {
"type": "string",
"minLength": 1
},
"stateAuthority": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
}
},
"additionalProperties": true

View file

@ -90,12 +90,26 @@ foreach ($handoffPath in $HandoffPaths) {
throw "Plugin handoff $($handoff.pluginId) advertisement runtime missing authority: $authority"
}
}
foreach ($sidecarProperty in @("processKind", "protocol", "requestSchema", "responseSchema", "commandEnvelope", "receiptSchema", "stateAuthority")) {
if (-not $manifest.runtime.sidecar.$sidecarProperty) {
throw "Plugin handoff $($handoff.pluginId) manifest sidecar missing property: $sidecarProperty"
}
if ($advertisement.runtime.sidecar.$sidecarProperty -ne $manifest.runtime.sidecar.$sidecarProperty) {
throw "Plugin handoff $($handoff.pluginId) advertisement sidecar $sidecarProperty does not match manifest: $($advertisement.runtime.sidecar.$sidecarProperty)"
}
}
$operations = @($abiFixture.operations | ForEach-Object { $_.operation })
foreach ($operation in @("describe", "validate", "project", "lower", "measure", "apply")) {
if (-not ($operations -contains $operation)) {
throw "Plugin handoff $($handoff.pluginId) ABI fixture missing operation: $operation"
}
if (-not (@($manifest.runtime.sidecar.operations) -contains $operation)) {
throw "Plugin handoff $($handoff.pluginId) manifest sidecar missing operation: $operation"
}
if (-not (@($advertisement.runtime.sidecar.operations) -contains $operation)) {
throw "Plugin handoff $($handoff.pluginId) advertisement sidecar missing operation: $operation"
}
}
foreach ($moveSet in @($handoff.moveSets)) {

View file

@ -58,9 +58,19 @@ node $consumerScript $consumerExport `
--expect-plugin-runtime sai.vn:executable-sidecar `
--expect-plugin-runtime-transport sai.vn:cultmesh `
--expect-plugin-runtime-authority sai.vn:no-provider-state-mutation `
--expect-plugin-runtime-field sai.vn:sidecar.processKind:long-running-daemon `
--expect-plugin-runtime-field sai.vn:sidecar.protocol:cultmesh-rpc-with-stdio-dev-transport `
--expect-plugin-runtime-field sai.vn:sidecar.requestSchema:gamecult.eve.plugin_abi.request.v1 `
--expect-plugin-runtime-field sai.vn:sidecar.responseSchema:gamecult.eve.plugin_abi.response.v1 `
--expect-plugin-runtime-field sai.vn:sidecar.stateAuthority:proposes-plugin-state-only-provider-accepts `
--expect-plugin-runtime norn.graph:executable-sidecar `
--expect-plugin-runtime-transport norn.graph:cultmesh `
--expect-plugin-runtime-authority norn.graph:no-provider-state-mutation
--expect-plugin-runtime-authority norn.graph:no-provider-state-mutation `
--expect-plugin-runtime-field norn.graph:sidecar.processKind:long-running-daemon `
--expect-plugin-runtime-field norn.graph:sidecar.protocol:cultmesh-rpc-with-stdio-dev-transport `
--expect-plugin-runtime-field norn.graph:sidecar.requestSchema:gamecult.eve.plugin_abi.request.v1 `
--expect-plugin-runtime-field norn.graph:sidecar.responseSchema:gamecult.eve.plugin_abi.response.v1 `
--expect-plugin-runtime-field norn.graph:sidecar.stateAuthority:proposes-plugin-state-only-provider-accepts
if ($LASTEXITCODE -ne 0) {
throw "Plugin owner conformance consumer smoke failed with exit code $LASTEXITCODE"

View file

@ -24,6 +24,7 @@ if (!exportDirectory) {
" --expect-plugin-runtime <pluginId:invocationModel>",
" --expect-plugin-runtime-transport <pluginId:transport>",
" --expect-plugin-runtime-authority <pluginId:authority>",
" --expect-plugin-runtime-field <pluginId:field.path:value>",
" --expect-plugin-handoff <pluginId>",
" --expect-provider-surface <providerId:surfaceId>",
" --expect-provider-surface-kind <providerId:surfaceId:surfaceKind>",
@ -189,6 +190,17 @@ function validateIndex(index, directory, expectations, errors) {
errors.push(`plugins:${expectation.pluginId}:runtime.authority:${expectation.authority}:missing`);
}
}
for (const expectation of expectations.pluginRuntimeFields) {
const plugin = plugins.find(candidate => candidate.pluginId === expectation.pluginId);
if (!plugin) {
errors.push(`plugins:${expectation.pluginId}:missing`);
continue;
}
const actual = readNestedField(plugin.runtimeBoundary, expectation.fieldPath);
if (actual !== expectation.value) {
errors.push(`plugins:${expectation.pluginId}:runtime.${expectation.fieldPath}:expected ${expectation.value} got ${actual || ""}`);
}
}
for (const expectedPlugin of expectations.pluginHandoffs) {
const plugin = plugins.find(candidate => candidate.pluginId === expectedPlugin);
if (!plugin) {
@ -497,6 +509,7 @@ function parseArguments(args) {
pluginRuntimes: [],
pluginRuntimeTransports: [],
pluginRuntimeAuthorities: [],
pluginRuntimeFields: [],
pluginHandoffs: [],
providers: [],
providerSurfaces: [],
@ -535,6 +548,7 @@ function parseArguments(args) {
["--expect-plugin-runtime", expectations.pluginRuntimes],
["--expect-plugin-runtime-transport", expectations.pluginRuntimeTransports],
["--expect-plugin-runtime-authority", expectations.pluginRuntimeAuthorities],
["--expect-plugin-runtime-field", expectations.pluginRuntimeFields],
["--expect-plugin-handoff", expectations.pluginHandoffs],
["--expect-provider-surface", expectations.providerSurfaces],
["--expect-provider-surface-kind", expectations.providerSurfaceKinds],
@ -585,6 +599,8 @@ function parseArguments(args) {
target.push(parsePluginExpectation(value, "transport"));
} else if (option === "--expect-plugin-runtime-authority") {
target.push(parsePluginExpectation(value, "authority"));
} else if (option === "--expect-plugin-runtime-field") {
target.push(parsePluginRuntimeFieldExpectation(value));
} else if (option === "--expect-plugin-handoff") {
target.push(value);
} else if (option === "--expect-provider-surface") {
@ -640,6 +656,19 @@ function parsePluginExpectation(value, field) {
};
}
function parsePluginRuntimeFieldExpectation(value) {
const parts = value.split(":");
if (parts.length < 3 || !parts[0] || !parts[1] || !parts.slice(2).join(":")) {
console.error(`Expected plugin runtime field in <pluginId:field.path:value> form, got: ${value}`);
process.exit(2);
}
return {
pluginId: parts[0],
fieldPath: parts[1],
value: parts.slice(2).join(":"),
};
}
function parseProviderExpectation(value, field) {
const separator = value.indexOf(":");
if (separator <= 0 || separator === value.length - 1) {

View file

@ -376,7 +376,16 @@
"invocationModel": "executable-sidecar",
"contract": "gamecult.eve.plugin_abi.v1",
"transports": ["cultmesh", "stdio"],
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"]
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.command_receipt.v1",
"stateAuthority": "proposes-plugin-state-only-provider-accepts"
}
},
"requiredFixtures": ["sai-vn"],
"schemaPath": "schemas/gamecult.eve.plugin.v1.schema.json",
@ -406,7 +415,16 @@
"invocationModel": "executable-sidecar",
"contract": "gamecult.eve.plugin_abi.v1",
"transports": ["cultmesh", "stdio"],
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"]
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.command_receipt.v1",
"stateAuthority": "proposes-plugin-state-only-provider-accepts"
}
},
"requiredFixtures": ["sai-vn"],
"schemaPath": "schemas/gamecult.eve.plugin.v1.schema.json",
@ -436,7 +454,16 @@
"invocationModel": "executable-sidecar",
"contract": "gamecult.eve.plugin_abi.v1",
"transports": ["cultmesh", "stdio"],
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"]
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultmesh-rpc-with-stdio-dev-transport",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"commandEnvelope": "gamecult.eve.command.v1",
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
"stateAuthority": "emits-render-results-only-provider-owns-source-state"
}
},
"requiredFixtures": ["sai-vn"],
"schemaPath": "schemas/gamecult.eve.plugin.v1.schema.json",

View file

@ -518,10 +518,12 @@ async function readPluginRuntimeBoundary(plugin) {
contract: manifestDocument.runtime?.contract || "",
transports: manifestDocument.runtime?.transports || [],
authority: manifestDocument.runtime?.authority || [],
sidecar: manifestDocument.runtime?.sidecar || {},
advertisementInvocationModel: advertisementDocument.runtime?.invocationModel || "",
advertisementContract: advertisementDocument.runtime?.contract || "",
advertisementTransports: advertisementDocument.runtime?.transports || [],
advertisementAuthority: advertisementDocument.runtime?.authority || [],
advertisementSidecar: advertisementDocument.runtime?.sidecar || {},
};
} catch {
return {
@ -529,10 +531,12 @@ async function readPluginRuntimeBoundary(plugin) {
contract: "",
transports: [],
authority: [],
sidecar: {},
advertisementInvocationModel: "",
advertisementContract: "",
advertisementTransports: [],
advertisementAuthority: [],
advertisementSidecar: {},
};
}
}
@ -560,6 +564,21 @@ function validatePluginRuntimeBoundary(plugin, runtimeBoundary) {
errors.push(...missingMembers(expectedRuntime.transports || [], runtimeBoundary.advertisementTransports || [], "runtime.advertisementTransports"));
errors.push(...missingMembers(expectedRuntime.authority || [], runtimeBoundary.authority || [], "runtime.authority"));
errors.push(...missingMembers(expectedRuntime.authority || [], runtimeBoundary.advertisementAuthority || [], "runtime.advertisementAuthority"));
errors.push(...comparePluginSidecar(expectedRuntime.sidecar || {}, runtimeBoundary.sidecar || {}, "runtime.sidecar"));
errors.push(...comparePluginSidecar(runtimeBoundary.sidecar || {}, runtimeBoundary.advertisementSidecar || {}, "runtime.advertisementSidecar"));
return errors;
}
function comparePluginSidecar(expected, actual, label) {
const errors = [];
for (const key of ["processKind", "protocol", "requestSchema", "responseSchema", "commandEnvelope", "receiptSchema", "stateAuthority"]) {
if (expected[key] && expected[key] !== actual[key]) {
errors.push(`${label}.${key}:expected ${expected[key]} got ${actual[key] || ""}`);
}
}
if (Array.isArray(expected.operations) && expected.operations.length) {
errors.push(...missingMembers(expected.operations, actual.operations || [], `${label}.operations`));
}
return errors;
}
@ -837,6 +856,11 @@ async function validatePluginAbiFixture(plugin) {
for (const operation of ["describe", "validate", "project", "lower", "measure", "apply"]) {
if (!operations.has(operation)) errors.push(`${plugin.abiFixturePath}:operation:${operation}:missing`);
}
errors.push(...missingMembers(
[...operations.keys()].filter(Boolean),
pluginManifest.runtime?.sidecar?.operations || [],
`${plugin.manifestPath}:runtime.sidecar.operations`,
));
if (!(pluginManifest.abiFixtures || []).includes(plugin.abiFixturePath)) {
errors.push(`${plugin.manifestPath}:abiFixtures:${plugin.abiFixturePath}:missing`);