Repository: vertexproject/synapse

Issue:
[BUG] Synapse Installation is broken due broken dependency lmdb
**Describe the bug**
When installed synapse via pip or from the reposistory the installation is broken on Python 3.13.1 (more specifications below) as a dependency `lmdb`. The lmdb fails to build with the following error.

```
lmdb/cpython.c: In function ‘val_from_buffer’:
      lmdb/cpython.c:586:12: error: implicit declaration of function ‘PyObject_AsReadBuffer’; did you mean ‘PyObject_GetBuffer’? [-Wimplicit-function-declaration]
        586 |     return PyObject_AsReadBuffer(buf,
            |            ^~~~~~~~~~~~~~~~~~~~~
            |            PyObject_GetBuffer
      error: command '/usr/bin/gcc' failed with exit code 1
      [end of output]

  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for lmdb
Failed to build lmdb
```

The issue it easy to fix. Bump the lmdb version to `lmdb_1.6.2` . I have tested it and worked fine after bumping the version.
I can create a PR if its alright :)

**To Reproduce**
```
pip install synapse
```

**Expected behavior**
The reposistory should be installed without any build error

**Environment (please complete the following information):**
 - OS: Linux  `Linux 6.6.72-1-lts #1 SMP PREEMPT_DYNAMIC Fri, 17 Jan 2025 14:04:26 +0000 x86_64 GNU/Linux`
 - Distribution: Arch Linux
 - Synapse Version: Latest
 - Python Version: Python 3.13.1
 - Output of `python -m pip freeze`

```
aiohappyeyeballs==2.4.4
aiohttp==3.11.11
aiohttp_socks==0.10.1
aioimaplib==1.1.0
aiosignal==1.3.2
aiosmtplib==3.0.2
antlr4-python3-runtime==4.9.3
arrow==1.3.0
attrs==25.1.0
base58==2.1.1
beautifulsoup4==4.12.3
bech32==1.2.0
cbor2==5.6.5
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
colorama==0.4.6
cpe==1.3.1
cryptography==43.0.3
fastjsonschema==2.19.1
fqdn==1.5.1
frozenlist==1.5.0
html5lib==1.1
idna==3.7
isoduration==20.11.0
jsonpointer==3.0.0
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
lark==1.2.2
more-itertools==10.6.0
msgpack==1.1.0
multidict==6.1.0
oauthlib==3.2.2
packaging==24.2
prompt_toolkit==3.0.50
propcache==0.2.1
pycparser==2.22
pycryptodome==3.21.0
Pygments==2.17.2
pyOpenSSL==24.2.1
python-bitcoinlib==0.12.2
python-dateutil==2.9.0.post0
python-socks==2.7.0
pytz==2023.4
PyYAML==6.0.2
referencing==0.36.2
regex==2024.11.6
requests==2.32.3
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.22.3
scalecodec==1.2.11
simplejson==3.19.3
six==1.17.0
soupsieve==2.6
stix2-patterns==2.0.0
stix2-validator==3.2.0
tornado==6.4.2
types-python-dateutil==2.9.0.20241206
typing_extensions==4.12.2
uri-template==1.3.0
urllib3==2.3.0
vcrpy==5.1.0
wcwidth==0.2.13
webcolors==24.11.1
webencodings==0.5.1
wrapt==1.17.2
xxhash==3.5.0
yarl==1.18.3
```

**Additional context**

Discussion:
@invisig0th: Thanks! We'll get the dep updated! 👍 

Solution patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 1650bb7f238..4d8de060643 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 2.198.0
+current_version = 2.199.0
 commit = True
 tag = True
 tag_message = 
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a486e652872..d10f2e5a200 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,23 @@
 Synapse Changelog
 *****************
 
+v2.199.0 - 2025-02-18
+=====================
+
+Features and Enhancements
+-------------------------
+- Updated property set behavior to avoid warning when deprecated properties are
+  set as subs.
+  (`#4143 <https://github.com/vertexproject/synapse/pull/4143>`_)
+- Increased performance of the Storm ``merge`` command and ``View.merge()``
+  method.
+  (`#4144 <https://github.com/vertexproject/synapse/pull/4144>`_)
+
+Bugfixes
+--------
+- Fixed an issue with Snap construction in the Storm ``merge`` command.
+  (`#4144 <https://github.com/vertexproject/synapse/pull/4144>`_)
+
 v2.198.0 - 2025-02-14
 =====================
 
diff --git a/changes/3e33fa492b7dcb6d97b2b836a8cbadee.yaml b/changes/3e33fa492b7dcb6d97b2b836a8cbadee.yaml
new file mode 100644
index 00000000000..f10473f6f05
--- /dev/null
+++ b/changes/3e33fa492b7dcb6d97b2b836a8cbadee.yaml
@@ -0,0 +1,5 @@
+---
+desc: Update the ``lmdb`` library constraints to ``>=1.2.1,<1.7.0``.
+prs: []
+type: feat
+...
diff --git a/changes/7354b1b174a3b577778dd44d4b9b82e2.yaml b/changes/7354b1b174a3b577778dd44d4b9b82e2.yaml
new file mode 100644
index 00000000000..d648ec64f42
--- /dev/null
+++ b/changes/7354b1b174a3b577778dd44d4b9b82e2.yaml
@@ -0,0 +1,5 @@
+---
+desc: Removed ``hiveauth.py`` and related APIs which depended on it.
+prs: []
+type: deprecation
+...
diff --git a/pyproject.toml b/pyproject.toml
index 5713cec8c1a..9987141b34b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
 
 [project]
 name = 'synapse'
-version = '2.198.0'
+version = '2.199.0'
 authors = [
     { name = 'The Vertex Project LLC', email = 'root@vertex.link'},
 ]
@@ -27,7 +27,7 @@ dependencies = [
     'cryptography>=43.0.1,<44.0.0',
     'msgpack>=1.0.5,<1.2.0',
     'xxhash>=1.4.4,<3.6.0',
-    'lmdb>=1.2.1,<1.5.0',
+    'lmdb>=1.2.1,<1.7.0',
     'tornado>=6.2.0,<7.0.0',
     'regex>=2022.9.11',
     'PyYAML>=5.4,<6.1.0',
diff --git a/requirements.txt b/requirements.txt
index 6211ef28e0c..f55d15e6da8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 pyOpenSSL>=24.0.0,<24.3.0
 msgpack>=1.0.5,<1.2.0
 xxhash>=1.4.4,<3.6.0
-lmdb>=1.2.1,<1.5.0
+lmdb>=1.2.1,<1.7.0
 tornado>=6.2.0,<7.0.0
 regex>=2022.9.11
 PyYAML>=5.4,<6.1.0
diff --git a/synapse/cortex.py b/synapse/cortex.py
index 75756f2df0e..5b7f30a796c 100644
--- a/synapse/cortex.py
+++ b/synapse/cortex.py
@@ -20,7 +20,6 @@
 import synapse.lib.cell as s_cell
 import synapse.lib.chop as s_chop
 import synapse.lib.coro as s_coro
-import synapse.lib.hive as s_hive
 import synapse.lib.view as s_view
 import synapse.lib.cache as s_cache
 import synapse.lib.const as s_const
@@ -855,7 +854,6 @@ class Cortex(s_oauth.OAuthMixin, s_cell.Cell):  # type: ignore
     cellapi = CoreApi
     viewapi = s_view.ViewApi
     layerapi = s_layer.LayerApi
-    hiveapi = s_hive.HiveApi
 
     viewctor = s_view.View.anit
     layrctor = s_layer.Layer.anit
@@ -4656,10 +4654,6 @@ async def getCellApi(self, link, user, path):
         if not path:
             return await self.cellapi.anit(self, link, user)
 
-        if path[0] == 'hive' and user.isAdmin():
-            s_common.deprecated('Cortex /hive telepath path', curv='2.198.0', eolv='2.199.0')
-            return await self.hiveapi.anit(self.hive, user)
-
         if path[0] == 'layer':
 
             if len(path) == 1:
diff --git a/synapse/lib/hive.py b/synapse/lib/hive.py
index aba00b0c8ec..2fdce1b330d 100644
--- a/synapse/lib/hive.py
+++ b/synapse/lib/hive.py
@@ -86,7 +86,7 @@ def __iter__(self):
         for name, node in self.kids.items():
             yield name, node
 
-class Hive(s_nexus.Pusher, s_telepath.Aware):
+class Hive(s_nexus.Pusher):
     '''
     An optionally persistent atomically accessed tree which implements
     primitives for use in making distributed/clustered services.
@@ -95,8 +95,6 @@ async def __anit__(self, conf=None, nexsroot=None, cell=None):
 
         await s_nexus.Pusher.__anit__(self, 'hive', nexsroot=nexsroot)
 
-        s_telepath.Aware.__init__(self)
-
         if conf is None:
             conf = {}
 
@@ -161,24 +159,6 @@ async def _loadHiveNode(self, node, tree, trim=False):
             for cullname in culls:
                 await node.pop((cullname,))
 
-    async def getHiveAuth(self):
-        '''
-        Retrieve a HiveAuth for hive standalone or non-cell uses.
-
-        Note:
-            This is for the hive's own auth, or for non-cell auth.  It isn't the same auth as for a cell
-        '''
-        import synapse.lib.hiveauth as s_hiveauth
-        if self.auth is None:
-
-            path = tuple(self.conf.get('auth:path').split('/'))
-
-            node = await self.open(path)
-            self.auth = await s_hiveauth.Auth.anit(node, nexsroot=self.nexsroot)
-            self.onfini(self.auth.fini)
-
-        return self.auth
-
     async def _onHiveFini(self):
         await self.root.fini()
 
@@ -423,27 +403,6 @@ async def _popHiveNode(self, node):
 
         return node.valu
 
-    async def getTeleApi(self, link, mesg, path):
-        s_common.deprecated('Hive.getTeleApi', curv='2.198.0', eolv='2.199.0')
-        auth = await self.getHiveAuth()
-
-        if not self.conf.get('auth:en'):
-            user = await auth.getUserByName('root')
-            return await HiveApi.anit(self, user)
-
-        name, info = mesg[1].get('auth')
-
-        user = await auth.getUserByName(name)
-        if user is None:
-            raise s_exc.NoSuchUser(name=name)
-
-        # passwd None always fails...
-        passwd = info.get('passwd')
-        if not await user.tryPasswd(passwd):
-            raise s_exc.AuthDeny(mesg='Invalid password', user=user.iden, username=user.name)
-
-        return await HiveApi.anit(self, user)
-
     async def _storLoadHive(self):
         pass
 
@@ -480,224 +439,6 @@ async def storNodeDele(self, full):
         lkey = '\x00'.join(full).encode('utf8')
         self.slab.pop(lkey, db=self.db)
 
-class HiveApi(s_base.Base):
-
-    async def __anit__(self, hive, user):
-
-        await s_base.Base.__anit__(self)
-
-        self.hive = hive
-        self.user = user
-
-        self.msgq = asyncio.Queue(maxsize=10000)
-
-        self.onfini(self._onHapiFini)
-
-    async def loadHiveTree(self, tree, path=(), trim=False):
-        s_common.deprecated('HiveApi.loadHiveTree', curv='2.167.0')
-        return await self.hive.loadHiveTree(tree, path=path, trim=trim)
-
-    async def saveHiveTree(self, path=()):
-        s_common.deprecated('HiveApi.saveHiveTree', curv='2.167.0')
-        return await self.hive.saveHiveTree(path=path)
-
-    async def treeAndSync(self, path, iden):
-        s_common.deprecated('HiveApi.treeAndSync', curv='2.167.0')
-
-        node = await self.hive.open(path)
-
-        # register handlers...
-        node.on('hive:add', self._onHiveEdit, base=self)
-        node.on('hive:set', self._onHiveEdit, base=self)
-        node.on('hive:pop', self._onHiveEdit, base=self)
-
-        # serialize the subtree into a message and return
-        # via the mesg queue so there is no get/update race
-        root = (node.valu, {})
-
-        todo = collections.deque([(node, root)])
-
-        # breadth first generator
-        while todo:
-
-            node, pode = todo.popleft()
-
-            for name, kidn in node.kids.items():
-
-                kidp = (kidn.valu, {})
-                pode[1][name] = kidp
-
-                todo.append((kidn, kidp))
-
-        await self.msgq.put(('hive:tree', {'path': path, 'tree': root}))
-        await self.msgq.put(('hive:sync', {'iden': iden}))
-        return
-
-    async def setAndSync(self, path, valu, iden, nexs=False):
-        s_common.deprecated('HiveApi.setAndSync', curv='2.167.0')
-
-        valu = await self.hive.set(path, valu, nexs=nexs)
-        await self.msgq.put(('hive:sync', {'iden': iden}))
-        return valu
-
-    async def addAndSync(self, path, valu, iden):
-        s_common.deprecated('HiveApi.addAndSync', curv='2.167.0')
-
-        valu = await self.hive.add(path, valu)
-        await self.msgq.put(('hive:sync', {'iden': iden}))
-        return valu
-
-    async def popAndSync(self, path, iden, nexs=False):
-        s_common.deprecated('HiveApi.popAndSync', curv='2.167.0')
-
-        valu = await self.hive.pop(path, nexs=nexs)
-        await self.msgq.put(('hive:sync', {'iden': iden}))
-        return valu
-
-    async def _onHapiFini(self):
-        await self.msgq.put(None)
-
-    async def _onHiveEdit(self, mesg):
-        self.msgq.put_nowait(mesg)
-
-    async def get(self, full):
-        s_common.deprecated('HiveApi.get', curv='2.167.0')
-        return await self.hive.get(full)
-
-    async def edits(self):
-        s_common.deprecated('HiveApi.edits', curv='2.167.0')
-
-        while not self.isfini:
-
-            item = await self.msgq.get()
-            if item is None:
-                return
-
-            yield item
-
-class TeleHive(Hive):
-    '''
-    A Hive that acts as a consistent read cache for a telepath proxy Hive
-    '''
-
-    async def __anit__(self, proxy):
-
-        self.proxy = proxy
-
-        await Hive.__anit__(self)
-
-        self.lock = asyncio.Lock()
-
-        self.syncevents = {}  # iden: asyncio.Event()
-
-        # fire a task to sync the sections of the tree we open
-        self.schedCoro(self._runHiveLoop())
-
-        self.mesgbus = await s_base.Base.anit()
-        self.mesgbus.on('hive:set', self._onHiveSet)
-        self.mesgbus.on('hive:pop', self._onHivePop)
-        self.mesgbus.on('hive:tree', self._onHiveTree)
-        self.mesgbus.on('hive:sync', self._onHiveSync)
-
-        self.onfini(self.mesgbus.fini)
-
-        self.onfini(proxy.fini)
-
-    async def _onHiveSync(self, mesg):
-
-        iden = mesg[1].get('iden')
-        evnt = self.syncevents.pop(iden, None)
-        if evnt is None:
-            return
-
-        evnt.set()
-
-    def _getSyncIden(self):
-        iden = s_common.guid()
-        evnt = asyncio.Event()
-        self.syncevents[iden] = evnt
-        return iden, evnt
-
-    async def _runHiveLoop(self):
-        while not self.isfini:
-            async for mesg in self.proxy.edits():
-                await self.mesgbus.dist(mesg)
-
-    async def _onHiveSet(self, mesg):
-        path = mesg[1].get('path')
-        valu = mesg[1].get('valu')
-        await Hive.set(self, path, valu)
-
-    async def _onHivePop(self, mesg):
-        path = mesg[1].get('path')
-        await Hive.pop(self, path)
-
-    async def _onHiveTree(self, mesg):
-
-        # get an entire tree update at once
-        path = mesg[1].get('path')
-        tree = mesg[1].get('tree')
-
-        node = await Hive.open(self, path)
-
-        todo = collections.deque([(node, path, tree)])
-
-        while todo:
-
-            node, path, (valu, kids) = todo.popleft()
-
-            # do *not* go through the set() API
-            node.valu = valu
-            for name, kidt in kids.items():
-
-                kidp = path + (name,)
-                kidn = await Hive.open(self, kidp)
-
-                todo.append((kidn, kidp, kidt))
-
-    async def set(self, path, valu, nexs=False):
-        iden, evnt = self._getSyncIden()
-        valu = await self.proxy.setAndSync(path, valu, iden, nexs=nexs)
-        await evnt.wait()
-        return valu
-
-    async def add(self, path, valu):
-        iden, evnt = self._getSyncIden()
-        valu = await self.proxy.addAndSync(path, valu, iden)
-        await evnt.wait()
-        return valu
-
-    async def pop(self, path, nexs=False):
-        iden, evnt = self._getSyncIden()
-        valu = await self.proxy.popAndSync(path, iden, nexs=nexs)
-        await evnt.wait()
-        return valu
-
-    async def get(self, path):
-        return await self.proxy.get(path)
-
-    async def open(self, path):
-
-        # try once pre-lock for speed
-        node = self.nodes.get(path)
-        if node is not None:
-            return node
-
-        async with self.lock:
-
-            # try again with lock to avoid race
-            node = self.nodes.get(path)
-            if node is not None:
-                return node
-
-            iden, evnt = self._getSyncIden()
-
-            await self.proxy.treeAndSync(path, iden)
-
-            await evnt.wait()
-
-            return self.nodes.get(path)
-
 class HiveDict(s_base.Base):
     '''
     '''
@@ -758,11 +499,6 @@ def iterpath(path):
     for i in range(len(path)):
         yield path[:i + 1]
 
-async def openurl(url, **opts):
-    s_common.deprecated('synapse.lib.hive.openurl()', curv='2.198.0', eolv='2.199.0')
-    prox = await s_telepath.openurl(url, **opts)
-    return await TeleHive.anit(prox)
-
 async def opendir(dirn, conf=None):
     slab = await s_slab.Slab.anit(dirn, map_size=s_const.gibibyte)
     db = slab.initdb('hive')
diff --git a/synapse/lib/hiveauth.py b/synapse/lib/hiveauth.py
deleted file mode 100644
index ed33623c3bc..00000000000
--- a/synapse/lib/hiveauth.py
+++ /dev/null
@@ -1,1336 +0,0 @@
-# pragma: no cover
-
-###
-### THIS WHOLE MODULE IS DEPRECATED AND EXPECTED TO BE REMOVED O/A v2.198.0
-###
-
-import logging
-import dataclasses
-
-from typing import Optional, Union
-
-import synapse.exc as s_exc
-import synapse.common as s_common
-
-import synapse.lib.base as s_base
-import synapse.lib.cache as s_cache
-import synapse.lib.nexus as s_nexus
-import synapse.lib.config as s_config
-
-import synapse.lib.crypto.passwd as s_passwd
-
-logger = logging.getLogger(__name__)
-
-reqValidRules = s_config.getJsValidator({
-    'type': 'array',
-    'items': {
-        'type': 'array',
-        'items': [
-            {'type': 'boolean'},
-            {'type': 'array', 'items': {'type': 'string'}},
-        ],
-        'minItems': 2,
-        'maxItems': 2,
-    }
-})
-
-def getShadow(passwd):  # pragma: no cover
-    '''This API is deprecated.'''
-    s_common.deprecated('hiveauth.getShadow()', curv='2.110.0', eolv='2.199.0')
-    salt = s_common.guid()
-    hashed = s_common.guid((salt, passwd))
-    return (salt, hashed)
-
-def textFromRule(rule):
-    s_common.deprecated('hiveauth.textFromRule()', curv='2.198.0', eolv='2.199.0') # pragma: no cover
-    text = '.'.join(rule[1])
-    if not rule[0]:
-        text = '!' + text
-    return text
-
-@dataclasses.dataclass(slots=True)
-class _allowedReason:
-    s_common.deprecated('hiveauth._allowedReason()', curv='2.198.0', eolv='2.199.0')
-    value: Union[bool | None]
-    default: bool = False
-    isadmin: bool = False
-    islocked: bool = False
-    gateiden: Union[str | None] = None
-    roleiden: Union[str | None] = None
-    rolename: Union[str | None] = None
-    rule: tuple = ()
-
-    @property
-    def mesg(self):
-        if self.islocked:
-            return 'The user is locked.'
-        if self.default:
-            return 'No matching rule found.'
-
-        if self.isadmin:
-            if self.gateiden:
-                return f'The user is an admin of auth gate {self.gateiden}.'
-            return 'The user is a global admin.'
-
-        if self.rule:
-            rt = textFromRule((self.value, self.rule))
-            if self.gateiden:
-                if self.roleiden:
-                    m = f'Matched role rule ({rt}) for role {self.rolename} on gate {self.gateiden}.'
-                else:
-                    m = f'Matched user rule ({rt}) on gate {self.gateiden}.'
-            else:
-                if self.roleiden:
-                    m = f'Matched role rule ({rt}) for role {self.rolename}.'
-                else:
-                    m = f'Matched user rule ({rt}).'
-            return m
-
-        return 'No matching rule found.'
-
-class Auth(s_nexus.Pusher):
-    '''
-    Auth is a user authentication and authorization stored in a Hive.  Users
-    correspond to separate logins with different passwords and potentially
-    different privileges.
-
-    Users are assigned "rules".  These rules are evaluated in order until a rule
-    matches.  Each rule is a tuple of boolean, and a rule path (a sequence of
-    strings).  Rules that are prefixes of a privilege match, i.e.  a rule
-    ('foo',) will match ('foo', 'bar').
-
-    Roles are just collections of rules.  When a user is "granted" a role those
-    rules are assigned to that user.  Unlike in an RBAC system, users don't
-    explicitly assume a role; they are merely a convenience mechanism to easily
-    assign the same rules to multiple users.
-
-    Authgates are objects that manage their own authorization.  Each
-    AuthGate has roles and users subkeys which contain rules specific to that
-    user or role for that AuthGate.  The roles and users of an AuthGate,
-    called GateRole and GateUser respectively, contain the iden of a role or
-    user defined prior and rules specific to that role or user; they do not
-    duplicate the metadata of the role or user.
-
-    Node layout::
-
-        Auth root (passed into constructor)
-        ├ roles
-        │   ├ <role iden 1>
-        │   ├ ...
-        │   └ last role
-        ├ users
-        │   ├ <user iden 1>
-        │   ├ ...
-        │   └ last user
-        └ authgates
-            ├ <iden 1>
-            │   ├ roles
-            │   │   ├ <role iden 1>
-            │   │   ├ ...
-            │   │   └ last role
-            │   └ users
-            │       ├ <user iden 1>
-            │       ├ ...
-            │       └ last user
-            ├ <iden 2>
-            │   ├ ...
-            └ ... last authgate
-
-    '''
-
-    async def __anit__(self, node, nexsroot=None, seed=None, maxusers=0):
-        '''
-        Args:
-            node (HiveNode): The root of the persistent storage for auth
-        '''
-        s_common.deprecated('Auth.__anit__()', curv='2.198.0', eolv='2.199.0')
-        # Derive an iden from the parent
-        iden = 'auth:' + ':'.join(node.full)
-        await s_nexus.Pusher.__anit__(self, iden, nexsroot=nexsroot)
-
-        self.node = node
-
-        if seed is None:
-            seed = s_common.guid()
-
-        self.maxusers = maxusers
-
-        self.usersbyiden = {}
-        self.rolesbyiden = {}
-        self.usersbyname = {}
-        self.rolesbyname = {}
-        self.authgates = {}
-
-        self.allrole = None
-        self.rootuser = None
-
-        roles = await self.node.open(('roles',))
-        for _, node in roles:
-            await self._addRoleNode(node)
-
-        users = await self.node.open(('users',))
-        for _, node in users:
-            await self._addUserNode(node)
-
-        authgates = await self.node.open(('authgates',))
-        for _, node in authgates:
-            try:
-                await self._addAuthGate(node)
-            except Exception:  # pragma: no cover
-                logger.exception('Failure loading AuthGate')
-
-        self.allrole = await self.getRoleByName('all')
-        if self.allrole is None:
-            # initialize the role of which all users are a member
-            guid = s_common.guid((seed, 'auth', 'role', 'all'))
-            await self._addRole(guid, 'all')
-            self.allrole = self.role(guid)
-
-        # initialize an admin user named root
-        self.rootuser = await self.getUserByName('root')
-        if self.rootuser is None:
-            guid = s_common.guid((seed, 'auth', 'user', 'root'))
-            await self._addUser(guid, 'root')
-            self.rootuser = self.user(guid)
-
-        await self.rootuser.setAdmin(True, logged=False)
-        await self.rootuser.setLocked(False, logged=False)
-
-        async def fini():
-            await self.allrole.fini()
-            await self.rootuser.fini()
-            [await u.fini() for u in self.users()]
-            [await r.fini() for r in self.roles()]
-            [await a.fini() for a in self.authgates.values()]
-
-        self.onfini(fini)
-
-    def users(self):
-        return self.usersbyiden.values()
-
-    def roles(self):
-        return self.rolesbyiden.values()
-
-    def role(self, iden):
-        return self.rolesbyiden.get(iden)
-
-    def user(self, iden):
-        return self.usersbyiden.get(iden)
-
-    async def reqUser(self, iden):
-
-        user = self.user(iden)
-        if user is None:
-            mesg = f'No user with iden {iden}.'
-            raise s_exc.NoSuchUser(mesg=mesg, user=iden)
-        return user
-
-    async def reqRole(self, iden):
-
-        role = self.role(iden)
-        if role is None:
-            mesg = f'No role with iden {iden}.'
-            raise s_exc.NoSuchRole(mesg=mesg)
-        return role
-
-    async def reqUserByName(self, name):
-        user = await self.getUserByName(name)
-        if user is None:
-            mesg = f'No user named {name}.'
-            raise s_exc.NoSuchUser(mesg=mesg, username=name)
-        return user
-
-    async def reqUserByNameOrIden(self, name):
-        user = await self.getUserByName(name)
-        if user is not None:
-            return user
-
-        user = self.user(name)
-        if user is None:
-            mesg = f'No user with name or iden {name}.'
-            raise s_exc.NoSuchUser(mesg=mesg)
-        return user
-
-    async def reqRoleByName(self, name):
-        role = await self.getRoleByName(name)
-        if role is None:
-            mesg = f'No role named {name}.'
-            raise s_exc.NoSuchRole(mesg=mesg)
-        return role
-
-    async def getUserByName(self, name):
-        '''
-        Get a user by their username.
-
-        Args:
-            name (str): Name of the user to get.
-
-        Returns:
-            HiveUser: A Hive User.  May return None if there is no user by the requested name.
-        '''
-        return self.usersbyname.get(name)
-
-    async def getUserIdenByName(self, name):
-        user = await self.getUserByName(name)
-        return None if user is None else user.iden
-
-    async def getRoleByName(self, name):
-        return self.rolesbyname.get(name)
-
-    async def _addUserNode(self, node):
-
-        user = await HiveUser.anit(node, self)
-
-        self.usersbyiden[user.iden] = user
-        self.usersbyname[user.name] = user
-
-        return user
-
-    async def setUserName(self, iden, name):
-        if not isinstance(name, str):
-            raise s_exc.BadArg(mesg='setUserName() name must be a string')
-
-        user = self.usersbyname.get(name)
-        if user is not None:
-            if user.iden == iden:
-                return
-            raise s_exc.DupUserName(name=name)
-
-        user = await self.reqUser(iden)
-
-        self.usersbyname.pop(user.name, None)
-        self.usersbyname[name] = user
-
-        user.name = name
-        await user.node.set(name)
-
-        beheld = {
-            'iden': iden,
-            'valu': name,
-        }
-        await self.feedBeholder('user:name', beheld)
-
-    async def setRoleName(self, iden, name):
-        if not isinstance(name, str):
-            raise s_exc.BadArg(mesg='setRoleName() name must be a string')
-
-        role = self.rolesbyname.get(name)
-        if role is not None:
-            if role.iden == iden:
-                return
-            raise s_exc.DupRoleName(name=name)
-
-        role = await self.reqRole(iden)
-
-        if role.name == 'all':
-            mesg = 'Role "all" may not be renamed.'
-            raise s_exc.BadArg(mesg=mesg)
-
-        self.rolesbyname.pop(role.name, None)
-        self.rolesbyname[name] = role
-
-        role.name = name
-        await role.node.set(name)
-
-        beheld = {
-            'iden': iden,
-            'valu': name,
-        }
-        await self.feedBeholder('role:name', beheld)
-
-    async def feedBeholder(self, evnt, info, gateiden=None, logged=True):
-        if self.nexsroot and self.nexsroot.started and logged:
-            behold = {
-                'event': evnt,
-                'offset': await self.nexsroot.index(),
-                'info': info
-            }
-
-            if gateiden:
-                gate = self.getAuthGate(gateiden)
-                if gate:
-                    behold['gates'] = [gate.pack()]
-
-            await self.fire('cell:beholder', **behold)
-
-    async def setUserInfo(self, iden, name, valu, gateiden=None, logged=True, mesg=None):
-
-        user = await self.reqUser(iden)
-
-        info = user.info
-        if gateiden is not None:
-            info = await user.genGateInfo(gateiden)
-
-        if name in ('locked', 'archived') and not valu:
-            self.checkUserLimit()
-
-        await info.set(name, valu)
-
-        if mesg is None:
-            mesg = {
-                'iden': iden,
-                'name': name,
-            }
-            if name != 'passwd':
-                mesg['valu'] = valu
-
-        await self.feedBeholder('user:info', mesg, gateiden=gateiden, logged=logged)
-
-        # since any user info *may* effect auth
-        user.clearAuthCache()
-
-    async def setRoleInfo(self, iden, name, valu, gateiden=None, logged=True, mesg=None):
-        role = await self.reqRole(iden)
-
-        info = role.info
-        if gateiden is not None:
-            info = await role.genGateInfo(gateiden)
-
-        await info.set(name, valu)
-
-        if mesg is None:
-            mesg = {
-                'iden': iden,
-                'name': name,
-                'valu': valu,
-            }
-        await self.feedBeholder('role:info', mesg, gateiden=gateiden, logged=logged)
-
-        role.clearAuthCache()
-
-    async def _addRoleNode(self, node):
-
-        role = await HiveRole.anit(node, self)
-
-        self.rolesbyiden[role.iden] = role
-        self.rolesbyname[role.name] = role
-
-        return role
-
-    async def _addAuthGate(self, node):
-        gate = await AuthGate.anit(node, self)
-        self.authgates[gate.iden] = gate
-        return gate
-
-    async def addAuthGate(self, iden, authgatetype):
-        '''
-        Retrieve AuthGate by iden.  Create if not present.
-
-        Note:
-            Not change distributed
-
-        Returns:
-            (HiveAuthGate)
-        '''
-        gate = self.getAuthGate(iden)
-        if gate is not None:
-            if gate.type != authgatetype:
-                raise s_exc.InconsistentStorage(mesg=f'Stored AuthGate is of type {gate.type}, not {authgatetype}')
-            return gate
-
-        path = self.node.full + ('authgates', iden)
-        node = await self.node.hive.open(path)
-        await self.node.hive.set(path, authgatetype)
-        return await self._addAuthGate(node)
-
-    async def delAuthGate(self, iden):
-        '''
-        Delete AuthGate by iden.
-
-        Note:
-            Not change distributed
-        '''
-        gate = self.getAuthGate(iden)
-        if gate is None:
-            raise s_exc.NoSuchAuthGate(iden=iden)
-
-        await gate.fini()
-        await gate.delete()
-        await gate.node.pop()
-
-        del self.authgates[iden]
-
-    def getAuthGate(self, iden):
-        return self.authgates.get(iden)
-
-    def getAuthGates(self):
-        return list(self.authgates.values())
-
-    def reqAuthGate(self, iden):
-        gate = self.authgates.get(iden)
-        if gate is None:
-            mesg = f'No auth gate found with iden: ({iden}).'
-            raise s_exc.NoSuchAuthGate(iden=iden, mesg=mesg)
-        return gate
-
-    def checkUserLimit(self):
-        '''
-        Check if we're at the specified user limit.
-
-        This should be called right before adding/unlocking/unarchiving a user.
-
-        Raises: s_exc.HitLimit if the number of active users is at the maximum.
-        '''
-        if self.maxusers == 0:
-            return
-
-        numusers = 0
-
-        for user in self.users():
-            if user.name == 'root':
-                continue
-
-            if user.isLocked() or user.isArchived():
-                continue
-
-            numusers += 1
-
-        if numusers >= self.maxusers:
-            mesg = f'Cell at maximum number of users ({self.maxusers}).'
-            raise s_exc.HitLimit(mesg=mesg)
-
-    async def addUser(self, name, passwd=None, email=None, iden=None):
-        '''
-        Add a User to the Hive.
-
-        Args:
-            name (str): The name of the User.
-            passwd (str): A optional password for the user.
-            email (str): A optional email for the user.
-            iden (str): A optional iden to use as the user iden.
-
-        Returns:
-            HiveUser: A Hive User.
-        '''
-
-        self.checkUserLimit()
-
-        if self.usersbyname.get(name) is not None:
-            raise s_exc.DupUserName(name=name)
-
-        if iden is None:
-            iden = s_common.guid()
-        else:
-            if not s_common.isguid(iden):
-                raise s_exc.BadArg(name='iden', arg=iden, mesg='Argument it not a valid iden.')
-
-            if self.usersbyiden.get(iden) is not None:
-                raise s_exc.DupIden(name=name, iden=iden,
-                                    mesg='User already exists for the iden.')
-
-        await self._push('user:add', iden, name)
-
-        user = self.user(iden)
-
-        if passwd is not None:
-            await user.setPasswd(passwd)
-
-        if email is not None:
-            await self.setUserInfo(user.iden, 'email', email)
-
-        # Everyone's a member of 'all'
-        await user.grant(self.allrole.iden)
-
-        return user
-
-    async def _addUser(self, iden, name):
-
-        user = self.usersbyname.get(name)
-        if user is not None:
-            return
-
-        node = await self.node.open(('users', iden))
-        await node.set(name)
-
-        await self._addUserNode(node)
-
-        user = self.usersbyname.get(name)
-        await self.feedBeholder('user:add', user.pack())
-
-    async def addRole(self, name, iden=None):
-        if self.rolesbyname.get(name) is not None:
-            raise s_exc.DupRoleName(name=name)
-
-        if iden is None:
-            iden = s_common.guid()
-
-        await self._push('role:add', iden, name)
-
-        return self.role(iden)
-
-    async def _addRole(self, iden, name):
-
-        role = self.rolesbyname.get(name)
-        if role is not None:
-            return
-
-        node = await self.node.open(('roles', iden))
-        await node.set(name)
-
-        await self._addRoleNode(node)
-
-        role = self.rolesbyname.get(name)
-        await self.feedBeholder('role:add', role.pack())
-
-    async def delUser(self, iden):
-
-        await self.reqUser(iden)
-        return await self._push('user:del', iden)
-
-    async def _delUser(self, iden):
-
-        if iden == self.rootuser.iden:
-            mesg = 'User "root" may not be deleted.'
-            raise s_exc.BadArg(mesg=mesg)
-
-        user = self.user(iden)
-        if user is None:
-            return
-
-        udef = user.pack()
-        self.usersbyiden.pop(user.iden)
-        self.usersbyname.pop(user.name)
-
-        path = self.node.full + ('users', user.iden)
-
-        for gate in self.authgates.values():
-            await gate._delGateUser(user.iden)
-
-        await user.fini()
-        await self.node.hive.pop(path)
-        await self.fire('user:del', udef=udef)
-        await self.feedBeholder('user:del', {'iden': iden})
-
-    def _getUsersInRole(self, role):
-        for user in self.users():
-            if role.iden in user.info.get('roles', ()):
-                yield user
-
-    async def delRole(self, iden):
-        await self.reqRole(iden)
-        return await self._push('role:del', iden)
-
-    async def _delRole(self, iden):
-
-        if iden == self.allrole.iden:
-            mesg = 'Role "all" may not be deleted.'
-            raise s_exc.BadArg(mesg=mesg)
-
-        role = self.role(iden)
-        if role is None:
-            return
-
-        for user in self._getUsersInRole(role):
-            await user.revoke(role.iden, nexs=False)
-
-        for gate in self.authgates.values():
-            await gate._delGateRole(role.iden)
-
-        self.rolesbyiden.pop(role.iden)
-        self.rolesbyname.pop(role.name)
-
-        await role.fini()
-
-        # directly set the node's value and let events prop
-        path = self.node.full + ('roles', role.iden)
-        await self.node.hive.pop(path)
-        await self.feedBeholder('role:del', {'iden': iden})
-
-class AuthGate(s_base.Base):
-    '''
-    The storage object for object specific rules for users/roles.
-    '''
-    async def __anit__(self, node, auth):
-        await s_base.Base.__anit__(self)
-        self.auth = auth
-
-        self.iden = node.name()
-        self.type = node.valu
-
-        self.node = node
-
-        self.gateroles = {}  # iden -> HiveRole
-        self.gateusers = {}  # iden -> HiveUser
-
-        for useriden, usernode in await node.open(('users',)):
-
-            user = self.auth.user(useriden)
-            if user is None:  # pragma: no cover
-                logger.warning(f'Hive: path {useriden} refers to unknown user')
-                continue
-
-            userinfo = await usernode.dict()
-            self.gateusers[user.iden] = user
-            user.authgates[self.iden] = userinfo
-            user.clearAuthCache()
-
-        for roleiden, rolenode in await node.open(('roles',)):
-
-            role = self.auth.role(roleiden)
-            if role is None:  # pragma: no cover
-                logger.warning(f'Hive: path {roleiden} refers to unknown role')
-                continue
-
-            roleinfo = await rolenode.dict()
-            self.gateroles[role.iden] = role
-            role.authgates[self.iden] = roleinfo
-
-    async def genUserInfo(self, iden):
-        node = await self.node.open(('users', iden))
-        userinfo = await node.dict()
-
-        user = self.auth.user(iden)
-        self.gateusers[iden] = user
-        user.authgates[self.iden] = userinfo
-
-        return userinfo
-
-    async def genRoleInfo(self, iden):
-        node = await self.node.open(('roles', iden))
-        roleinfo = await node.dict()
-
-        role = self.auth.role(iden)
-        self.gateroles[iden] = role
-        role.authgates[self.iden] = roleinfo
-
-        return roleinfo
-
-    async def _delGateUser(self, iden):
-        self.gateusers.pop(iden, None)
-        await self.node.pop(('users', iden))
-
-    async def _delGateRole(self, iden):
-        self.gateroles.pop(iden, None)
-        await self.node.pop(('roles', iden))
-
-    async def delete(self):
-
-        await self.fini()
-
-        for _, user in list(self.gateusers.items()):
-            user.authgates.pop(self.iden, None)
-            user.clearAuthCache()
-
-        for _, role in list(self.gateroles.items()):
-            role.authgates.pop(self.iden, None)
-            role.clearAuthCache()
-
-        await self.node.pop()
-
-    def pack(self):
-
-        users = []
-        for user in self.gateusers.values():
-
-            gateinfo = user.authgates.get(self.iden)
-            if gateinfo is None:
-                continue
-
-            users.append({
-                'iden': user.iden,
-                'rules': gateinfo.get('rules', ()),
-                'admin': gateinfo.get('admin', False),
-            })
-
-        roles = []
-        for role in self.gateroles.values():
-
-            gateinfo = role.authgates.get(self.iden)
-            if gateinfo is None:
-                continue
-
-            roles.append({
-                'iden': role.iden,
-                'rules': gateinfo.get('rules', ()),
-                'admin': gateinfo.get('admin', False),
-            })
-
-        return {
-            'iden': self.iden,
-            'type': self.type,
-            'users': users,
-            'roles': roles,
-        }
-
-class HiveRuler(s_base.Base):
-    '''
-    A HiveNode that holds a list of rules.  This includes HiveUsers, HiveRoles, and the AuthGate variants of those
-    '''
-
-    async def __anit__(self, node, auth):
-        await s_base.Base.__anit__(self)
-
-        self.iden = node.name()
-
-        self.auth = auth
-        self.node = node
-        self.name = node.valu
-        self.info = await node.dict()
-
-        self.info.setdefault('admin', False)
-        self.info.setdefault('rules', ())
-
-        self.authgates = {}
-
-    async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None):  # pragma: no cover
-        raise s_exc.NoSuchImpl(mesg='Subclass must implement _setRulrInfo')
-
-    def getRules(self, gateiden=None):
-
-        if gateiden is None:
-            return list(self.info.get('rules', ()))
-
-        gateinfo = self.authgates.get(gateiden)
-        if gateinfo is None:
-            return []
-
-        return list(gateinfo.get('rules', ()))
-
-    async def setRules(self, rules, gateiden=None, nexs=True, mesg=None):
-        reqValidRules(rules)
-        return await self._setRulrInfo('rules', rules, gateiden=gateiden, nexs=nexs, mesg=mesg)
-
-    async def addRule(self, rule, indx=None, gateiden=None, nexs=True):
-        reqValidRules((rule,))
-        rules = self.getRules(gateiden=gateiden)
-
-        mesg = {
-            'name': 'rule:add',
-            'iden': self.iden,
-            'valu': rule,
-        }
-        if indx is None:
-            rules.append(rule)
-        else:
-            rules.insert(indx, rule)
-            mesg['indx'] = indx
-
-        await self.setRules(rules, gateiden=gateiden, nexs=nexs, mesg=mesg)
-
-    async def delRule(self, rule, gateiden=None):
-        reqValidRules((rule,))
-        rules = self.getRules(gateiden=gateiden)
-        if rule not in rules:
-            return False
-
-        mesg = {
-            'name': 'rule:del',
-            'iden': self.iden,
-            'valu': rule,
-        }
-        rules.remove(rule)
-        await self.setRules(rules, gateiden=gateiden, mesg=mesg)
-        return True
-
-class HiveRole(HiveRuler):
-    '''
-    A role within the Hive authorization subsystem.
-
-    A role in HiveAuth exists to bundle rules together so that the same
-    set of rules can be applied to multiple users.
-    '''
-    def pack(self):
-        return {
-            'type': 'role',
-            'iden': self.iden,
-            'name': self.name,
-            'rules': self.info.get('rules'),
-            'authgates': {name: info.pack() for (name, info) in self.authgates.items()},
-        }
-
-    async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None):
-        return await self.auth.setRoleInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
-
-    async def setName(self, name):
-        return await self.auth.setRoleName(self.iden, name)
-
-    def clearAuthCache(self):
-        for user in self.auth.users():
-            if user.hasRole(self.iden):
-                user.clearAuthCache()
-
-    async def genGateInfo(self, gateiden):
-        info = self.authgates.get(gateiden)
-        if info is None:
-            gate = self.auth.reqAuthGate(gateiden)
-            info = self.authgates[gateiden] = await gate.genRoleInfo(self.iden)
-        return info
-
-    def allowed(self, perm, default=None, gateiden=None):
-
-        perm = tuple(perm)
-        if gateiden is not None:
-            info = self.authgates.get(gateiden)
-            if info is not None:
-                for allow, path in info.get('rules', ()):
-                    if perm[:len(path)] == path:
-                        return allow
-            return default
-
-        # 2. check role rules
-        for allow, path in self.info.get('rules', ()):
-            if perm[:len(path)] == path:
-                return allow
-
-        return default
-
-class HiveUser(HiveRuler):
-    '''
-    A user (could be human or computer) of the system within HiveAuth.
-
-    Cortex-wide rules are stored here.  AuthGate-specific rules for this user are stored in an GateUser.
-    '''
-    async def __anit__(self, node, auth):
-        await HiveRuler.__anit__(self, node, auth)
-
-        self.info.setdefault('roles', ())
-        self.info.setdefault('admin', False)
-        self.info.setdefault('passwd', None)
-        self.info.setdefault('locked', False)
-        self.info.setdefault('archived', False)
-
-        # arbitrary profile data for application layer use
-        prof = await self.node.open(('profile',))
-        self.profile = await prof.dict(nexs=True)
-
-        # TODO: max size check / max count check?
-        varz = await self.node.open(('vars',))
-        self.vars = await varz.dict(nexs=True)
-
-        self.permcache = s_cache.FixedCache(self._allowed)
-        self.allowedcache = s_cache.FixedCache(self._getAllowedReason)
-
-    def pack(self, packroles=False):
-
-        roles = self.info.get('roles', ())
-        if packroles:
-            _roles = []
-            for r in roles:
-                role = self.auth.role(r)
-                if role is None:
-                    logger.error(f'User {self.iden} ({self.name}) contains a missing role: {r}')
-                    continue
-                _roles.append(role.pack())
-            roles = _roles
-
-        return {
-            'type': 'user',
-            'iden': self.iden,
-            'name': self.name,
-            'rules': self.info.get('rules', ()),
-            'roles': roles,
-            'admin': self.info.get('admin', ()),
-            'email': self.info.get('email'),
-            'locked': self.info.get('locked'),
-            'archived': self.info.get('archived'),
-            'authgates': {name: info.pack() for (name, info) in self.authgates.items()},
-        }
-
-    async def _setRulrInfo(self, name, valu, gateiden=None, nexs=True, mesg=None):
-        return await self.auth.setUserInfo(self.iden, name, valu, gateiden=gateiden, logged=nexs, mesg=mesg)
-
-    async def setName(self, name):
-        return await self.auth.setUserName(self.iden, name)
-
-    async def allow(self, perm):
-        if not self.allowed(perm):
-            await self.addRule((True, perm), indx=0)
-
-    def allowed(self,
-                perm: tuple[str, ...],
-                default: Optional[str] = None,
-                gateiden: Optional[str] = None,
-                deepdeny: bool = False) -> Union[bool, None]:
-        '''
-        Check if a user is allowed a given permission.
-
-        Args:
-            perm: The permission tuple to check.
-            default: The default rule value if there is no match.
-            gateiden: The gate iden to check against.
-            deepdeny: If True, give precedence for checking deny rules which are more specific than the requested
-                      permission.
-
-        Notes:
-            The use of the deepdeny argument is intended for checking a less-specific part of a permissions tree, in
-            order to know about possible short circuit options. Using it to check a more specific part may have
-            unintended results.
-
-        Returns:
-            The allowed value of the permission.
-        '''
-        perm = tuple(perm)
-        return self.permcache.get((perm, default, gateiden, deepdeny))
-
-    def _allowed(self, pkey):
-        '''
-        NOTE: This must remain in sync with any changes to _getAllowedReason()!
-        '''
-
-        perm, default, gateiden, deepdeny = pkey
-
-        if self.info.get('locked'):
-            return False
-
-        if self.info.get('admin'):
-            return True
-
-        if deepdeny and self._hasDeepDeny(perm, gateiden):
-            return False
-
-        # 1. check authgate user rules
-        if gateiden is not None:
-
-            info = self.authgates.get(gateiden)
-            if info is not None:
-
-                if info.get('admin'):
-                    return True
-
-                for allow, path in info.get('rules', ()):
-                    if perm[:len(path)] == path:
-                        return allow
-
-        # 2. check user rules
-        for allow, path in self.info.get('rules', ()):
-            if perm[:len(path)] == path:
-                return allow
-
-        # 3. check authgate role rules
-        if gateiden is not None:
-
-            for role in self.getRoles():
-
-                info = role.authgates.get(gateiden)
-                if info is None:
-                    continue
-
-                for allow, path in info.get('rules', ()):
-                    if perm[:len(path)] == path:
-                        return allow
-
-        # 4. check role rules
-        for role in self.getRoles():
-            for allow, path in role.info.get('rules', ()):
-                if perm[:len(path)] == path:
-                    return allow
-
-        return default
-
-    def getAllowedReason(self, perm, default=None, gateiden=None):
-        '''
-        A routine which will return a tuple of (allowed, info).
-        '''
-        perm = tuple(perm)
-        return self.allowedcache.get((perm, default, gateiden))
-
-    def _getAllowedReason(self, pkey):
-        '''
-        NOTE: This must remain in sync with any changes to _allowed()!
-        '''
-        perm, default, gateiden = pkey
-        if self.info.get('locked'):
-            return _allowedReason(False, islocked=True)
-
-        if self.info.get('admin'):
-            return _allowedReason(True, isadmin=True)
-
-        # 1. check authgate user rules
-        if gateiden is not None:
-
-            info = self.authgates.get(gateiden)
-            if info is not None:
-
-                if info.get('admin'):
-                    return _allowedReason(True, isadmin=True, gateiden=gateiden)
-
-                for allow, path in info.get('rules', ()):
-                    if perm[:len(path)] == path:
-                        return _allowedReason(allow, gateiden=gateiden, rule=path)
-
-        # 2. check user rules
-        for allow, path in self.info.get('rules', ()):
-            if perm[:len(path)] == path:
-                return _allowedReason(allow, rule=path)
-
-        # 3. check authgate role rules
-        if gateiden is not None:
-
-            for role in self.getRoles():
-
-                info = role.authgates.get(gateiden)
-                if info is None:
-                    continue
-
-                for allow, path in info.get('rules', ()):
-                    if perm[:len(path)] == path:
-                        return _allowedReason(allow, gateiden=gateiden, roleiden=role.iden, rolename=role.name,
-                                              rule=path)
-
-        # 4. check role rules
-        for role in self.getRoles():
-            for allow, path in role.info.get('rules', ()):
-                if perm[:len(path)] == path:
-                    return _allowedReason(allow, roleiden=role.iden, rolename=role.name, rule=path)
-
-        return _allowedReason(default, default=True)
-
-    def _hasDeepDeny(self, perm, gateiden):
-
-        permlen = len(perm)
-
-        # 1. check authgate user rules
-        if gateiden is not None:
-
-            info = self.authgates.get(gateiden)
-            if info is not None:
-
-                if info.get('admin'):
-                    return False
-
-                for allow, path in info.get('rules', ()):
-                    if allow:
-                        continue
-                    if path[:permlen] == perm and len(path) > permlen:
-                        return True
-
-        # 2. check user rules
-        for allow, path in self.info.get('rules', ()):
-            if allow:
-                continue
-
-            if path[:permlen] == perm and len(path) > permlen:
-                return True
-
-        # 3. check authgate role rules
-        if gateiden is not None:
-
-            for role in self.getRoles():
-
-                info = role.authgates.get(gateiden)
-                if info is None:
-                    continue
-
-                for allow, path in info.get('rules', ()):
-                    if allow:
-                        continue
-                    if path[:permlen] == perm and len(path) > permlen:
-                        return True
-
-        # 4. check role rules
-        for role in self.getRoles():
-            for allow, path in role.info.get('rules', ()):
-                if allow:
-                    continue
-                if path[:permlen] == perm and len(path) > permlen:
-                    return True
-
-        return False
-
-    def clearAuthCache(self):
-        self.permcache.clear()
-        self.allowedcache.clear()
-
-    async def genGateInfo(self, gateiden):
-        info = self.authgates.get(gateiden)
-        if info is None:
-            gate = self.auth.reqAuthGate(gateiden)
-            info = await gate.genUserInfo(self.iden)
-        return info
-
-    def confirm(self, perm, default=None, gateiden=None):
-        if not self.allowed(perm, default=default, gateiden=gateiden):
-            self.raisePermDeny(perm, gateiden=gateiden)
-
-    def raisePermDeny(self, perm, gateiden=None):
-
-        perm = '.'.join(perm)
-        if gateiden is None:
-            mesg = f'User {self.name!r} ({self.iden}) must have permission {perm}'
-            raise s_exc.AuthDeny(mesg=mesg, perm=perm, user=self.iden, username=self.name)
-
-        gate = self.auth.reqAuthGate(gateiden)
-        mesg = f'User {self.name!r} ({self.iden}) must have permission {perm} on object {gate.iden} ({gate.type}).'
-        raise s_exc.AuthDeny(mesg=mesg, perm=perm, user=self.iden, username=self.name)
-
-    def getRoles(self):
-        for iden in self.info.get('roles', ()):
-            role = self.auth.role(iden)
-            if role is None:
-                logger.warning(f'user {self.iden} has non-existent role: {iden}')
-                continue
-            yield role
-
-    def hasRole(self, iden):
-        return iden in self.info.get('roles', ())
-
-    async def grant(self, roleiden, indx=None):
-
-        role = await self.auth.reqRole(roleiden)
-
-        roles = list(self.info.get('roles'))
-        if role.iden in roles:
-            return
-
-        if indx is None:
-            roles.append(role.iden)
-        else:
-            roles.insert(indx, role.iden)
-
-        mesg = {'name': 'role:grant', 'iden': self.iden, 'role': role.pack()}
-        await self.auth.setUserInfo(self.iden, 'roles', roles, mesg=mesg)
-
-    async def setRoles(self, roleidens):
-        '''
-        Replace all the roles for a given user with a new list of roles.
-
-        Args:
-            roleidens (list): A list of roleidens.
-
-        Notes:
-            The roleiden for the "all" role must be present in the new list of roles. This replaces all existing roles
-            that the user has with the new roles.
-
-        Returns:
-            None
-        '''
-        current_roles = list(self.info.get('roles'))
-
-        roleidens = list(roleidens)
-
-        if current_roles == roleidens:
-            return
-
-        if self.auth.allrole.iden not in roleidens:
-            mesg = 'Role "all" must be in the list of roles set.'
-            raise s_exc.BadArg(mesg=mesg)
-
-        roles = []
-        for iden in roleidens:
-            r = await self.auth.reqRole(iden)
-            roles.append(r.pack())
-
-        mesg = {'name': 'role:set', 'iden': self.iden, 'roles': roles}
-        await self.auth.setUserInfo(self.iden, 'roles', roleidens, mesg=mesg)
-
-    async def revoke(self, iden, nexs=True):
-
-        role = await self.auth.reqRole(iden)
-
-        if role.name == 'all':
-            mesg = 'Role "all" may not be revoked.'
-            raise s_exc.BadArg(mesg=mesg)
-
-        roles = list(self.info.get('roles'))
-        if role.iden not in roles:
-            return
-
-        roles.remove(role.iden)
-        mesg = {'name': 'role:revoke', 'iden': self.iden, 'role': role.pack()}
-        await self.auth.setUserInfo(self.iden, 'roles', roles, logged=nexs, mesg=mesg)
-
-    def isLocked(self):
-        return self.info.get('locked')
-
-    def isAdmin(self, gateiden=None):
-
-        # being a global admin always wins
-        admin = self.info.get('admin', False)
-        if admin or gateiden is None:
-            return admin
-
-        gateinfo = self.authgates.get(gateiden)
-        if gateinfo is None:
-            return False
-
-        return gateinfo.get('admin', False)
-
-    def reqAdmin(self, gateiden=None, mesg=None):
-
-        if self.isAdmin(gateiden=gateiden):
-            return
-
-        if mesg is None:
-            mesg = 'This action requires global admin permissions.'
-            if gateiden is not None:
-                mesg = f'This action requires admin permissions on gate: {gateiden}'
-
-        raise s_exc.AuthDeny(mesg=mesg, user=self.iden, username=self.name)
-
-    def isArchived(self):
-        return self.info.get('archived')
-
-    async def setAdmin(self, admin, gateiden=None, logged=True):
-        if not isinstance(admin, bool):
-            raise s_exc.BadArg(mesg='setAdmin requires a boolean')
-        await self.auth.setUserInfo(self.iden, 'admin', admin, gateiden=gateiden, logged=logged)
-
-    async def setLocked(self, locked, logged=True):
-        if not isinstance(locked, bool):
-            raise s_exc.BadArg(mesg='setLocked requires a boolean')
-        await self.auth.setUserInfo(self.iden, 'locked', locked, logged=logged)
-
-    async def setArchived(self, archived):
-        if not isinstance(archived, bool):
-            raise s_exc.BadArg(mesg='setArchived requires a boolean')
-        await self.auth.setUserInfo(self.iden, 'archived', archived)
-        if archived:
-            await self.setLocked(True)
-
-    async def tryPasswd(self, passwd, nexs=True):
-
-        if self.info.get('locked', False):
-            return False
-
-        if passwd is None:
-            return False
-
-        onepass = self.info.get('onepass')
-        if onepass is not None:
-            if isinstance(onepass, dict):
-                shadow = onepass.get('shadow')
-                expires = onepass.get('expires')
-                if expires >= s_common.now():
-                    if await s_passwd.checkShadowV2(passwd=passwd, shadow=shadow):
-                        await self.auth.setUserInfo(self.iden, 'onepass', None)
-                        logger.debug(f'Used one time password for {self.name}',
-                                     extra={'synapse': {'user': self.iden, 'username': self.name}})
-                        return True
-            else:
-                # Backwards compatible password handling
-                expires, params, hashed = onepass
-                if expires >= s_common.now():
-                    if s_common.guid((params, passwd)) == hashed:
-                        await self.auth.setUserInfo(self.iden, 'onepass', None)
-                        logger.debug(f'Used one time password for {self.name}',
-                                     extra={'synapse': {'user': self.iden, 'username': self.name}})
-                        return True
-
-        shadow = self.info.get('passwd')
-        if shadow is None:
-            return False
-
-        if isinstance(shadow, dict):
-            return await s_passwd.checkShadowV2(passwd=passwd, shadow=shadow)
-
-        # Backwards compatible password handling
-        salt, hashed = shadow
-        if s_common.guid((salt, passwd)) == hashed:
-            logger.debug(f'Migrating password to shadowv2 format for user {self.name}',
-                         extra={'synapse': {'user': self.iden, 'username': self.name}})
-            # Update user to new password hashing scheme.
-            await self.setPasswd(passwd=passwd, nexs=nexs)
-
-            return True
-
-        return False
-
-    async def setPasswd(self, passwd, nexs=True):
-        # Prevent empty string or non-string values
-        if passwd is None:
-            shadow = None
-        elif passwd and isinstance(passwd, str):
-            shadow = await s_passwd.getShadowV2(passwd=passwd)
-        else:
-            raise s_exc.BadArg(mesg='Password must be a string')
-        await self.auth.setUserInfo(self.iden, 'passwd', shadow, logged=nexs)
diff --git a/synapse/lib/snap.py b/synapse/lib/snap.py
index ed874ffbb32..07f1eed79b0 100644
--- a/synapse/lib/snap.py
+++ b/synapse/lib/snap.py
@@ -138,16 +138,30 @@ async def addEdge(self, verb, n2iden):
 
         if not await self.ctx.snap.hasNodeEdge(self.buid, verb, s_common.uhex(n2iden)):
             self.edges.add(tupl)
-
             if len(self.edges) >= 1000:
-                nodeedits = self.ctx.getNodeEdits()
-                await self.ctx.snap.applyNodeEdits(nodeedits)
-                self.edges.clear()
-
+                await self.flushEdits()
             return True
 
         return False
 
+    async def flushEdits(self):
+        if (nodeedit := self.getNodeEdit()) is not None:
+            nodecache = {self.buid: self.node}
+            nodes = await self.ctx.snap.applyNodeEdits((nodeedit,), nodecache=nodecache, meta=self.ctx.meta)
+
+            if self.node is None:
+                if nodes and nodes[0].buid == self.buid:
+                    self.node = nodes[0]
+                else:  # pragma: no cover
+                    self.node = await self.ctx.snap.getNodeByBuid(self.buid)
+
+            self.tags.clear()
+            self.props.clear()
+            self.tagprops.clear()
+            self.edges.clear()
+            self.edgedels.clear()
+            self.nodedata.clear()
+
     async def delEdge(self, verb, n2iden):
 
         if not isinstance(verb, str):
@@ -175,12 +189,8 @@ async def delEdge(self, verb, n2iden):
 
         if await self.ctx.snap.layers[-1].hasNodeEdge(self.buid, verb, s_common.uhex(n2iden)):
             self.edgedels.add(tupl)
-
             if len(self.edgedels) >= 1000:
-                nodeedits = self.ctx.getNodeEdits()
-                await self.ctx.snap.applyNodeEdits(nodeedits)
-                self.edgedels.clear()
-
+                await self.flushEdits()
             return True
 
         return False
@@ -429,6 +439,9 @@ async def set(self, name, valu, norminfo=None, ignore_ro=False):
                 full = f'{prop.name}:{subname}'
                 subprop = self.form.props.get(full)
                 if subprop is not None and not subprop.locked:
+                    if subprop.deprecated:
+                        self.ctx.snap._skipPropDeprWarn(subprop.full)
+
                     await self.set(full, subvalu)
 
         propadds = norminfo.get('adds')
@@ -438,11 +451,14 @@ async def set(self, name, valu, norminfo=None, ignore_ro=False):
 
         return True
 
-    async def getSetOps(self, name, valu, norminfo=None):
+    async def getSetSubOps(self, name, valu, norminfo=None):
         prop = self.form.props.get(name)
-        if prop is None:
+        if prop is None or prop.locked:
             return ()
 
+        if prop.deprecated:
+            self.ctx.snap._skipPropDeprWarn(prop.full)
+
         retn = await self._set(prop, valu, norminfo=norminfo)
         if retn is False:
             return ()
@@ -459,9 +475,7 @@ async def getSetOps(self, name, valu, norminfo=None):
         if propsubs is not None:
             for subname, subvalu in propsubs.items():
                 full = f'{prop.name}:{subname}'
-                subprop = self.form.props.get(full)
-                if subprop is not None and not subprop.locked:
-                    ops.append(self.getSetOps(full, subvalu))
+                ops.append(self.getSetSubOps(full, subvalu))
 
         propadds = norminfo.get('adds')
         if propadds is not None:
@@ -474,7 +488,8 @@ class SnapEditor:
     '''
     A SnapEditor allows tracking node edits with subs/deps as a transaction.
     '''
-    def __init__(self, snap):
+    def __init__(self, snap, meta=None):
+        self.meta = meta
         self.snap = snap
         self.protonodes = {}
         self.maxnodes = snap.core.maxnodes
@@ -492,6 +507,19 @@ def getNodeEdits(self):
                 nodeedits.append(nodeedit)
         return nodeedits
 
+    async def flushEdits(self):
+        nodecache = {}
+        nodeedits = []
+        for protonode in self.protonodes.values():
+            if (nodeedit := protonode.getNodeEdit()) is not None:
+                nodeedits.append(nodeedit)
+                nodecache[protonode.buid] = protonode.node
+
+        if nodeedits:
+            await self.snap.applyNodeEdits(nodeedits, nodecache=nodecache, meta=self.meta)
+
+        self.protonodes.clear()
+
     async def _addNode(self, form, valu, props=None, norminfo=None):
 
         self.snap.core._checkMaxNodes()
@@ -568,7 +596,7 @@ async def getAddNodeOps(self, formname, valu, props=None, norminfo=None):
         subs = norminfo.get('subs')
         if subs is not None:
             for prop, valu in subs.items():
-                ops.append(protonode.getSetOps(prop, valu))
+                ops.append(protonode.getSetSubOps(prop, valu))
 
         adds = norminfo.get('adds')
         if adds is not None:
@@ -604,7 +632,7 @@ async def _initProtoNode(self, form, norm, norminfo):
         subs = norminfo.get('subs')
         if subs is not None:
             for prop, valu in subs.items():
-                ops.append(protonode.getSetOps(prop, valu))
+                ops.append(protonode.getSetSubOps(prop, valu))
 
             while ops:
                 oset = ops.popleft()
@@ -861,6 +889,10 @@ async def warnonce(self, mesg, log=True, **info):
         self._warnonce_keys.add(mesg)
         await self.warn(mesg, log, **info)
 
+    def _skipPropDeprWarn(self, name):
+        mesg = f'The property {name} is deprecated or using a deprecated type and will be removed in 3.0.0'
+        self._warnonce_keys.add(mesg)
+
     async def getNodeByBuid(self, buid):
         '''
         Retrieve a node tuple by binary id.
@@ -1218,11 +1250,13 @@ async def applyNodeEdit(self, edit, nodecache=None):
         if nodes:
             return nodes[0]
 
-    async def applyNodeEdits(self, edits, nodecache=None):
+    async def applyNodeEdits(self, edits, nodecache=None, meta=None):
         '''
         Sends edits to the write layer and evaluates the consequences (triggers, node object updates)
         '''
-        meta = await self.getSnapMeta()
+        if meta is None:
+            meta = await self.getSnapMeta()
+
         saveoff, changes, nodes = await self._applyNodeEdits(edits, meta, nodecache=nodecache)
         return nodes
 
diff --git a/synapse/lib/storm.py b/synapse/lib/storm.py
index 1d52181a187..c59171cbb4c 100644
--- a/synapse/lib/storm.py
+++ b/synapse/lib/storm.py
@@ -3653,23 +3653,26 @@ async def diffgenr():
 
             genr = diffgenr()
 
-        async with await runt.snap.view.parent.snap(user=runt.user.iden) as snap:
+        async with await runt.snap.view.parent.snap(user=runt.user) as snap:
             snap.strict = False
 
             snap.on('warn', runt.snap.dist)
 
+            meta = {'user': runt.user.iden}
+
+            if doapply:
+                editor = s_snap.SnapEditor(snap, meta=meta)
+
             async for node, path in genr:
 
                 # the timestamp for the adds/subs of each node merge will match
                 nodeiden = node.iden()
-                meta = {'user': runt.user.iden, 'time': s_common.now()}
+
+                meta['time'] = s_common.now()
 
                 sodes = await node.getStorNodes()
                 sode = sodes[0]
 
-                if doapply:
-                    editor = s_snap.SnapEditor(snap)
-
                 subs = []
 
                 # check all node perms first
@@ -3818,13 +3821,11 @@ async def diffgenr():
                     subs.append((s_layer.EDIT_NODE_DEL, valu, ()))
 
                 if doapply:
-                    addedits = editor.getNodeEdits()
-                    if addedits:
-                        await runt.snap.view.parent.storNodeEdits(addedits, meta=meta)
+                    await editor.flushEdits()
 
                     if subs:
                         subedits = [(node.buid, node.form.name, subs)]
-                        await runt.snap.view.storNodeEdits(subedits, meta=meta)
+                        await runt.snap.applyNodeEdits(subedits, nodecache={node.buid: node}, meta=meta)
 
                 runt.snap.clearCachedNode(node.buid)
                 yield await runt.snap.getNodeByBuid(node.buid), path
diff --git a/synapse/lib/version.py b/synapse/lib/version.py
index 78c09170fd2..35c9bb51c2a 100644
--- a/synapse/lib/version.py
+++ b/synapse/lib/version.py
@@ -223,6 +223,6 @@ def reqVersion(valu, reqver,
 ##############################################################################
 # The following are touched during the release process by bumpversion.
 # Do not modify these directly.
-version = (2, 198, 0)
+version = (2, 199, 0)
 verstring = '.'.join([str(x) for x in version])
 commit = ''
diff --git a/synapse/lib/view.py b/synapse/lib/view.py
index 89198fbf681..8ad8a7f17e0 100644
--- a/synapse/lib/view.py
+++ b/synapse/lib/view.py
@@ -1474,7 +1474,8 @@ async def merge(self, useriden=None, force=False):
 
             meta = await snap.getSnapMeta()
             async for nodeedits in fromlayr.iterLayerNodeEdits():
-                await self.parent.storNodeEdits([nodeedits], meta)
+                meta['time'] = s_common.now()
+                await snap.saveNodeEdits([nodeedits], meta)
 
     async def swapLayer(self):
         oldlayr = self.layers[0]
diff --git a/synapse/tools/hive/load.py b/synapse/tools/hive/load.py
index 5aaab9bb1f0..11fd76eee5c 100644
--- a/synapse/tools/hive/load.py
+++ b/synapse/tools/hive/load.py
@@ -10,7 +10,8 @@
 import synapse.lib.msgpack as s_msgpack
 import synapse.lib.version as s_version
 
-reqver = '>=0.2.0,<3.0.0'
+# Change to v2.199.0 before release.
+reqver = '>=2.198,<3.0.0'
 
 async def main(argv, outp=s_output.stdout):
 
@@ -42,16 +43,10 @@ async def main(argv, outp=s_output.stdout):
     async with s_telepath.withTeleEnv():
 
         async with await s_telepath.openurl(opts.hiveurl) as hive:
-            classes = hive.sharinfo.get('classes', ())
-
             try:
                 s_version.reqVersion(hive._getSynVers(), reqver)
-                if 'synapse.lib.hive.HiveApi' in classes:
-                    s_common.deprecated('Connecting directly to a Hive via Telepath', curv='2.198.0', eolv='2.199.0')
-                    await hive.loadHiveTree(tree, path=path, trim=opts.trim)
-                else:
-                    todo = s_common.todo('loadHiveTree', tree, path=path, trim=opts.trim)
-                    await hive.dyncall('cell', todo)
+                todo = s_common.todo('loadHiveTree', tree, path=path, trim=opts.trim)
+                await hive.dyncall('cell', todo)
 
             except s_exc.BadVersion as e:
                 valu = s_version.fmtVersion(*e.get('valu'))


Test patch:
diff --git a/synapse/tests/test_cortex.py b/synapse/tests/test_cortex.py
index 54ca12eff82..b79d70c45fa 100644
--- a/synapse/tests/test_cortex.py
+++ b/synapse/tests/test_cortex.py
@@ -4629,12 +4629,6 @@ async def test_storm_lift_compute(self):
             self.len(2, await core.nodes('[ inet:dns:a=(vertex.link,1.2.3.4) inet:dns:a=(woot.com,5.6.7.8)]'))
             self.len(4, await core.nodes('inet:dns:a inet:fqdn=:fqdn'))
 
-    async def test_cortex_hive(self):
-        async with self.getTestCore() as core:
-            await core.hive.set(('visi',), 200)
-            async with core.getLocalProxy(share='cortex/hive') as hive:
-                self.eq(200, await hive.get(('visi',)))
-
     async def test_cortex_delnode_perms(self):
 
         async with self.getTestCore() as core:
diff --git a/synapse/tests/test_datamodel.py b/synapse/tests/test_datamodel.py
index 72f8d4f640c..c5a18d6dae4 100644
--- a/synapse/tests/test_datamodel.py
+++ b/synapse/tests/test_datamodel.py
@@ -338,11 +338,33 @@ async def test_datamodel_edges(self):
 
     async def test_datamodel_locked_subs(self):
 
-        async with self.getTestCore() as core:
-            await core.setDeprLock('it:prod:softver:semver:major', True)
-            nodes = await core.nodes('[ it:prod:softver=* :semver=3.1.0 ]')
-            self.none(nodes[0].get('semver:major'))
-            self.eq(1, nodes[0].get('semver:minor'))
+        conf = {'modules': [('synapse.tests.utils.DeprModule', {})]}
+        async with self.getTestCore(conf=conf) as core:
+
+            msgs = await core.stormlist('[ test:deprsub=bar :range=(1, 5) ]')
+            self.stormHasNoWarnErr(msgs)
+
+            msgs = await core.stormlist('[ test:deprsub2=(foo, (2, 6)) ]')
+            self.stormHasNoWarnErr(msgs)
+
+            nodes = await core.nodes('test:deprsub=bar')
+            self.eq(1, nodes[0].get('range:min'))
+            self.eq(5, nodes[0].get('range:max'))
+
+            nodes = await core.nodes('test:deprsub2=(foo, (2, 6))')
+            self.eq(2, nodes[0].get('range:min'))
+            self.eq(6, nodes[0].get('range:max'))
+
+            await core.setDeprLock('test:deprsub:range:min', True)
+            nodes = await core.nodes('[ test:deprsub=foo :range=(1, 5) ]')
+            self.none(nodes[0].get('range:min'))
+            self.eq(5, nodes[0].get('range:max'))
+
+            await core.nodes('test:deprsub2 | delnode')
+            await core.setDeprLock('test:deprsub2:range:max', True)
+            nodes = await core.nodes('[ test:deprsub2=(foo, (2, 6)) ]')
+            self.none(nodes[0].get('range:max'))
+            self.eq(2, nodes[0].get('range:min'))
 
     def test_datamodel_schema_basetypes(self):
         # N.B. This test is to keep synapse.lib.schemas.datamodel_basetypes const
diff --git a/synapse/tests/test_lib_aha.py b/synapse/tests/test_lib_aha.py
index b17bf71207d..e5c7096ad4e 100644
--- a/synapse/tests/test_lib_aha.py
+++ b/synapse/tests/test_lib_aha.py
@@ -144,7 +144,7 @@ async def test_lib_aha_offon(self):
 
                 conf = {'aha:provision': purl}
                 async with self.getTestCryo(dirn=cryo0_dirn, conf=conf) as cryo:
-                    self.isin(len(await wait00.wait(timeout=6)), (1, 2))
+                    self.len(1 * replaymult, await wait00.wait(timeout=6))
 
                     svc = await aha.getAhaSvc('0.cryo...')
                     linkiden = svc.get('svcinfo', {}).get('online')
@@ -152,16 +152,16 @@ async def test_lib_aha_offon(self):
                     # Tear down the Aha cell.
                     await aha.__aexit__(None, None, None)
 
-            async with self.getTestAha(dirn=dirn) as aha:
-                wait01 = aha.waiter(1, 'aha:svcdown')
-                await wait01.wait(timeout=6)
-                svc = await aha.getAhaSvc('0.cryo...')
-                self.notin('online', svc.get('svcinfo'))
-
-                # Try setting something down a second time
-                await aha.setAhaSvcDown('0.cryo', linkiden, network='synapse')
-                svc = await aha.getAhaSvc('0.cryo...')
-                self.notin('online', svc.get('svcinfo'))
+            with self.getAsyncLoggerStream('synapse.lib.aha', f'Set [0.cryo.synapse] offline.') as stream:
+                async with self.getTestAha(dirn=dirn) as aha:
+                    self.true(await asyncio.wait_for(stream.wait(), timeout=12))
+                    svc = await aha.getAhaSvc('0.cryo...')
+                    self.notin('online', svc.get('svcinfo'))
+
+                    # Try setting something down a second time
+                    await aha.setAhaSvcDown('0.cryo', linkiden, network='synapse')
+                    svc = await aha.getAhaSvc('0.cryo...')
+                    self.notin('online', svc.get('svcinfo'))
 
     async def test_lib_aha_basics(self):
 
diff --git a/synapse/tests/test_lib_cell.py b/synapse/tests/test_lib_cell.py
index 3a520823485..2e233f3b751 100644
--- a/synapse/tests/test_lib_cell.py
+++ b/synapse/tests/test_lib_cell.py
@@ -1203,12 +1203,12 @@ async def test_cell_system_info(self):
 
     async def test_cell_hiveapi(self):
 
-        async with self.getTestCore() as core:
+        async with self.getTestCell() as cell:
 
-            await core.setHiveKey(('foo', 'bar'), 10)
-            await core.setHiveKey(('foo', 'baz'), 30)
+            await cell.setHiveKey(('foo', 'bar'), 10)
+            await cell.setHiveKey(('foo', 'baz'), 30)
 
-            async with core.getLocalProxy() as proxy:
+            async with cell.getLocalProxy() as proxy:
                 self.eq((), await proxy.getHiveKeys(('lulz',)))
                 self.eq((('bar', 10), ('baz', 30)), await proxy.getHiveKeys(('foo',)))
 
diff --git a/synapse/tests/test_lib_hive.py b/synapse/tests/test_lib_hive.py
index 9400044e006..38fea8d4677 100644
--- a/synapse/tests/test_lib_hive.py
+++ b/synapse/tests/test_lib_hive.py
@@ -1,12 +1,8 @@
-import asyncio
-
 import synapse.exc as s_exc
 import synapse.common as s_common
 
 import synapse.tests.utils as s_test
 
-import synapse.lib.hive as s_hive
-
 tree0 = {
     'kids': {
         'hehe': {'value': 'haha'},
@@ -73,40 +69,6 @@ async def test_hive_slab(self):
 
                 self.none(await hive.get(('foo', 'bar', 'lulz')))
 
-    async def test_hive_telepath(self):
-
-        # confirm that the primitives used by higher level APIs
-        # work using telepath remotes and property synchronize.
-
-        async with self.getTestHiveDmon() as dmon:
-
-            turl = self.getTestUrl(dmon, 'hive')
-
-            async with await s_hive.openurl(turl) as hive0:
-
-                path = ('foo', 'bar')
-
-                evnt = asyncio.Event()
-
-                def onedit(mesg):
-                    evnt.set()
-
-                node0 = await hive0.open(path)
-                node0.on('hive:set', onedit)
-
-                async with await s_hive.openurl(turl) as hive1:
-
-                    node1 = await hive1.open(path)
-                    await node1.set(200)
-
-                    await evnt.wait()
-
-                    self.eq(200, node0.valu)
-
-                    self.eq(201, await node0.add(1))
-                    self.eq(202, await node1.add(1))
-                    self.eq(203, await node0.add(1))
-
     async def test_hive_dir(self):
 
         async with self.getTestHive() as hive:
diff --git a/synapse/tests/test_lib_storm.py b/synapse/tests/test_lib_storm.py
index acc58c3c500..6fa8f50a260 100644
--- a/synapse/tests/test_lib_storm.py
+++ b/synapse/tests/test_lib_storm.py
@@ -1757,6 +1757,10 @@ async def test_storm_diff_merge(self):
             msgs = await core.stormlist('test:ro | merge', opts=altview)
             self.stormIsInWarn("Cannot merge read only property with conflicting value", msgs)
 
+            await core.nodes('[ test:str=foo +(refs)> { for $i in $lib.range(1001) {[ test:int=$i ]}}]', opts=altview)
+            await core.nodes('test:str=foo -(refs)+> * merge --apply', opts=altview)
+            self.len(1001, await core.nodes('test:str=foo -(refs)> *'))
+
     async def test_storm_merge_stricterr(self):
 
         conf = {'modules': [('synapse.tests.utils.DeprModule', {})]}
diff --git a/synapse/tests/test_tools_hiveload.py b/synapse/tests/test_tools_hiveload.py
index bf989fd446f..b35f0219861 100644
--- a/synapse/tests/test_tools_hiveload.py
+++ b/synapse/tests/test_tools_hiveload.py
@@ -37,8 +37,8 @@ async def test_tools_hiveload_vercheck(self):
             hivepath0 = os.path.join(dirn, 'hivesave0.mpk')
             s_msgpack.dumpfile(htree0, hivepath0)
 
-            async with self.getTestHiveDmon() as dmon:
-                hurl = self.getTestUrl(dmon, 'hive')
+            async with self.getTestCell() as cell:
+                hurl = cell.getLocalUrl()
 
                 argv = [hurl, hivepath0]
 
@@ -64,43 +64,39 @@ async def test_tools_hiveload(self):
             s_msgpack.dumpfile(htree1, hivepath1)
             s_common.yamlsave(htree0, yamlpath0)
 
-            async with self.getTestHiveDmon() as dmon:
-
-                hive = dmon.shared.get('hive')
-                hurl = self.getTestUrl(dmon, 'hive')
+            async with self.getTestCell() as cell:
+                hurl = cell.getLocalUrl()
 
                 retn = await s_hiveload.main([hurl, hivepath0])
                 self.eq(0, retn)
 
-                self.eq(20, await hive.get(('hehe',)))
-                self.eq(30, await hive.get(('haha',)))
-                self.eq('bar', await hive.get(('haha', 'foo')))
-                self.eq('faz', await hive.get(('haha', 'baz')))
+                self.eq(20, await cell.hive.get(('hehe',)))
+                self.eq(30, await cell.hive.get(('haha',)))
+                self.eq('bar', await cell.hive.get(('haha', 'foo')))
+                self.eq('faz', await cell.hive.get(('haha', 'baz')))
 
                 retn = await s_hiveload.main([hurl, hivepath1])
                 self.eq(0, retn)
 
-                self.eq(20, await hive.get(('hehe',)))
-                self.eq(30, await hive.get(('haha',)))
-                self.eq('bar', await hive.get(('haha', 'foo')))
-                self.eq('faz', await hive.get(('haha', 'baz')))
+                self.eq(20, await cell.hive.get(('hehe',)))
+                self.eq(30, await cell.hive.get(('haha',)))
+                self.eq('bar', await cell.hive.get(('haha', 'foo')))
+                self.eq('faz', await cell.hive.get(('haha', 'baz')))
 
                 retn = await s_hiveload.main(['--trim', hurl, hivepath1])
                 self.eq(0, retn)
 
-                self.eq(20, await hive.get(('hehe',)))
-                self.eq(30, await hive.get(('haha',)))
-                self.eq('faz', await hive.get(('haha', 'baz')))
-
-                self.none(await hive.get(('haha', 'foo')))
+                self.eq(20, await cell.hive.get(('hehe',)))
+                self.eq(30, await cell.hive.get(('haha',)))
+                self.eq('faz', await cell.hive.get(('haha', 'baz')))
 
-            async with self.getTestHiveDmon() as dmon:
+                self.none(await cell.hive.get(('haha', 'foo')))
 
-                hive = dmon.shared.get('hive')
-                hurl = self.getTestUrl(dmon, 'hive')
+            async with self.getTestCell() as cell:
+                hurl = cell.getLocalUrl()
 
                 await s_hiveload.main(['--path', 'v/i/s/i', '--yaml', hurl, yamlpath0])
-                self.eq('bar', await hive.get(('v', 'i', 's', 'i', 'haha', 'foo')))
+                self.eq('bar', await cell.hive.get(('v', 'i', 's', 'i', 'haha', 'foo')))
 
             path = os.path.join(dirn, 'cell')
             async with await s_cell.Cell.anit(path) as cell:
diff --git a/synapse/tests/test_tools_hivesave.py b/synapse/tests/test_tools_hivesave.py
index 4d9be30006a..1b2f5711ca0 100644
--- a/synapse/tests/test_tools_hivesave.py
+++ b/synapse/tests/test_tools_hivesave.py
@@ -17,8 +17,8 @@ async def test_tools_hivesave_vercheck(self):
 
             hivepath0 = os.path.join(dirn, 'hivesave0.mpk')
 
-            async with self.getTestHiveDmon() as dmon:
-                hurl = self.getTestUrl(dmon, 'hive')
+            async with self.getTestCell() as cell:
+                hurl = cell.getLocalUrl()
 
                 argv = [hurl, hivepath0]
 
@@ -39,12 +39,10 @@ async def test_tools_hivesave(self):
             hivepath0 = os.path.join(dirn, 'hivesave0.mpk')
             yamlpath0 = os.path.join(dirn, 'hivesave0.yaml')
 
-            async with self.getTestHiveDmon() as dmon:
+            async with self.getTestCell() as cell:
+                hurl = cell.getLocalUrl()
 
-                hive = dmon.shared.get('hive')
-                await hive.set(('baz', 'faz'), 'visi')
-
-                hurl = self.getTestUrl(dmon, 'hive')
+                await cell.hive.set(('baz', 'faz'), 'visi')
 
                 retn = await s_hivesave.main([hurl, hivepath0])
                 self.eq(0, retn)
diff --git a/synapse/tests/utils.py b/synapse/tests/utils.py
index a6b6145b5c4..f0ebbe6cbc3 100644
--- a/synapse/tests/utils.py
+++ b/synapse/tests/utils.py
@@ -519,6 +519,12 @@ def getStorNode(self, form):
         ('test:deprarray', ('array', {'type': 'test:deprprop'}), {}),
         ('test:deprform', ('test:str', {}), {}),
         ('test:deprndef', ('ndef', {}), {}),
+        ('test:deprsub', ('str', {}), {}),
+        ('test:range', ('range', {}), {}),
+        ('test:deprsub2', ('comp', {'fields': (
+            ('name', 'test:str'),
+            ('range', 'test:range'))
+        }), {}),
     ),
     'forms': (
         ('test:deprprop', {}, ()),
@@ -527,6 +533,17 @@ def getStorNode(self, form):
             ('deprprop', ('test:deprarray', {}), {}),
             ('okayprop', ('str', {}), {}),
         )),
+        ('test:deprsub', {}, (
+            ('range', ('test:range', {}), {}),
+            ('range:min', ('int', {}), {'deprecated': True}),
+            ('range:max', ('int', {}), {}),
+        )),
+        ('test:deprsub2', {}, (
+            ('name', ('str', {}), {}),
+            ('range', ('test:range', {}), {}),
+            ('range:min', ('int', {}), {}),
+            ('range:max', ('int', {}), {'deprecated': True}),
+        )),
     ),
 
 }
@@ -2356,25 +2373,6 @@ async def getTestHiveFromDirn(self, dirn):
             async with await s_hive.SlabHive.anit(slab) as hive:
                 yield hive
 
-    @contextlib.asynccontextmanager
-    async def getTestHiveDmon(self):
-        with self.getTestDir() as dirn:
-            async with self.getTestHiveFromDirn(dirn) as hive:
-                async with self.getTestDmon() as dmon:
-                    dmon.share('hive', hive)
-                    yield dmon
-
-    @contextlib.asynccontextmanager
-    async def getTestTeleHive(self):
-
-        async with self.getTestHiveDmon() as dmon:
-
-            turl = self.getTestUrl(dmon, 'hive')
-
-            async with await s_hive.openurl(turl) as hive:
-
-                yield hive
-
     async def runCoreNodes(self, core, query, opts=None):
         '''
         Run a storm query through a Cortex as a SchedCoro and return the results.