Blender add-on: pass CultMeshGrantAllWrites() to its served node #4

Merged
libby merged 2 commits from libby/blender-grant-peer-writes into main 2026-11-07 19:19:41 +00:00
3 changed files with 30 additions and 0 deletions

View file

@ -345,6 +345,12 @@ class BrokkrBlenderTarget:
guard = SessionGuard() if require_token else None
if guard is not None:
server_class = guarded_server_class(server_class, guard)
# The add-on's own clients put command intents to this server, so it grants peer writes
# explicitly: cultmesh_py refuses them by default, and the session token above is what
# authenticates a peer when it is required. A cultmesh_py older than the gate has no
# CultMeshGrantAllWrites and already accepts every write.
grant_all = getattr(cultmesh, "CultMeshGrantAllWrites", None)
authority_args = {"write_authority": grant_all()} if grant_all is not None else {}
server = server_class(
node=node,
verse_catalog=verse_catalog,
@ -354,6 +360,7 @@ class BrokkrBlenderTarget:
display_name="Brokkr Blender Editor",
max_snapshot_documents=int(max_snapshot_documents),
max_snapshot_bytes=int(max_snapshot_bytes),
**authority_args,
).start()
endpoint = f"cultnet://{resolved_host}:{server.port}"

View file

@ -51,6 +51,12 @@ write, not only at connect. Reads stay open. `Stop Server`, quitting, or
disabling the add-on revokes it; it is held in memory only and never saved to
preferences or the cache.
Peer write scope: CultLib's `cultmesh_py` refuses peer writes unless an authority grants them, and the add-on
passes `CultMeshGrantAllWrites()`. A peer that gets past the session token (or any peer, when the token is off) can
therefore put or delete **any registered document** on the served node, not only command intents. That is acceptable
on loopback, the default; a grant scoped to command intents is future work. Do not combine `Allow Non-Loopback Bind`
with the token off.
Run the tests (no Blender needed; `CULTLIB_PACKAGES` adds the real-server tests):
```text

View file

@ -261,6 +261,23 @@ class RealServerTests(unittest.TestCase):
response = server._handle_connection_message(message, {})
self.assertIn("malformed", response[0].get("code", "")) # past the guard, refused by the store as an empty put
def test_a_peer_put_is_accepted_over_tcp_because_the_target_grants_peer_writes(self):
status = self.start()
cultmesh, _ = bt._load_cultmesh(PACKAGES)
import cultnet_py
document = self.target._documents["command_intent"]
client = cultmesh.CultMesh.create_client("127.0.0.1", status["port"], timeout_seconds=3.0)
with client.subscribe_database(subscription_id="peer", schema_ids=[bt.COMMAND_INTENT_SCHEMA],
include_snapshot=False) as subscription:
subscription.send(cultnet_py.document_put_raw(
message_id="peer-put", key="blender/commands/peer", schema_id=document.catalog_entry().schema_id,
stored_at="2026-11-07T00:00:00Z",
payload=document.encode_payload({
"schema": bt.COMMAND_INTENT_SCHEMA, "commandId": "peer", "action": "createObject"}),
))
change = subscription.read_next_change() # a refusal would raise CultNetPeerError instead
self.assertEqual(change.record_key, "blender/commands/peer")
def queue_intent(self, key="blender/commands/q1"):
node, documents = self.target._open_node(self.cache, PACKAGES)
node.database.put(documents["command_intent"], key, {