From 89687fceb4d0af32e672738a18c352f1b3b744df Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 10 Sep 2026 13:45:07 +0200 Subject: [PATCH] Let the browser sample read the provider's advertisement Eve refuses to build a command intent for a surface that has not advertised a command boundary and receipt schema, and the intent it builds now carries the operation under operation.operationId. The browser sample did neither: the click was refused inside the page and the checkpoint saw only a 30 s timeout on the receipt. The C# provider now publishes its gamecult.eve.provider_advertisement.v1 document with the surface's worldInteraction, and the page leases that and hands it to the lowering as `provider`. The provider owns the declaration; the page reads it rather than restating it. The command sink reads intent.operation.operationId. The checkpoint now forwards the page's console and pageerror output. "CultMesh browser operation is required." took one run to find with it and had been a bare timeout without it. scripts/verify-eve-getting-started.ps1 -EveRoot ..\Eve (Eve main) -> Eve getting-started verification passed: clean .NET and TypeScript artifacts, DOM and headless convergence, real Chromium and C# clients, Odin discovery, authority-bound routes, receipts, persistence, and route replacement. Co-Authored-By: Claude Fable 5.1 --- samples/eve-browser-network/Program.cs | 45 ++++++++++++++++++++- samples/eve-browser-network/browser/main.ts | 13 +++++- scripts/verify-eve-browser-network.mjs | 4 ++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/samples/eve-browser-network/Program.cs b/samples/eve-browser-network/Program.cs index bfadd7db..68f72e05 100644 --- a/samples/eve-browser-network/Program.cs +++ b/samples/eve-browser-network/Program.cs @@ -14,6 +14,8 @@ using Microsoft.Extensions.DependencyInjection; const string counterKey = "counter:main"; const string surfaceKey = "sample.counter"; +const string providerKey = "sample.counter-ui"; +const string commandBoundary = "sample.counter.commands"; var arguments = Args.Parse(args); if (arguments.Mode == "provider") { @@ -89,7 +91,7 @@ static async Task RunOdinAsync(Args arguments) static async Task RunProviderAsync(Args arguments) { - var documentTypes = new[] { typeof(CounterState), typeof(EveSurfaceDocument) }; + var documentTypes = new[] { typeof(CounterState), typeof(EveSurfaceDocument), typeof(EveProviderAdvertisementDocument) }; var registry = CultMesh.CreateCultCacheDocumentRegistry(documentTypes); var documents = CultMesh.CreateCultNetDocumentRegistry(documentTypes, registry); using var node = await CultMesh.CreateNodeAsync(arguments.StatePath, new CultMeshNodeOptions @@ -111,6 +113,10 @@ static async Task RunProviderAsync(Args arguments) await node.Database.PutAsync(new CultRecordKey(counterKey), CounterState.Initial(counterKey)); if (node.Cache.Get(new CultRecordKey(surfaceKey)) == null) await node.Database.PutAsync(new CultRecordKey(surfaceKey), CreateCounterSurface()); + // The advertisement is how a lowering learns where this surface's commands + // go and what comes back. Eve refuses to build a command intent without it, + // and the browser must not have to restate it: the provider is the owner. + await node.Database.PutAsync(new CultRecordKey(providerKey), CreateProviderAdvertisement(arguments)); await node.FlushAsync(); await using var schemaServer = new CultNetWebSocketSchemaServer(); @@ -341,6 +347,43 @@ static void ForceCollection() GC.Collect(); } +static EveProviderAdvertisementDocument CreateProviderAdvertisement(Args arguments) +{ + var worldInteraction = new EveWorldInteractionAdvertisement( + projectionKind: "surface-state", + stateSchemas: ["sample.counter_state.v1"], + commandBoundary: commandBoundary, + commandRecordRef: "", + receiptSchema: EveCommandReceiptDocument.SchemaId, + receiptRecordRef: "", + assetManifestRecordRef: "", + loweringTargets: ["browser"], + ownership: arguments.AuthorityRuntimeId); + return new EveProviderAdvertisementDocument( + providerId: providerKey, + serviceId: arguments.VerseId, + verseId: arguments.VerseId, + title: "CultMesh browser counter", + kind: "sample.daemon", + cultMeshAddress: arguments.AuthorityRuntimeId, + updatedAtUtc: DateTime.UtcNow.ToString("O"), + freshness: new EveProviderFreshness("live", DateTime.UtcNow.ToString("O"), 0), + schemas: ["sample.counter_state.v1", EveSurfaceDocument.SchemaId, EveCommandReceiptDocument.SchemaId], + witnesses: [], + surfaces: + [ + new EveAdvertisedSurface( + surfaceKey, + EveSurfaceDocument.SchemaId, + surfaceKey, + "cultmesh", + "live", + "surface", + worldInteraction) + ], + commands: [new EveAdvertisedCommand("sample.counter.increment", surfaceKey, "cultmesh", "Increment")]); +} + static EveSurfaceDocument CreateCounterSurface() { var networkRoute = new CultMeshRouteHint(CultMeshLocalityKind.Network, "cultmesh"); diff --git a/samples/eve-browser-network/browser/main.ts b/samples/eve-browser-network/browser/main.ts index 8251a0a3..b04ee5c8 100644 --- a/samples/eve-browser-network/browser/main.ts +++ b/samples/eve-browser-network/browser/main.ts @@ -1,4 +1,4 @@ -import { parseEveCommandReceipt, parseEveSurfaceDocument } from "@gamecult/eve-contracts"; +import { parseEveCommandReceipt, parseEveProviderAdvertisement, parseEveSurfaceDocument } from "@gamecult/eve-contracts"; import { renderEveSurface } from "@gamecult/eve-browser-lowering"; import { CultMeshBrowserClient, @@ -52,14 +52,23 @@ try { recordKey: "sample.counter", subscriptionId: "browser-surface", }); + // The provider owns the declaration of where this surface's commands go and + // what comes back; the page reads it rather than restating it. + const advertisement = await mesh.leaseRawDocument({ + schemaId: "gamecult.eve.provider_advertisement.v1", + recordKey: "sample.counter-ui", + subscriptionId: "browser-provider", + }); + const provider = parseEveProviderAdvertisement(decodeCultNetPayload(advertisement.current!)); const currentSurface = parseEveSurfaceDocument(decodeCultNetPayload(surface.current!)); renderEveSurface(currentSurface, host, { activeSurfaceId: "sample.counter", clientId: "sample.chromium", + provider, commandSink: async intent => { const response = await mesh.invoke({ serviceId: "sample.counter", - operation: intent.command, + operation: intent.operation.operationId, payloadSchema: "sample.increment.v1", payload: { amount: 1 }, idempotencyKey: window.__sampleCommandId, diff --git a/scripts/verify-eve-browser-network.mjs b/scripts/verify-eve-browser-network.mjs index 8c865b1d..c1388712 100644 --- a/scripts/verify-eve-browser-network.mjs +++ b/scripts/verify-eve-browser-network.mjs @@ -107,6 +107,10 @@ try { browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true }); let page = await browser.newPage(); + // A command the lowering refuses is caught inside the page; without this + // the only symptom is a 30 s timeout on the receipt. + page.on("console", message => console.log(`[browser:${message.type()}] ${message.text()}`)); + page.on("pageerror", error => console.log(`[browser:pageerror] ${error.message}`)); const url = `http://127.0.0.1:${httpPort}/?odin=${encodeURIComponent(odinEndpoint)}&token=${token}`; await page.goto(url); await page.waitForFunction(() => window.__sampleReady || window.__sampleError);