Server protocol
This page is for authors of server adapters. It states what a Gramlot server exposes to the browser runtime: the bootstrap, the request and response envelopes, the close message, the outcomes and, over HTTP, the URLs, methods and status codes. Page authors do not need it: Writing pages and Calling the server describe the same behaviour from the page.
The core implements every rule that does not depend on the transport, in Python and
JavaScript (GramlotServer, GramlotFileServer, see
Classes and server adapters). An adapter adds
only what its transport needs. The conformance check of §150 verifies an adapter
over HTTP. The section numbers of this page are the rule numbers the conformance
check prints (§150).
Part A · The contract
Scope and terms
The contract has one logical form. It is realised over HTTP (Part B), inside a Web Worker (Part C) and, for a server that offers it, over WebSocket (Part D).
GramlotServeropens pages, keeps the page registry, builds the Source fragments and runs the endpoints. It has no transport. An adapter connects it to a transport and owns what the transport adds: URLs, bodies, status codes, cookies, headers, Worker messages.A page is a server page class (
Page) opened for one browser tab. Opening it answers the bootstrap (§010).A fragment is a piece of Source built on the server. The whole page is the fragment
main, mounted in the root element of the bootstrap. Every other fragment is mounted below a node of the Source in the browser.An endpoint is a server method the browser calls for a value.
Every call names a content type,
sourcefor a fragment ordatafor a value (§015). Every message is an envelope (§020). The same envelope travels over HTTP,postMessageand WebSocket.The core gives every adapter the operations of this contract:
GramlotServer.decode_body,call,close,open_page,close_allandtheme_file(JavaScriptdecodeBody,call,close,openPageorregisterPage,closeAll,themeFile) and, for a pages folder,GramlotFileServer.companion_file(JavaScriptcompanionFile).The server adapter of a page is the browser object that holds the server-dependent behaviour,
gramlot.serverAdapter, an instance ofServerAdapter(§045). The server chooses its class in the bootstrap (§010). The core consults it; page authors do not call it.
Page lifecycle and bootstrap
open_page(path, *, owner=None, prefix="")(JavaScriptopenPage(path, {owner, prefix})) registers the page with apageIdand an owner (§135) and returns the bootstrap: the page ID, the HTML document and its nonce.The HTML document has a
<title>fromPage.title, an import map that maps@gramlot/gramlot/pageto the runtime URL, the rootdiv(gramlot-rootby default) and a module script that importsPageBootstrapand runs it with{config, resources}. The import map and the module script carry the same nonce.configis{pageId, rpcUrl, closeUrl, rootId, capabilities, nameSyntax}, plusdevices,localeandtimeZonewhen the page declares them.rpcUrlreceives every request envelope andcloseUrlthe close message.capabilitieslists the optional features the server offers (§040).nameSyntaxis the syntax of a method name the server accepts (§015), written{kind: "regex", pattern, flags}.resourcesis{css: [url], js: [{url, group}]}, pluscollections(ordered verified records withname,url, andbuildId) andserverAdapterwhen the server has them.cssstarts with the theme of the page: the base theme of the server (optioncss_theme, JavaScriptcssTheme, defaultgramlot-base,False/falsefor none), unless the page sets its owncss_theme(a theme name, orFalse/falsefor none). The CSS ofPage.cssand the companion stylesheet follow. Each URL appears once, in its last position.serverAdapteris the URL of a module whose default export is theServerAdapterclass of the page (GramlotServer.server_adapter, JavaScriptserverAdapter; none by default). The module imports the core from@gramlot/gramlot/page, which the import map maps to the runtime already loaded. Without it the page uses the HTTPServerAdapterof the runtime (§045).As on a static host,
<path>/index.htmlopens the page<path>/and/index.htmlopens the index page.A page that does not exist raises
PageNotFound. A full registry raisesServerCapacity.Every call names its
pageId. A call for a page of another owner, or for an unknown or expired page, answers the outcomepage_expired(§025). Page IDs are not login credentials.A page is closed by the close message (§020) or by
close_all()/closeAll()when the adapter shuts down.A page expires
page_ttlseconds (JavaScriptpageTtl, default 1800) after its last use. Opening the page sets its expiry to now pluspage_ttl. Every call that names a page of its owner that has not expired moves the expiry to now pluspage_ttlagain, whatever the outcome of the method (not_found,not_authenticated,not_authorizedandapplication_errorincluded), because the page is in use. Nothing else renews a page: a call of another owner, of an unknown or of an expired page answerspage_expiredand changes nothing, and a request the server cannot decode, the close message and the requests that name no page renew nothing. A page that no call names forpage_ttlseconds is removed, and its next call answerspage_expired. The renewal is made byGramlotServer.call; an adapter does nothing for it.GramlotServercreates a newPageinstance for every call. The contract keeps no state between calls; a server may keep it as a capability (§040).
Prepared collection resources are inventoried at host startup. Python pages select
importable descriptors through Page.gramlots; the host includes dependencies
before dependants and makes only inventoried browser assets available. A collection
bootstrap entry is {name, url, buildId}, not a bare URL. Theme CSS precedes
collection CSS, followed by page CSS. Startup waits for stylesheets and modules,
and an identity/load failure prevents normal page startup. See the
complete collection lifecycle.
Content types and method names
|
The browser asks for |
The page declares |
The value |
Mounted |
|---|---|---|---|---|
|
a fragment: |
|
the fragment document: the TYTX JSON text of the |
in the root for |
|
a value |
|
one value as TYTX JSON: a scalar, a Bag or null; never code |
in Data, at the path the caller names |
A fragment method populates the root it receives and returns nothing. An endpoint returns its value.
Inline code runs only when it arrives inside a fragment. A
GramlotBuilderBaginside adatavalue is a value and activates nothing.mainis reserved to the whole page: it cannot be declared as a fragment or an endpoint. A declared method name matches^[A-Za-z][A-Za-z0-9_]*$in both languages. The same method cannot be both a fragment and an endpoint.The
nameof a request follows the syntax the server declares,GramlotServer.name_syntax(JavaScriptnameSyntax), aNameSyntax. It is a regex matched against the whole name, in the portable format of genro-builders ({kind: "regex", pattern, flags}): a construct that Python and JavaScript read differently (\w,(?P<…>), inline flags,IGNORECASE) is refused when the syntax is created. The bootstrap carries it to the browser, where theremotehelper and a name read from Data are checked before a request is sent. A name outside the syntax is not declared and answersnot_found.The core default,
NAME_SYNTAX, accepts a method of the page or a patha.b.c.methodread from the page: segments of an ASCII letter followed by ASCII letters, digits and_, separated by.. Every segment before the last is a public attribute (its name does not start with_); a property getter runs when a request names it. The last segment is a method declared with the marker of the content type on the class of the object before it; itsauthrule (§030) is the only authorization of the request. In JavaScript every object on the path is an instance ofExposed, so an inherited member such asconstructornever leads to a method. A subclass ofGramlotServermay declare a narrower syntax; a server whose syntax is not dotted resolves its names itself.Python methods receive
paramsas keyword arguments. JavaScript methods receive them as one object. The protocol does not check parameter types.
The envelope
request: {id, pageId, contentType, name, params}
response: {id, contentType, value}
{id, contentType, error: {code, name, message}}
close: {pageId}
idis a string the browser generates. The response carries it back; over a transport without ordering (WebSocket) it pairs request and response. A Source node has one request in flight at a time.pageIdis the page of §010.contentTypeissourceordata(§015). The response repeats it.nameis the fragment or endpoint name.mainis the whole page.paramsis an object of named values, encoded as TYTX JSON;{}when there are none.null, an array or a scalar is a transport error (§120) and never reaches the page. A string value carrying a TYTX suffix ("x::D") is decoded by TYTX, not passed through as text: endpoints must not rely on such strings.valueis the fragment document or the endpoint value (§015).erroris the outcome (§025).nameandmessageare those of the exception. WithGRAMLOT_DEV=DEBUGthe core addsdetails, the traceback.Extra fields are allowed on both sides. The base runtime ignores them; an extension may read them.
A message from the server without
idis reserved to push (§040). The base runtime ignores it.The close message is
{pageId}alone. The browser sends it without waiting for an answer; over HTTP it is a beacon onpagehide(§125).
Outcomes
|
Cause |
Core class |
|---|---|---|
|
the page declares no fragment or endpoint of that |
|
|
unknown, expired (no call for |
|
|
the target has an |
|
|
the target has an |
|
|
the method raised; |
any other exception, including a value TYTX cannot serialise |
An outcome is a normal response of the transport (HTTP 200). Status codes are for
transport failures only (§130). The class names are the API of GramlotServer; the
code is their form in the envelope.
auth
One attribute,
auth, protects Source elements and endpoints. Its value is a rule over tags with|,&and!, the syntax of the genro-routesAuthPlugin.Endpoint: an unsatisfied rule answers
not_authenticatedornot_authorized; the method does not run.Element: when the server serialises a fragment, an element whose rule is refused is left out with all its children and never reaches the browser. An accepted element is sent without its
authattribute. The Source the method built is not changed. Anauthvalue that is not a string answersapplication_error. A builder without an evaluator (the browser, authoring alone) keeps every element.The evaluator is a method of
GramlotServer:evaluate_auth(rule, *, owner, identity), JavaScriptevaluateAuth(rule, {owner, identity}). It answersNone/nullto allow,not_authenticatedornot_authorized; any other result answersapplication_error. Endpoints and elements go through the same check.identityis the identity the adapter passes with the call,call(text, owner=…, identity=…)(JavaScriptcall(text, {owner, identity})), for example the user of a login. The server keeps nothing of it between calls, so an adapter never stores a per-request identity on the shared server object.The base evaluator knows no identity: every rule answers
not_authenticated. A server with an identity system overrides it and announces theauthcapability (§040).authprotects what the server builds, not the page source. A JavaScript page module served to the browser for itsLogic(§145) is readable. A page that runs in a Worker (Part C) runs in the browser, whereauthprotects nothing.
Declaring fragments and endpoints
Python:
@sourceand@endpoint, bare or withauth="rule", on methods of thePagesubclass or of an object the page reaches (§015).JavaScript:
registerSource('name', {auth})andregisterEndpoint('name', {auth}), static methods ofExposedcalled after the class body;PageextendsExposed.mainis the methodPage.mainin both languages.
Required and optional behaviour
Every server, the Worker included, must:
open a page and answer the bootstrap with
capabilitiesandnameSyntax(§010);accept the envelope and answer the outcomes (§020, §025);
read a request body as UTF-8, with a leading byte order mark removed (§120);
build
mainand the declared fragments forsource(§015);run the declared endpoints for
data(§015);refuse every
authrule unless it evaluates identities (§030);close a page on the close message, and all pages on shutdown (§010);
pass the conformance check of its transport (§150, §205).
A server may offer more, and then names the capability in config.capabilities. The
base server offers none.
Capability |
Meaning |
Current adapters |
|---|---|---|
|
evaluates |
the |
|
the envelope over WebSocket (Part D) |
none |
|
messages without |
none |
|
the browser sends the changes of the |
none |
|
page state kept between calls |
none |
|
catalogues of fragments |
none |
The names without a current adapter are reserved for Genro, through genro-kajenn. The
base runtime only reads the list. A server may also reject a request body above a
size of its choice (§130) and read pages again in development (GRAMLOT_DEV).
Extension points
The base library is extended without modification through:
the extra fields of the envelope (§020);
the server adapter of the page,
gramlot.serverAdapter. The server names the module of its class in the bootstrap (resources.serverAdapter, §010) andPageBootstrapgives an instance toGramlot. The core class,ServerAdapter, realises HTTP. Its method iscreateTransport({rpcUrl, closeUrl, document}), which returns the transport ofgramlot.rpc;the transport of
gramlot.rpc. It has three required members:call(text, signal)sends a request envelope text and resolves with the response envelope text;close(pageId, {beacon})sends the close message;dispose()releases the transport. A page sendscloseand thendisposewhen it is disposed, and disposes itself on apagehidethat does not keep it in the back/forward cache (with a beacon over HTTP).HttpTransportholds no connection: itsdisposereleases nothing;a synchronous call, optional for a transport. A transport that offers it declares
supportsCallSynctrue and hascallSync(text), which returns the response envelope text and blocks the page until the answer arrives.gramlot.rpc.callSync(contentType, name, params)uses it, and adataRemotewithsyncreads through it. On a transport without it,callSyncthrows anddataRemotereads asynchronously.HttpTransportsends it with a synchronousXMLHttpRequest, which the browsers deprecate on the main thread; a Worker or a WebSocket transport cannot;the
authevaluator ofGramlotServer, with the identity of the call (§030);the name syntax of
GramlotServer(§015).
Part B · HTTP
The URLs are written without the mount prefix (§105).
Mount prefix
An adapter may serve Gramlot below a mount prefix, such as
/py. Every URL of this part is then below the prefix. The adapter removes the prefix before dispatching and answers 404 to paths outside it.The prefix without its final slash answers 301 to the prefix with the slash, with the query string kept, because pages link each other with relative URLs.
open_page/openPageadds the prefix once to the root-relative URLs of the bootstrap (/…, not//…). Relative and absolute URLs stay as written.The default bootstrap URLs are: runtime
/assets/gramlot.js, rpc/gramlot/rpc, close/gramlot/close. They are options ofGramlotServer(runtime_url,rpc_url,close_url; JavaScriptruntimeUrl,rpcUrl,closeUrl).
Runtime URL
GETandHEADof/assets/gramlot.jsanswer 200 with the media typetext/javascript. Any other method answers 405.The file is the one
runtime_asset()/runtimeAsset()returns: the readablegramlot.jswithGRAMLOT_DEV=DEBUG, the minifiedgramlot.min.jsotherwise. The URL is the same for both.
Page request
GETof a page path answers 200text/htmlwith the bootstrap document of §010.The nonce is new at each opening: 16 random bytes in URL-safe base64.
PageNotFoundanswers 404.ServerCapacityanswers 503.Any method other than
GETon a page path,HEADincluded, answers 405 with anAllowheader that listsGET.The adapter passes the path to
open_page, which maps<path>/index.htmlto<path>/(§010).
RPC request
/gramlot/rpcaccepts POST only (405 otherwise), withContent-Type: application/json(415 otherwise). The media type is compared without regard to case, surrounding whitespace and parameters such ascharset. An absent or empty type, or a lookalike such asapplication/jsonp, is refused.The adapter reads the body with
decode_body(data)/decodeBody(data): UTF-8, with a leading byte order mark (EF BB BF) removed, as RFC 8259 §8.1 allows. The Gramlot runtime never sends the mark; a body saved by an editor may carry it. Bytes that are not UTF-8 raiseInvalidRequest, and the adapter answers 400 without calling the server.The body is the request envelope of §020 as TYTX JSON text.
callraisesInvalidRequest, and the adapter answers 400, when the text cannot be decoded, is not an object, has anid,pageId,contentTypeornamethat is not a string, has acontentTypeother thansourceanddata, or hasparamsthat is not an object.The adapter passes the
ownerof the request (§135) with every call and, when it has an identity system, theidentity(§030).The response is 200
application/jsonwith the response envelope of §020, for values and outcomes alike.
Close request
/gramlot/closeaccepts POSTapplication/jsonwith{pageId}, with the transport errors of §120 and the body read withdecode_body. It answers 200application/json{"ok": true}.The adapter passes the text to
close(text, owner=…)(JavaScriptclose(text, {owner})), which removes the page when the requester owns it. A text that is not JSON, or whosepageIdis not a string, raisesInvalidRequest(400). An unknown or unownedpageIdanswers{"ok": true}and leaves every page open.The browser sends the close message as a beacon on
pagehide. The adapter reads the body by itsContent-Length, which a beacon sends.
Transport status codes
Status |
Cause |
Section |
|---|---|---|
301 |
the mount prefix without its final slash |
§105 |
400 |
a body that is not a valid envelope or close message |
§120, §125 |
404 |
|
§115, §105 |
405 |
a method the URL does not accept; on a page path with |
§110, §115, §120, §125 |
413 |
optional: a body above a size the adapter or the web server in front of it chooses; the protocol fixes no size |
adapter |
415 |
a body that is not |
§120, §125 |
503 |
|
§115 |
Every other failure is an outcome inside a 200 response (§025).
Owner
GramlotServerregisters each page with an owner and answerspage_expiredto every call of another owner; a close message of another owner leaves the page open. The core takes the owner with each operation (open_page,call,close); the way to recognise it belongs to the adapter.An adapter over HTTP must give each browser its own owner: a page opened by one browser is reachable from another only as
page_expired. A null owner for every request is not allowed over HTTP. The Worker (Part C) keeps the null owner, because the page and the Worker live in the same browser.An adapter that recognises the owner by a cookie names it
gramlot_owner, sets it on the page response withPath=<prefix or />; HttpOnly; SameSite=Lax, reuses the value the browser already sends and passes it asownerto every call. The value is random, for example 24 random bytes in URL-safe base64.The cookie has no lifetime: the attributes above include no
Max-Ageand noExpires, so it is a session cookie. Nothing renews it. The expiry of a page (§010) does not depend on it: a call whose cookie is gone has another owner and answerspage_expired.An adapter may take the owner from its own identity system instead, with the same isolation between browsers.
Content Security Policy
The policy belongs to the application. When an adapter is configured with one, it
sends it as the Content-Security-Policy header of each page response, with
{nonce} replaced by the nonce of that bootstrap, so the policy allows the two
bootstrap scripts (§115). The profiles are described in
Classes and server adapters.
Themes and companion files
GETandHEADof/themes/<path>answer the file of the core themes, with the media type of its extension. The file isGramlotServer.theme_file(path)(JavaScriptthemeFile),None/nullwhen the core has no such file inside its themes folder. EveryGramlotServerhas it, a subclass that does not read a pages folder included, because the bootstrap lists the theme of the page (§010). In JavaScript it needs Node.js or Bun: a Worker has no files. The adapter adds methods, media types and headers.GETandHEADof a.cssor.jsURL answer the file whose real path is inside the pages folder: page modules, companions such asfoo.cssandfoo_aux.js, andPage.cssfiles placed there. Any other file of the folder is not served. The file isGramlotFileServer.companion_file(path)(JavaScriptcompanionFile); a path with a NUL character names no file.A JavaScript page module whose
Logiclives in the module itself is served to the browser, so its source,authrules included, is readable; itsmainstill runs on the server. A page whose logic is infoo_aux.jsis not served. Python page modules are never served.Every root-relative URL of the bootstrap
resourcesanswers 200 withtext/css(CSS) ortext/javascript(JavaScript).
Conformance check
check_protocol(base_url, page_path) (Python, gramlot.server) and
checkProtocol(baseUrl, pagePath) (JavaScript, @gramlot/gramlot/server) run the
list below, in this order, over HTTP against a running adapter. base_url includes
the mount prefix. page_path names a page of the adapter that declares the fragments
check_fragment and check_fragment_auth (an element with an auth rule) and the
endpoints check_endpoint, check_endpoint_auth (with an auth rule) and
check_endpoint_raise, as the conformance fixture pages of the core do. The first
failure raises AssertionError (Python) or rejects with the AssertionError of
node:assert (JavaScript). Its message starts with the identifier of the rule; the
last number of the identifier is the section of this page. The optional argument
page_ttl (JavaScript option pageTtl) is the page_ttl in seconds of the server of
the adapter, a short one so that the check is fast; with it the check also runs item
18, which takes about 2.5 times page_ttl, and without it item 18 is left out.
§105: with a prefix,
GET <prefix>answers 301 to<prefix>/.§115:
GET <page>answers 200text/htmlwith the bootstrap module script; the import map carries its nonce;confighasrpcUrl,closeUrland acapabilitiesarray. §010:config.nameSyntaxis{kind: "regex", pattern, flags}.§105: the bootstrap URLs are the defaults below the prefix.
§140: a
Content-Security-Policyheader, when present, contains'nonce-<nonce>'.§135: a
gramlot_ownercookie, when set, hasHttpOnly,SameSite=Laxand the prefix path; later requests send it.§110:
GETof the runtime answers 200text/javascript;POSTanswers 405.§145: each root-relative bootstrap resource answers 200 with its media type.
§115: an unknown page answers 404;
POST,HEAD,PUT,DELETEandPATCHof<page>answer 405 with anAllowheader that listsGETand not the method; whenpage_pathends with/,<page>index.htmlanswers the page.§120 and §125:
rpcanswers 405 toGET;rpcandcloseanswer 415 totext/plain, to JSON lookalike media types and to an empty media type, and 400 to invalid UTF-8 inside otherwise valid JSON. Mixed-case JSON media types andcharsetparameters are accepted. Invalid close requests leave the page usable. A body with a leading byte order mark is accepted:rpcanswers the endpoint value,closeanswers{"ok": true}.rpcalso answers 400 to invalid JSON, to a body withoutpageId, toparams: null, to arrayparamsand to an unknowncontentType.§020:
source/mainanswers 200application/jsonwith an envelope whoseidandcontentTyperepeat the request and whosevalueis a string.§135: the same call without the cookies of the page answers
page_expired; with an owner cookie, the call with another owner value answerspage_expired; a close without the cookies of the page leaves the page usable.§025:
mainof an unknownpageIdanswerspage_expired; an unknown fragment and an unknown endpoint answernot_found;{contentType: 'data', name: 'main'}answersnot_found;constructor,constructor.prototype.check_endpointand (source)constructor.prototype.check_fragmentanswernot_found.§015:
source/check_fragmentwithparamsanswers a fragment document;data/check_endpointwithparamsanswers the value the fixture computes from them:3,"x"and a date, which comes back as the same typed value.§030:
data/check_endpoint_authanswersnot_authenticatedon an adapter without theauthcapability; on an adapter with it,not_authenticated,not_authorizedor a value.§030:
source/check_fragment_authanswers a fragment document; without theauthcapability the document does not contain the element with theauthrule.§025:
data/check_endpoint_raiseanswersapplication_errorwith the exceptionname.§125:
closeanswers 200{"ok": true};mainof the closed page then answerspage_expired.§010, only with
page_ttl: a page opened now answers a call at 0.5page_ttl, answers another one at 1.2page_ttlafter its opening (0.7page_ttlafter the previous call, so the previous call renewed it), and answerspage_expiredto a call 1.3page_ttlafter the previous one.
Not checked: the 503 of a full registry, HEAD of the runtime and of the files, and
the 413 of a server that chooses a size. Run the check against every adapter, with
and without a mount prefix and a Content Security Policy.
Part C · Worker
Messages
The serverless package @gramlot/gramlot-serverless of gramlot-js-server realises
the contract inside a dedicated Web Worker, one per tab. The Worker runs
GramlotWorkerServer, a GramlotServer; the window side is WorkerTransport. Every
message goes through postMessage.
Open: the window sends
{id, open: true}and receives{id, open}, whereopenholds the bootstrap data of §010 returned byregisterPage, withcapabilities: []andnameSyntax.Calls: the window sends
{id, text}, wheretextis the request envelope of §020, and receives{id, text}with the response envelope.A failed open, or an
InvalidRequest, answers{id, error: {name, message}}.Close: the close message
{pageId}, withoutid.The transport errors of §120 do not exist: the window is the only sender.
WorkerTransporthas no synchronous call (§045): adataRemotewithsyncreads asynchronously.The owner is null; the owner check stays and always matches.
authprotects nothing: the page runs in the browser (§030).The serverless package runs the envelope part of the conformance list (items 10 and 12–16 of §150) over
postMessage.
Part D · WebSocket
Messages
A server with the websocket capability accepts each request envelope as one text
message and answers each with its response envelope. Responses may arrive in any
order; id pairs them. A message without id from the server is a push (push
capability). The close message is {pageId} without id; closing the socket also
closes the page. No current adapter offers this capability, and the base runtime has
no WebSocket transport.
Part E · Application metadata
Root web-app manifest
GET <prefix>/app.webmanifest answers the result of the root page’s manifest
method, only when its installable property is true, as a JSON object with the media
type application/manifest+json. HEAD has the same status and headers and no body.
Other methods answer 405. A missing or not installable root answers 404. No page is
registered, and no envelope or endpoint is involved. The result is public metadata.
page_manifest(prefix=...)/pageManifest({prefix}) are the server operations; the
adapter maps HTTP requests and responses. The bootstrap of the root page adds the
manifest link; other pages do not. An application mounted at /demo uses /demo/
for the default scope, id and start URL. The page side is described in
Writing pages.
Device suitability
Page.devices is a Python tuple or a JavaScript array of desktop, tablet and
phone. An empty value declares nothing. A non-empty value reaches the browser as a
copy in config.devices (HTTP) or in the result of registerPage (Worker), and the
runtime exposes it frozen as gramlot.devices. It is information only: not a
permission, a redirect rule or a layout change. Adapters do not interpret it.
Optional initial Data companion
The successful source/main response retains id, contentType and value (the existing Source representation). If the page has a same-name _data.tytx companion, the envelope additionally carries data, a TYTX-encoded Bag. Missing companions omit that field. Errors carry neither value nor data. Remote Source responses and endpoint values are unchanged.
GramlotServer.resolveData(path) / resolve_data(path) supplies this optional Bag; the neutral server returns null/None, the file server reads the companion, and the Worker reads its embedded serialized copy. The registered page path is used only after normal owner/expiry validation. RpcHandler.call keeps returning the value; callEnvelope exposes the checked envelope to startup. Startup validates Source and Data before installing the companion into the existing Builder-owned Data root, then installs main normally. A Data installation is not rolled back after a later Source installation error, consistent with existing startup rules.
This adds an initial-Data mechanism without replacing dataSetter, defaults, endpoints or resolvers. The initial main result is not replaced by a {source, data} value.