Merge Eve invocation and presentation authority

This commit is contained in:
Meta Vens 2026-08-26 20:14:12 +02:00
commit bfdaff2d68
23 changed files with 444 additions and 31 deletions

View file

@ -1279,8 +1279,8 @@ export function createInventoryDropIntent(source, target, destinationX, destinat
const targetIndex = numberProp(target.targetIndex, -1);
const targetKind = firstString(target.targetKind, "");
const payload = {};
copyInventoryPayloadProps(source, payload);
copyInventoryPayloadProps(target, payload);
copyPayloadProps(source, payload);
copyPayloadProps(target, payload);
Object.assign(payload, {
sourceKind,
originEntityKey: firstString(source.sourceEntityKey, source.entityKey, ""),
@ -1353,7 +1353,7 @@ function parseInventoryCells(value) {
return Number.isInteger(x) && Number.isInteger(y) ? [{ x, y }] : [];
});
}
function copyInventoryPayloadProps(source, payload) {
function copyPayloadProps(source, payload) {
for (const [key, value] of Object.entries(source)) {
if (key.startsWith("payload.") && key.length > "payload.".length)
payload[key.slice("payload.".length)] = value;
@ -1702,7 +1702,9 @@ export function createEveCommandIntent(commandId, props = {}, options = {}) {
...stringArray(props.captureBindings),
])];
const bindings = captureBindingValues(surface?.surface?.root, providerId, surfaceId, captureBindings, options.draftStore);
const payload = commandPayload(action);
const payload = {};
copyPayloadProps(props, payload);
Object.assign(payload, commandPayload(action));
if (captureBindings.length)
payload.bindings = bindings;
const intent = {

View file

@ -1520,8 +1520,8 @@ export function createInventoryDropIntent(
const targetIndex = numberProp(target.targetIndex, -1);
const targetKind = firstString(target.targetKind, "");
const payload: Record<string, unknown> = {};
copyInventoryPayloadProps(source, payload);
copyInventoryPayloadProps(target, payload);
copyPayloadProps(source, payload);
copyPayloadProps(target, payload);
Object.assign(payload, {
sourceKind,
originEntityKey: firstString(source.sourceEntityKey, source.entityKey, ""),
@ -1610,7 +1610,7 @@ function parseInventoryCells(value: unknown): InventoryPlacementCell[] {
});
}
function copyInventoryPayloadProps(source: Record<string, unknown>, payload: Record<string, unknown>): void {
function copyPayloadProps(source: Record<string, unknown>, payload: Record<string, unknown>): void {
for (const [key, value] of Object.entries(source)) {
if (key.startsWith("payload.") && key.length > "payload.".length)
payload[key.slice("payload.".length)] = value;
@ -2007,7 +2007,9 @@ export function createEveCommandIntent(
captureBindings,
options.draftStore,
);
const payload = commandPayload(action);
const payload: Record<string, unknown> = {};
copyPayloadProps(props, payload);
Object.assign(payload, commandPayload(action));
if (captureBindings.length) payload.bindings = bindings;
const intent: EveCommandIntent = {
schema: "gamecult.eve.command_invocation.v1",

View file

@ -59,6 +59,30 @@ test("world interactions emit provider-routed command intents without local auth
assert.equal(intent.payload.directionX, 1);
});
test("ordinary controls preserve provider-declared payload fields", () => {
const intent = createEveCommandIntent("aetheria.hangar.launch", {
label: "LAUNCH",
command: "aetheria.hangar.launch",
"payload.expectedProgressionVerseId": "gamecult.aetheria",
"payload.expectedProgressionSourceRevision": "7",
}, {
activeSurfaceId: "aetheria.hangar",
clientId: "browser.hangar-test",
provider: { providerId: "aetheria", surfaces: [{
surfaceId: "aetheria.hangar",
worldInteraction: {
commandBoundary: "aetheria.hangar.commands",
receiptSchema: "gamecult.eve.command_receipt.v1",
},
}] },
});
assert.equal(intent.payload.expectedProgressionVerseId, "gamecult.aetheria");
assert.equal(intent.payload.expectedProgressionSourceRevision, "7");
assert.equal(intent.payload.label, undefined);
assert.equal(intent.payload.command, undefined);
});
test("inventory drops select the provider-advertised operation and preserve spatial identity", () => {
const intent = createInventoryDropIntent({
sourceKind: "cargo",

View file

@ -18,4 +18,9 @@ export interface EveCommandInvocation {
clientId: string;
commandBoundary: string;
receiptSchema: string;
delegation?: {
originalInvocationHash: string;
originalClientId: string;
delegatingRuntimeId: string;
};
}

View file

@ -13,9 +13,21 @@ export interface EveCommandReceipt {
[k: string]: any;
}[];
issuedAtUtc?: string;
/**
* Provider-state generation that causally owns this result. It is not implicitly an Eve surface version.
*/
sourceVersion: number;
/**
* Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality.
*/
presentationSurfaceVersion?: number;
/**
* Canonical digest of the immutable invocation envelope finalized by this receipt.
*/
invocationHash?: string;
navigation?: {
verseId: string;
authorityRuntimeId?: string;
providerId?: string;
surfaceId: string;
surfaceKind?: string;

View file

@ -32,9 +32,21 @@ export interface EveCommandReceipt {
[k: string]: any;
}[];
issuedAtUtc?: string;
/**
* Provider-state generation that causally owns this result. It is not implicitly an Eve surface version.
*/
sourceVersion: number;
/**
* Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality.
*/
presentationSurfaceVersion?: number;
/**
* Canonical digest of the immutable invocation envelope finalized by this receipt.
*/
invocationHash?: string;
navigation?: {
verseId: string;
authorityRuntimeId?: string;
providerId?: string;
surfaceId: string;
surfaceKind?: string;

View file

@ -596,6 +596,25 @@ export declare const eveContractSchemas: {
readonly type: "string";
readonly minLength: 1;
};
readonly delegation: {
readonly type: "object";
readonly required: readonly ["originalInvocationHash", "originalClientId", "delegatingRuntimeId"];
readonly properties: {
readonly originalInvocationHash: {
readonly type: "string";
readonly minLength: 1;
};
readonly originalClientId: {
readonly type: "string";
readonly minLength: 1;
};
readonly delegatingRuntimeId: {
readonly type: "string";
readonly minLength: 1;
};
};
readonly additionalProperties: false;
};
};
readonly additionalProperties: false;
};
@ -653,6 +672,17 @@ export declare const eveContractSchemas: {
readonly sourceVersion: {
readonly type: "integer";
readonly minimum: 0;
readonly description: "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version.";
};
readonly presentationSurfaceVersion: {
readonly type: "integer";
readonly minimum: 1;
readonly description: "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality.";
};
readonly invocationHash: {
readonly type: "string";
readonly minLength: 1;
readonly description: "Canonical digest of the immutable invocation envelope finalized by this receipt.";
};
readonly navigation: {
readonly type: "object";
@ -662,6 +692,10 @@ export declare const eveContractSchemas: {
readonly type: "string";
readonly minLength: 1;
};
readonly authorityRuntimeId: {
readonly type: "string";
readonly minLength: 1;
};
readonly providerId: {
readonly type: "string";
};

View file

@ -714,6 +714,29 @@ export const eveContractSchemas = {
"receiptSchema": {
"type": "string",
"minLength": 1
},
"delegation": {
"type": "object",
"required": [
"originalInvocationHash",
"originalClientId",
"delegatingRuntimeId"
],
"properties": {
"originalInvocationHash": {
"type": "string",
"minLength": 1
},
"originalClientId": {
"type": "string",
"minLength": 1
},
"delegatingRuntimeId": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false
}
},
"additionalProperties": false
@ -787,7 +810,18 @@ export const eveContractSchemas = {
},
"sourceVersion": {
"type": "integer",
"minimum": 0
"minimum": 0,
"description": "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version."
},
"presentationSurfaceVersion": {
"type": "integer",
"minimum": 1,
"description": "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality."
},
"invocationHash": {
"type": "string",
"minLength": 1,
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
},
"navigation": {
"type": "object",
@ -800,6 +834,10 @@ export const eveContractSchemas = {
"type": "string",
"minLength": 1
},
"authorityRuntimeId": {
"type": "string",
"minLength": 1
},
"providerId": {
"type": "string"
},

View file

@ -20,4 +20,9 @@ issuedAt: string
clientId: string
commandBoundary: string
receiptSchema: string
delegation?: {
originalInvocationHash: string
originalClientId: string
delegatingRuntimeId: string
}
}

View file

@ -15,9 +15,21 @@ diagnostics?: {
[k: string]: any
}[]
issuedAtUtc?: string
/**
* Provider-state generation that causally owns this result. It is not implicitly an Eve surface version.
*/
sourceVersion: number
/**
* Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality.
*/
presentationSurfaceVersion?: number
/**
* Canonical digest of the immutable invocation envelope finalized by this receipt.
*/
invocationHash?: string
navigation?: {
verseId: string
authorityRuntimeId?: string
providerId?: string
surfaceId: string
surfaceKind?: string

View file

@ -34,9 +34,21 @@ diagnostics?: {
[k: string]: any
}[]
issuedAtUtc?: string
/**
* Provider-state generation that causally owns this result. It is not implicitly an Eve surface version.
*/
sourceVersion: number
/**
* Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality.
*/
presentationSurfaceVersion?: number
/**
* Canonical digest of the immutable invocation envelope finalized by this receipt.
*/
invocationHash?: string
navigation?: {
verseId: string
authorityRuntimeId?: string
providerId?: string
surfaceId: string
surfaceKind?: string

View file

@ -714,6 +714,29 @@ export const eveContractSchemas = {
"receiptSchema": {
"type": "string",
"minLength": 1
},
"delegation": {
"type": "object",
"required": [
"originalInvocationHash",
"originalClientId",
"delegatingRuntimeId"
],
"properties": {
"originalInvocationHash": {
"type": "string",
"minLength": 1
},
"originalClientId": {
"type": "string",
"minLength": 1
},
"delegatingRuntimeId": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": false
}
},
"additionalProperties": false
@ -787,7 +810,18 @@ export const eveContractSchemas = {
},
"sourceVersion": {
"type": "integer",
"minimum": 0
"minimum": 0,
"description": "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version."
},
"presentationSurfaceVersion": {
"type": "integer",
"minimum": 1,
"description": "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality."
},
"invocationHash": {
"type": "string",
"minLength": 1,
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
},
"navigation": {
"type": "object",
@ -800,6 +834,10 @@ export const eveContractSchemas = {
"type": "string",
"minLength": 1
},
"authorityRuntimeId": {
"type": "string",
"minLength": 1
},
"providerId": {
"type": "string"
},

View file

@ -82,6 +82,7 @@ test("accepts canonical provider, surface, command, invocation, and receipt docu
providerId: "voidbot.swarm",
surfaceId: "voidbot.swarm",
sourceVersion: 1,
invocationHash: "sha256:command-1",
}).state, "reconciled");
});

View file

@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Description>Portable Eve surface and provider contracts.</Description>
<PackageId>GameCult.Eve.Surface</PackageId>
<Version>0.3.2</Version>
<Version>0.3.6</Version>
<Authors>GameCult</Authors>
<PackageLicenseExpression>MPL-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/GameCult/Eve</PackageProjectUrl>

View file

@ -0,0 +1,53 @@
using System;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
#nullable enable
namespace GameCult.Eve.Surface
{
/// <summary>Canonical digest for one immutable Eve command invocation.</summary>
public static class EveCommandInvocationHash
{
public static string Compute(EveSurfaceCommandRequest request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
var canonical = new StringBuilder();
Append(request.Schema);
Append(request.ProviderId);
Append(request.SurfaceId);
Append(request.OperationRecord.OperationId);
Append(request.OperationRecord.SchemaId);
Append(request.OperationRecord.RouteKind);
Append(request.OperationRecord.RouteDescription);
Append(request.OperationRecord.IdempotencyKey);
foreach (var field in request.PayloadFields.OrderBy(pair => pair.Key, StringComparer.Ordinal))
{
Append(field.Key);
Append(field.Value);
}
Append(request.IssuedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
Append(request.ClientId);
Append(request.CommandBoundary);
Append(request.ReceiptSchema);
Append(request.Delegation?.OriginalInvocationHash);
Append(request.Delegation?.OriginalClientId);
Append(request.Delegation?.DelegatingRuntimeId);
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(canonical.ToString()));
var digest = new StringBuilder(bytes.Length * 2);
foreach (var value in bytes) digest.Append(value.ToString("x2", CultureInfo.InvariantCulture));
return digest.ToString();
void Append(string? value)
{
value ??= "";
canonical.Append(value.Length.ToString(CultureInfo.InvariantCulture));
canonical.Append(':');
canonical.Append(value);
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d5f81f817c44d1ab25d76166354364d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -366,8 +366,10 @@ namespace GameCult.Eve.Surface
string message,
string issuedAtUtc,
long sourceVersion,
EveSurfaceNavigationTarget? navigation = null)
: this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation)
EveSurfaceNavigationTarget? navigation = null,
string invocationHash = "",
long presentationSurfaceVersion = 0)
: this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash, presentationSurfaceVersion)
{
}
@ -385,7 +387,9 @@ namespace GameCult.Eve.Surface
string message,
string issuedAtUtc,
long sourceVersion,
EveSurfaceNavigationTarget? navigation = null)
EveSurfaceNavigationTarget? navigation = null,
string invocationHash = "",
long presentationSurfaceVersion = 0)
{
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ReceiptId = receiptId ?? "";
@ -400,6 +404,8 @@ namespace GameCult.Eve.Surface
IssuedAtUtc = issuedAtUtc ?? "";
SourceVersion = sourceVersion;
Navigation = navigation;
InvocationHash = invocationHash ?? "";
PresentationSurfaceVersion = presentationSurfaceVersion;
}
[Key(0)] public string Schema { get; }
@ -415,6 +421,8 @@ namespace GameCult.Eve.Surface
[Key(10)] public string IssuedAtUtc { get; }
[Key(11)] public long SourceVersion { get; }
[Key(12)] public EveSurfaceNavigationTarget? Navigation { get; }
[Key(13)] public string InvocationHash { get; }
[Key(14)] public long PresentationSurfaceVersion { get; }
}
/// <summary>
@ -424,7 +432,7 @@ namespace GameCult.Eve.Surface
public sealed class EveCommandReceiptCompatibilityFormatter :
IMessagePackFormatter<EveCommandReceiptDocument?>
{
private const int LegacyFieldCount = 13;
private const int LegacyFieldCount = 15;
private const int RequiredAndScalarFieldCount = 12;
public void Serialize(
@ -434,9 +442,10 @@ namespace GameCult.Eve.Surface
{
if (value == null) { writer.WriteNil(); return; }
writer.WriteMapHeader(value.Navigation == null
? RequiredAndScalarFieldCount
: RequiredAndScalarFieldCount + 1);
var optionalFields = (value.Navigation == null ? 0 : 1) +
(string.IsNullOrWhiteSpace(value.InvocationHash) ? 0 : 1) +
(value.PresentationSurfaceVersion <= 0 ? 0 : 1);
writer.WriteMapHeader(RequiredAndScalarFieldCount + optionalFields);
Write(ref writer, "schema", value.Schema);
Write(ref writer, "receiptId", value.ReceiptId);
Write(ref writer, "commandId", value.CommandId);
@ -454,6 +463,13 @@ namespace GameCult.Eve.Surface
writer.Write("navigation");
WriteNavigation(ref writer, value.Navigation);
}
if (!string.IsNullOrWhiteSpace(value.InvocationHash))
Write(ref writer, "invocationHash", value.InvocationHash);
if (value.PresentationSurfaceVersion > 0)
{
writer.Write("presentationSurfaceVersion");
writer.Write(value.PresentationSurfaceVersion);
}
}
public EveCommandReceiptDocument? Deserialize(
@ -483,10 +499,13 @@ namespace GameCult.Eve.Surface
var issuedAtUtc = ReadString(ref reader, fields, 10);
var sourceVersion = fields > 11 ? reader.ReadInt64() : 0;
var navigation = fields > 12 ? ReadNavigation(ref reader, options) : null;
var invocationHash = ReadString(ref reader, fields, 13);
var presentationSurfaceVersion = fields > 14 ? reader.ReadInt64() : 0;
for (var index = LegacyFieldCount; index < fields; index++) reader.Skip();
return new EveCommandReceiptDocument(
schema, receiptId, commandId, command, state, ownerRepo, authority,
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation);
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash,
presentationSurfaceVersion);
}
finally { reader.Depth--; }
}
@ -511,6 +530,8 @@ namespace GameCult.Eve.Surface
var issuedAtUtc = "";
long sourceVersion = 0;
EveSurfaceNavigationTarget? navigation = null;
var invocationHash = "";
long presentationSurfaceVersion = 0;
var fields = reader.ReadMapHeader();
for (var index = 0; index < fields; index++)
{
@ -529,13 +550,16 @@ namespace GameCult.Eve.Surface
case "issuedAtUtc": issuedAtUtc = ReadString(ref reader); break;
case "sourceVersion": sourceVersion = reader.ReadInt64(); break;
case "navigation": navigation = ReadNavigation(ref reader, options); break;
case "invocationHash": invocationHash = ReadString(ref reader); break;
case "presentationSurfaceVersion": presentationSurfaceVersion = reader.ReadInt64(); break;
default: reader.Skip(); break;
}
}
return new EveCommandReceiptDocument(
schema, receiptId, commandId, command, state, ownerRepo, authority,
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation);
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash,
presentationSurfaceVersion);
}
finally { reader.Depth--; }
}
@ -546,8 +570,9 @@ namespace GameCult.Eve.Surface
{
if (navigation == null) { writer.WriteNil(); return; }
writer.WriteMapHeader(5);
writer.WriteMapHeader(6);
Write(ref writer, "verseId", navigation.VerseId);
Write(ref writer, "authorityRuntimeId", navigation.AuthorityRuntimeId);
Write(ref writer, "providerId", navigation.ProviderId);
Write(ref writer, "surfaceId", navigation.SurfaceId);
Write(ref writer, "surfaceKind", navigation.SurfaceKind);
@ -571,6 +596,7 @@ namespace GameCult.Eve.Surface
try
{
var verseId = "";
var authorityRuntimeId = "";
var providerId = "";
var surfaceId = "";
var surfaceKind = "";
@ -581,6 +607,7 @@ namespace GameCult.Eve.Surface
switch (reader.ReadString())
{
case "verseId": verseId = ReadString(ref reader); break;
case "authorityRuntimeId": authorityRuntimeId = ReadString(ref reader); break;
case "providerId": providerId = ReadString(ref reader); break;
case "surfaceId": surfaceId = ReadString(ref reader); break;
case "surfaceKind": surfaceKind = ReadString(ref reader); break;
@ -588,7 +615,8 @@ namespace GameCult.Eve.Surface
default: reader.Skip(); break;
}
}
return new EveSurfaceNavigationTarget(verseId, providerId, surfaceId, surfaceKind, endpoints);
return new EveSurfaceNavigationTarget(
verseId, providerId, surfaceId, surfaceKind, endpoints, authorityRuntimeId);
}
finally { reader.Depth--; }
}
@ -626,9 +654,11 @@ namespace GameCult.Eve.Surface
string providerId,
string surfaceId,
string surfaceKind,
string[]? rendezvousEndpoints = null)
string[]? rendezvousEndpoints = null,
string authorityRuntimeId = "")
{
VerseId = verseId ?? "";
AuthorityRuntimeId = authorityRuntimeId ?? "";
ProviderId = providerId ?? "";
SurfaceId = surfaceId ?? "";
SurfaceKind = surfaceKind ?? "";
@ -640,5 +670,6 @@ namespace GameCult.Eve.Surface
[Key(2)] public string SurfaceId { get; }
[Key(3)] public string SurfaceKind { get; }
[Key(4)] public string[] RendezvousEndpoints { get; }
[Key(5)] public string AuthorityRuntimeId { get; }
}
}

View file

@ -335,7 +335,8 @@ namespace GameCult.Eve.Surface
DateTimeOffset issuedAt,
string clientId,
string commandBoundary = "",
string receiptSchema = "")
string receiptSchema = "",
EveCommandDelegationRecord? delegation = null)
: this(
SchemaId,
providerId,
@ -346,7 +347,8 @@ namespace GameCult.Eve.Surface
issuedAt,
clientId,
commandBoundary,
receiptSchema)
receiptSchema,
delegation)
{
}
@ -360,7 +362,8 @@ namespace GameCult.Eve.Surface
DateTimeOffset issuedAt,
string clientId,
string commandBoundary,
string receiptSchema)
string receiptSchema,
EveCommandDelegationRecord? delegation = null)
{
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ProviderId = providerId;
@ -373,6 +376,7 @@ namespace GameCult.Eve.Surface
ClientId = clientId;
CommandBoundary = commandBoundary ?? "";
ReceiptSchema = receiptSchema ?? "";
Delegation = delegation;
}
[Key(0)]
@ -411,10 +415,32 @@ namespace GameCult.Eve.Surface
[Key(8)]
public string ReceiptSchema { get; }
[Key(9)]
public EveCommandDelegationRecord? Delegation { get; }
[IgnoreMember]
public string CommandId => Operation.IdempotencyKey ?? "";
}
[MessagePackObject]
public sealed class EveCommandDelegationRecord
{
[SerializationConstructor]
public EveCommandDelegationRecord(
string originalInvocationHash,
string originalClientId,
string delegatingRuntimeId)
{
OriginalInvocationHash = originalInvocationHash ?? "";
OriginalClientId = originalClientId ?? "";
DelegatingRuntimeId = delegatingRuntimeId ?? "";
}
[Key(0)] public string OriginalInvocationHash { get; }
[Key(1)] public string OriginalClientId { get; }
[Key(2)] public string DelegatingRuntimeId { get; }
}
/// <summary>
/// Reads the current Eve surface envelope and the original seven-field v1 envelope.
/// </summary>

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.surface",
"displayName": "Eve Surface Contract",
"version": "0.3.2",
"version": "0.3.6",
"unity": "2021.3",
"description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.",
"dependencies": {

View file

@ -33,7 +33,17 @@
"issuedAt": { "type": "string" },
"clientId": { "type": "string", "minLength": 1 },
"commandBoundary": { "type": "string", "minLength": 1 },
"receiptSchema": { "type": "string", "minLength": 1 }
"receiptSchema": { "type": "string", "minLength": 1 },
"delegation": {
"type": "object",
"required": ["originalInvocationHash", "originalClientId", "delegatingRuntimeId"],
"properties": {
"originalInvocationHash": { "type": "string", "minLength": 1 },
"originalClientId": { "type": "string", "minLength": 1 },
"delegatingRuntimeId": { "type": "string", "minLength": 1 }
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View file

@ -67,7 +67,18 @@
},
"sourceVersion": {
"type": "integer",
"minimum": 0
"minimum": 0,
"description": "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version."
},
"presentationSurfaceVersion": {
"type": "integer",
"minimum": 1,
"description": "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality."
},
"invocationHash": {
"type": "string",
"minLength": 1,
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
},
"navigation": {
"type": "object",
@ -80,6 +91,10 @@
"type": "string",
"minLength": 1
},
"authorityRuntimeId": {
"type": "string",
"minLength": 1
},
"providerId": {
"type": "string"
},

View file

@ -68,6 +68,62 @@ public sealed class EveSurfaceSerializationCompatibilityTests
private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard;
[Test]
public void CommandInvocationHashBindsEveryImmutableEnvelopeField()
{
var request = new EveSurfaceCommandRequest(
"provider",
"surface",
new CultMeshOperationInvocationDescriptor(
"launch",
"launch.v1",
new CultMeshRouteHint(CultMeshLocalityKind.Network, "commands"),
"command-1"),
CultMesh.OperationPayload(("shipId", "ship:one")),
DateTimeOffset.Parse("2026-08-19T00:00:00Z"),
"pilot:one",
"eve:commands",
EveCommandReceiptDocument.SchemaId);
var same = new EveSurfaceCommandRequest(
request.Schema,
request.ProviderId,
request.SurfaceId,
request.OperationRecord,
new Dictionary<string, string>(request.PayloadFields, StringComparer.Ordinal),
request.IssuedAt,
request.ClientId,
request.CommandBoundary,
request.ReceiptSchema);
var changedPayload = new EveSurfaceCommandRequest(
request.ProviderId,
request.SurfaceId,
request.Operation,
CultMesh.OperationPayload(("shipId", "ship:two")),
request.IssuedAt,
request.ClientId,
request.CommandBoundary,
request.ReceiptSchema);
Assert.That(EveCommandInvocationHash.Compute(same), Is.EqualTo(EveCommandInvocationHash.Compute(request)));
Assert.That(EveCommandInvocationHash.Compute(changedPayload), Is.Not.EqualTo(EveCommandInvocationHash.Compute(request)));
var delegated = new EveSurfaceCommandRequest(
request.Schema,
request.ProviderId,
request.SurfaceId,
request.OperationRecord,
new Dictionary<string, string>(request.PayloadFields, StringComparer.Ordinal),
request.IssuedAt,
"progression-router",
request.CommandBoundary,
request.ReceiptSchema,
new EveCommandDelegationRecord(
EveCommandInvocationHash.Compute(request),
request.ClientId,
"progression-router"));
Assert.That(EveCommandInvocationHash.Compute(delegated), Is.Not.EqualTo(EveCommandInvocationHash.Compute(request)));
Assert.That(delegated.Delegation!.OriginalInvocationHash, Is.EqualTo(EveCommandInvocationHash.Compute(request)));
}
[Test]
public void LegacySevenFieldSurfaceDocumentDeserializes()
{
@ -208,7 +264,10 @@ public sealed class EveSurfaceSerializationCompatibilityTests
"aetheria.daemon",
"aetheria.pilot",
"interactive-world",
new[] { "cultnet+tcp://odin.gamecult.example:3076" }));
new[] { "cultnet+tcp://odin.gamecult.example:3076" },
"commander-daemon"),
"sha256:launch-envelope",
presentationSurfaceVersion: 77);
var bytes = MessagePackSerializer.Serialize(document, Options);
var reader = new MessagePackReader(bytes);
@ -216,13 +275,16 @@ public sealed class EveSurfaceSerializationCompatibilityTests
var restored = MessagePackSerializer.Deserialize<EveCommandReceiptDocument>(bytes, Options);
Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Map));
Assert.That(reader.ReadMapHeader(), Is.EqualTo(13));
Assert.That(reader.ReadMapHeader(), Is.EqualTo(15));
Assert.That(wireJson.RootElement.GetProperty("navigation").ValueKind, Is.EqualTo(JsonValueKind.Object));
Assert.That(restored.Navigation, Is.Not.Null);
Assert.That(restored.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria"));
Assert.That(restored.Navigation.AuthorityRuntimeId, Is.EqualTo("commander-daemon"));
Assert.That(restored.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot"));
Assert.That(restored.Navigation.SurfaceKind, Is.EqualTo("interactive-world"));
Assert.That(restored.Navigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.gamecult.example:3076" }));
Assert.That(restored.InvocationHash, Is.EqualTo("sha256:launch-envelope"));
Assert.That(restored.PresentationSurfaceVersion, Is.EqualTo(77));
}
[Test]
@ -260,6 +322,9 @@ public sealed class EveSurfaceSerializationCompatibilityTests
Assert.That(restored.CommandId, Is.EqualTo("legacy-command"));
Assert.That(restored.Navigation, Is.Not.Null);
Assert.That(restored.Navigation!.SurfaceId, Is.EqualTo("aetheria.pilot"));
Assert.That(restored.Navigation.AuthorityRuntimeId, Is.Empty);
Assert.That(restored.InvocationHash, Is.Empty);
Assert.That(restored.PresentationSurfaceVersion, Is.Zero);
});
}
@ -286,7 +351,11 @@ public sealed class EveSurfaceSerializationCompatibilityTests
Assert.Multiple(() =>
{
Assert.That(wireJson.RootElement.TryGetProperty("navigation", out _), Is.False);
Assert.That(wireJson.RootElement.TryGetProperty("invocationHash", out _), Is.False);
Assert.That(wireJson.RootElement.TryGetProperty("presentationSurfaceVersion", out _), Is.False);
Assert.That(restored.Navigation, Is.Null);
Assert.That(restored.InvocationHash, Is.Empty);
Assert.That(restored.PresentationSurfaceVersion, Is.Zero);
});
}

View file

@ -16,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj" />
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
</ItemGroup>
<ItemGroup>