Skip to content

1.0.2.dev5 → 1.0.2.dev6 — Record Visibility & Context Scopes (incl. cross-owner USERS / ORGANIZATIONS)¤

Summary¤

This changelog documents the storage work delivered across the 1.0.2.dev5 → 1.0.2.dev6 line. Two related capabilities landed:

  1. Record visibility — every storage record now carries a read-access scope (Visibility) that is independent of write ownership. You can tag a record PUBLIC / PRIVATE / INTERNAL on write and filter by it on list.
  2. Context scopes — the old string scope: Literal["mission", "setup"] argument on the storage service was replaced by a typed Context enum, and the enum was extended with two read-only cross-owner scopes, USERS and ORGANIZATIONS. dev6 completes the client so those scopes are actually emitted on the wire — a kin can now list records shared by other kins of the same user/organization.

The concrete owner id for the cross-owner scopes is resolved server-side from request metadata; the client only sends the context kind.

Requirement: these features need agentic-mesh-protocol with the visibility fields and the CONTEXT_USERS / CONTEXT_ORGANIZATIONS enum values (shipped in the proto ≥ 1.0.1.dev4). It is pulled in transitively by this SDK version.

What changed¤

Context — the owner/scope of an operation (replaces scope)¤

from digitalkin.models.services.services import Context


class ContextStorage(Enum):
    UNSPECIFIED = "unspecified"
    MISSIONS = "missions"  # this mission (default)
    SETUP_VERSIONS = "setup_versions"  # this setup version (shared across missions)
    USERS = "users"  # read-only: all kins of the same user
    ORGANIZATIONS = "organizations"  # read-only: all kins of the same organization
  • MISSIONS (default) and SETUP_VERSIONS are read/write owner contexts.
  • USERS and ORGANIZATIONS are read-only, list-only cross-owner scopes. The strategy holds no user/org id; it sends only the kind and the storage service resolves the concrete id from the x-user-id / x-organization-id request metadata.

Every public storage method now takes context: ContextStorage instead of the old scope: str:

Method Signature (relevant args)
store store(collection, record_id, data, data_type=DataType.OUTPUT, context=ContextStorage.MISSIONS, visibility=Visibility.UNSPECIFIED)
read read(collection, record_id, context=ContextStorage.MISSIONS)
update update(collection, record_id, data, context=ContextStorage.MISSIONS, visibility=Visibility.UNSPECIFIED)
remove remove(collection, record_id, context=ContextStorage.MISSIONS)
list list(collection, context=ContextStorage.MISSIONS, visibilities=None)
remove_collection remove_collection(collection, context=ContextStorage.MISSIONS)
upsert upsert(collection, record_id, data, data_type=DataType.OUTPUT, context=ContextStorage.MISSIONS, visibility=Visibility.UNSPECIFIED)

Visibility — read-access scope of a record¤

from digitalkin.models.services.storage import Visibility

class Visibility(Enum):
    UNSPECIFIED = 0   # let the storage service apply its default
    PUBLIC      = 1
    PRIVATE     = 2
    INTERNAL    = 3
  • The integer values mirror the storage proto exactly.
  • Ownership (who may edit) stays keyed on the record's context; Visibility only governs who may read it.
  • StorageRecord gained a visibility: Visibility field (default UNSPECIFIED), populated from the wire on read.
  • visibility=UNSPECIFIED is the proto default (0) and is wire-identical to not setting it, so the storage service applies its own default.

Cross-owner wire mapping (completed in dev6)¤

GrpcStorage._context_enum now maps the resolved context to the right wire enum, including the new cross-owner kinds:

  • setup_versions:…CONTEXT_SETUP_VERSIONS
  • users:CONTEXT_USERS
  • organizations:CONTEXT_ORGANIZATIONS
  • otherwise → CONTEXT_MISSIONS

and StorageStrategy._resolve_context returns a kind-only marker (users: / organizations:) for the cross-owner scopes, since the concrete id is resolved server-side.

Local DefaultStorage has no cross-owner data model, so listing under USERS / ORGANIZATIONS returns [] in local/dev mode. Cross-owner reads are a remote (GrpcStorage) capability.

How to use¤

All examples assume you have a storage strategy (e.g. context.storage inside a trigger handler).

Write a record with a visibility¤

from digitalkin.models.services.storage import Visibility
from digitalkin.models.services.services import Context

# Readable by every kin of the same user, owned by this mission
await storage.store(
    "reports",
    "q3-summary",
    {"title": "Q3", "body": "..."},
    visibility=Visibility.PUBLIC,
)

# Persist under the setup version (survives across missions), keep it internal
await storage.upsert(
    "shared_config",
    "defaults",
    {"lang": "fr"},
    context=Context.SETUP_VERSIONS,
    visibility=Visibility.INTERNAL,
)

Change a record's visibility later¤

# UNSPECIFIED leaves the current visibility unchanged
await storage.update("reports", "q3-summary", {"title": "Q3", "body": "..."},
                     visibility=Visibility.PRIVATE)

List and filter by visibility¤

# All readable records in this mission
records = await storage.list("reports")

# Only PUBLIC + INTERNAL records
records = await storage.list(
    "reports",
    visibilities=[Visibility.PUBLIC, Visibility.INTERNAL],
)

for r in records:
    print(r.record_id, r.visibility.name, r.context)

Cross-owner reads (discover data produced by other kins of the same user)¤

# Records other kins of the SAME USER created and shared, subject to visibility.
# The server resolves the concrete user id from the request metadata.
records = await storage.list(
    "reports",
    context=ContextStorage.USERS,
    visibilities=[Visibility.PUBLIC],
)

# Same, but across the whole organization
records = await storage.list("reports", context=ContextStorage.ORGANIZATIONS)

Cross-owner scopes are read-only: use them with list only. store / update / remove always target the owning MISSIONS / SETUP_VERSIONS context.

Migration¤

  • scope=context=: replace every scope="mission" / scope="setup" string argument with context=ContextStorage.MISSIONS / context=ContextStorage.SETUP_VERSIONS. The parameter was renamed and retyped from a str literal to the Context enum, so passing the old string raises TypeError.
  • data_type: pass the DataType enum (e.g. DataType.OUTPUT), not a string — data_type="OUTPUT" no longer works.
  • New optional args: visibility (on store/update/upsert) and visibilities (on list) are optional; omit them to keep the previous behaviour (server default visibility, no visibility filter).
  • No change to read / remove semantics beyond the scopecontext rename.

Minimal before/after:

# before (<= 1.0.0a0)
await storage.list("reports", scope="setup")
await storage.store("reports", "r1", data, data_type="OUTPUT")

# after (>= 1.0.2.dev6)
from digitalkin.models.services.storage import DataType
from digitalkin.models.services.services import Context

await storage.list("reports", context=Context.SETUP_VERSIONS)
await storage.store("reports", "r1", data, data_type=DataType.OUTPUT)

Verification¤

Storage regression coverage lives in tests/services/storage/:

  • test_grpc_storage.py — round-trips visibility on store/update, the visibilities filter on list, and test_list_cross_owner_context_and_visibilities locks the wire mapping (USERS → CONTEXT_USERS, ORGANIZATIONS → CONTEXT_ORGANIZATIONS).
  • test_storage_strategy_locks.py — per-record lock keys use the resolved context string, so locks are created and cleaned up under the right owner.