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:
- Record visibility — every storage record now carries a read-access scope (
Visibility) that is independent of write ownership. You can tag a recordPUBLIC/PRIVATE/INTERNALon write and filter by it on list. - Context scopes — the old string
scope: Literal["mission", "setup"]argument on the storage service was replaced by a typedContextenum, and the enum was extended with two read-only cross-owner scopes,USERSandORGANIZATIONS.dev6completes 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-protocolwith thevisibilityfields and theCONTEXT_USERS/CONTEXT_ORGANIZATIONSenum 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) andSETUP_VERSIONSare read/write owner contexts.USERSandORGANIZATIONSare 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 thex-user-id/x-organization-idrequest 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;Visibilityonly governs who may read it. StorageRecordgained avisibility: Visibilityfield (defaultUNSPECIFIED), populated from the wire on read.visibility=UNSPECIFIEDis 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_VERSIONSusers:→CONTEXT_USERSorganizations:→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
DefaultStoragehas no cross-owner data model, so listing underUSERS/ORGANIZATIONSreturns[]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
listonly.store/update/removealways target the owningMISSIONS/SETUP_VERSIONScontext.
Migration¤
scope=→context=: replace everyscope="mission"/scope="setup"string argument withcontext=ContextStorage.MISSIONS/context=ContextStorage.SETUP_VERSIONS. The parameter was renamed and retyped from astrliteral to theContextenum, so passing the old string raisesTypeError.data_type: pass theDataTypeenum (e.g.DataType.OUTPUT), not a string —data_type="OUTPUT"no longer works.- New optional args:
visibility(onstore/update/upsert) andvisibilities(onlist) are optional; omit them to keep the previous behaviour (server default visibility, no visibility filter). - No change to
read/removesemantics beyond thescope→contextrename.
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-tripsvisibilityonstore/update, thevisibilitiesfilter onlist, andtest_list_cross_owner_context_and_visibilitieslocks 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.