Skip to content

Ownership

system.devices enforces ownership across five scoping fields. Together they define which principal and which Space a Device record belongs to, and they prevent accidental cross-tenant leakage and intentional ownership hijacking.

The five scoping fields

FieldWhat it scopes
principalIdThe owner: a HiveCast principal id (e.g. p_<sha20>).
spaceIdThe Space the Device runs in: durable Space identity (spc_<hex>).
authorityRootThe bus / security / runtime root (e.g. space.spc-7f3a9b2c).
publicNamespaceOptional typed namespace claim (space.<spacePath>).
routeKeyOptional v1 alias for spacePath.

A Device record may have all five (Spaces with public path, paired Devices), four (Spaces without public path), or just principalId (a new principal whose default Space is implicit). A truly local-only, unpaired Host has none.

The "scope mutation cannot lose previous scope" rule

The most important ownership rule: devices.register will refuse to update a record if the new payload disagrees with the existing record on any populated scope field. From SystemDevicesActor.deviceRecordCanCarryForward():

ts
// projects/matrix-3/packages/system/src/SystemDevicesActor.ts:510-536
function deviceRecordCanCarryForward(previous: IDeviceRecord, payload: Record<string, unknown>): boolean {
  const nextPrincipalId = readString(payload.principalId);
  // ...
  if (previous.principalId && previous.principalId !== nextPrincipalId) return false;
  if (previous.spaceId && previous.spaceId !== nextSpaceId) return false;
  if (previous.authorityRoot && previous.authorityRoot !== nextAuthorityRoot) return false;
  if (previous.hostLinkId && previous.hostLinkId !== nextHostLinkId) return false;
  if (previous.publicNamespace && previous.publicNamespace !== nextPublicNamespace) return false;
  if (previous.routeKey && previous.routeKey !== nextRouteKey) return false;
  return true;
}

Read carefully: the rule is "if previous had a value, new payload MUST have the same value." Going from principalId: undefined to a populated value is allowed; going from one populated principalId to a different one is not. This is exactly the property you want — the first authoritative heartbeat establishes ownership, and subsequent heartbeats cannot reassign it.

onDevicesRegister(), onDevicesHeartbeat(), onDevicesDeregister(), and onDevicesRuntimesRegister() all gate on this check via deviceMatchesPayloadMutationScope().

How devices.list filters by scope

devices.list({ principalId, spaceId?, authorityRoot?, … }) filters via deviceMatchesScope(). The rules:

ts
// projects/matrix-3/packages/system/src/SystemDevicesActor.ts:442-477
function deviceMatchesScope(device: IDeviceRecord, scope: IDeviceScope): boolean {
  if (!scope.hasScope) {
    return !device.principalId && !device.spaceId
      && !device.authorityRoot && !device.publicNamespace
      && !device.routeKey;
  }
  // each populated scope field must match the device's value
  // and at least one populated scope must positively match a populated device value
}

Two important consequences:

  1. No-scope query returns ONLY no-scope devices. A devices.list({}) without any scoping fields returns local-owner Devices that have no principal/Space/authority. It does NOT return all Devices system-wide.
  2. At least one positive match is required. A query with principalId = X returns Devices where device.principalId === X. A query with only spaceId = Y and a Device record that has spaceId = Y but no principalId would still match — but in practice a paired Device always has both.

How merging respects ownership

hostLinkDeviceView() merges a presence record with a Host Link record only if deviceCanMergeWithHostLink() says they agree on identity:

ts
// projects/matrix-3/packages/system/src/SystemDevicesActor.ts:479-508
function deviceCanMergeWithHostLink(presence, link): presence is IDeviceRecord {
  if (presence.deviceId !== link.hostId) return false;
  if (presence.hostLinkId !== link.id) return false;
  if (presence.principalId !== link.principalId) return false;
  if (presence.spaceId !== link.spaceId) return false;
  if (presence.authorityRoot !== link.authorityRoot) return false;
  if (link.publicNamespace && presence.publicNamespace !== link.publicNamespace) return false;
  if (link.routeKey && presence.routeKey !== link.routeKey) return false;
  return true;
}

If a presence record claims hostId = X but says principalId = Y, and the Host Link for hostId X says principalId = Z, the merge is refused. The view falls back to the host-link projection (no presence data merged in). This neutralizes a buggy or malicious Device that tries to attach to someone else's Host Link.

Cross-Space scoping at higher layers

Above system.devices, the full authority model in WORKSTREAMS/core-and-packaging/MATRIX-AUTHORITY-MODEL.md defines:

  • A principal may own multiple Spaces.
  • A Device belongs to one (principal, Space) pair.
  • The bus authorityRoot is space.<spaceId> for new Spaces.
  • Singleton authority mounts (system.devices, system.runtimes, system.registry, system.auth, host.control) have one live owner per authority root.

Implication: each Space gets its own system.devices. A multi-Space account does not have a single global Device list — it has one per Space, addressed under each Space's authority root.

What the scoping fields are NOT

They are not access-control tokens. The principalId on a Device record is an identity field; it does not by itself authorize the caller. The actual authorization layer is bearer-token verification on auth.hostLink.verifyHeartbeat and the singleton authority property that forces all writes through one authoritative actor per root.

scopes[] on the Host Link record is for capability annotation. Today it is recorded for audit but not used as a per-op gate. That gate is target state.

See also

Source: projects/matrix-3/packages/system/src/SystemDevicesActor.ts (deviceRecordCanCarryForward(), deviceMatchesScope(), deviceCanMergeWithHostLink()).