Security

Security

dbzero provides security controls for applications that need to protect persisted object data without giving up the natural Python object model. These controls let application code continue to work with memo objects through attributes, queries, references, and collections, while dbzero enforces authorization rules at the points where protected data is read, filtered, or modified.

Restricted Mode

Restricted mode is a standard dbzero feature introduced in 0.4.1. It is a sandboxing control for memo instances. Use it when AI agents, plugin code, tool handlers, or other constrained execution paths should work with application objects through the normal public API without using memo objects to inspect Python or dbzero metadata.

Restricted mode does not make a prefix read-only. On a writable prefix, public memo fields can still be read and written, and public memo methods can still be called. The restriction is on reflection-style access through memo objects and restricted method proxies.

For a restricted memo object:

  • Public fields such as memory.summary are available.
  • Public methods such as memory.add_note("...") are callable.
  • Private and metadata-style attributes such as __class__, __dict__, __mro__, __subclasses__, and _private raise AttributeError.
  • Method introspection such as dir(memory.add_note), memory.add_note.__self__, memory.add_note.__func__, memory.add_note.__globals__, and memory.add_note.__closure__ raises AttributeError.

Restricted mode is sandboxing, not an authorization policy. Use dbzero-pro protected fields or data filtering when different users, tenants, or roles need different access to application data.

Workspace-Wide Restricted Mode

Pass restricted=True to db0.init() to make future opened prefixes restricted by default. This also applies to prefixes that dbzero auto-opens in read-only mode.

import dbzero as db0
 
db0.init("./agent-workspace", prefix="memory", restricted=True)
 
assert db0.get_config()["restricted"] is True
assert db0.get_prefix_stats("memory")["restricted"] is True

Prefix-Scoped Restricted Mode

Pass restricted=True to db0.open() when only one prefix should be restricted:

db0.init("./app-data")
 
db0.open("host-config", "rw")
db0.open("agent-memory", "rw", restricted=True)
 
assert db0.get_prefix_stats("host-config")["restricted"] is False
assert db0.get_prefix_stats("agent-memory")["restricted"] is True

If the workspace default is restricted, an individual prefix can still be opened unrestricted with restricted=False:

db0.init("./app-data", restricted=True)
db0.open("host-admin-data", "rw", restricted=False)

Restricted mode also works with read-only prefixes:

db0.open("shared-knowledge", "r", restricted=True)

Dynamic Restricted Context

Restricted context variables are available starting with dbzero 0.4.2. Use restricted_context when the same process has trusted host code and constrained code, and restricted mode should depend on the current execution context.

Pass a contextvars.ContextVar to db0.init() or db0.open(). When dbzero checks memo reflection access, it calls the context variable's get() method. A truthy value enables restricted mode for that access. A false value, or an unset context variable, leaves access unrestricted.

from contextvars import ContextVar
import dbzero as db0
 
agent_restricted = ContextVar("agent_restricted", default=False)
 
db0.init("./app-data", restricted_context=agent_restricted)
db0.open("agent-memory")
 
# Host code is unrestricted while the context value is false.
assert agent_restricted.get() is False
 
token = agent_restricted.set(True)
try:
    # Memo objects from dynamically restricted prefixes now block
    # reflection-style access in this context.
    run_agent_tool()
finally:
    agent_restricted.reset(token)

Prefix-specific restricted contexts override the workspace default for that prefix:

agent_restricted = ContextVar("agent_restricted", default=False)
 
db0.init("./app-data")
db0.open("agent-memory", "rw", restricted_context=agent_restricted)

db0.get_config()["restricted"] and db0.get_prefix_stats(prefix)["restricted"] report static restricted mode. They do not report the current truthiness of a restricted context variable.

Increasing Restriction at Runtime

Use db0.set_restricted() to increase the restriction level after initialization:

from contextvars import ContextVar
import dbzero as db0
 
agent_restricted = ContextVar("agent_restricted", default=False)
 
db0.init("./app-data")
db0.open("agent-memory")
 
# Add dynamic restricted mode to the workspace and open prefixes.
db0.set_restricted(restricted_context=agent_restricted)
 
# Later, static restricted mode can replace dynamic restriction.
db0.set_restricted(restricted=True)

Restriction can only become stronger: unrestricted prefixes can become dynamically restricted, and dynamically restricted prefixes can become statically restricted. Static restricted mode cannot be weakened or converted to a context-controlled mode, because a context variable can evaluate false.

Practical AI-Agent Example

In this pattern, the host process stores agent memory in a restricted prefix and passes memo instances into tool code. The tool can use the bare application API but cannot inspect metadata through the memo object or its bound methods.

import dbzero as db0
 
@db0.memo
class AgentMemory:
    def __init__(self, summary: str):
        self.summary = summary
        self.notes = []
 
    def add_note(self, note: str):
        self.notes.append(note)
 
def agent_tool(memory: AgentMemory, observation: str):
    # Normal public API access works.
    memory.add_note(observation)
    return memory.summary
 
db0.init("./app-data")
db0.open("agent-memory", "rw", restricted=True)
 
memory = AgentMemory("Customer prefers concise answers.")
agent_tool(memory, "Asked about refund policy.")
 
assert memory.notes == ["Asked about refund policy."]
 
# Reflection-style access through restricted memo objects is blocked.
try:
    memory.__class__
except AttributeError:
    pass
 
try:
    memory.add_note.__globals__
except AttributeError:
    pass

Protected Fields

dbzero-procommercial edition

Protected fields are a dbzero-pro feature for field-level access control on memo objects. Declare a protected memo class with protect_fields=True:

import dbzero as db0
 
@db0.memo(protect_fields=True)
class Customer:
    def __init__(self, name: str, ssn: str):
        self.name = name
        self.ssn = ssn

Once a memo class is protected, dbzero checks field access when Python code reads, creates, updates, or deletes protected fields. This means protection happens at normal object access points such as customer.ssn, customer.ssn = value, and field initialization inside __init__.

Denied field reads raise PermissionError by default. If data masking is configured with a missing_value_placeholder, denied reads return that placeholder instead. This is useful for serialization or API responses where inaccessible fields should be omitted or masked instead of failing the whole operation.

Protected-field metadata is persisted with the memo class. Derived memo classes inherit field protection and cannot disable it while a protected base class is in use.

Activating Data Masking

Protected fields require data masking to be initialized for the process. Configure it when calling db0.init() with the data_masking option:

from contextvars import ContextVar
import dbzero as db0
 
account_id = ContextVar("account_id")
 
db0.init(
    "/var/lib/my-app/dbzero",
    prefix="main",
    data_masking={
        "context_var": account_id,
        "prefix": "main",
        "missing_value_placeholder": None,
    },
)

The data_masking mapping accepts:

  • context_var: a required ContextVar containing the current account id.
  • prefix: an optional prefix, prefix object, or sequence of prefixes for prefix-scoped masking. Omit it for workspace-wide masking.
  • missing_value_placeholder: an optional value returned when a field read is denied.
  • mode: an optional mode string. It defaults to "RELEASE". Use "DEBUG" only for development and tests.

Set the account id for each request, task, or execution context before accessing protected data:

account_id.set(123)

Field permissions are managed per protected memo class and account. A typical setup defines a field-access enum and grants only the fields an account may use:

@db0.enum(values=["CREATE", "READ", "UPDATE", "DELETE"])
class FieldAccess:
    pass
 
db0.set_field_access(Customer, 123, (FieldAccess.READ,), "name")
db0.set_field_access(Customer, 123, (FieldAccess.CREATE,), "name", "ssn")
 
customer = Customer("Alice", "123-45-6789")
 
assert customer.name == "Alice"
 
# Raises PermissionError, or returns the configured placeholder.
customer.ssn

Use protected fields for application data where field visibility or mutation rights vary by account, tenant, role, or execution context.

Data Filtering Predicates

dbzero-procommercial edition

Data filtering predicates provide a row-level security layer for dbzero objects. They are useful when whole memo objects should be visible only when they match the current account, tenant, role, or other authorization context.

Declare access-controlled memo classes with access_control=True:

import dbzero as db0
 
@db0.memo(access_control=True)
class Document:
    def __init__(self, title: str, body: str):
        self.title = title
        self.body = body

When data filtering is active, dbzero applies the current predicate at application-visible access boundaries. This includes find(), fetch(), deserialized queries, and memo references exposed through object fields or dbzero collections. Objects outside the current predicate are filtered out or denied before application code receives them.

Use this layer for multi-tenancy, per-user grants, role-based visibility, account scoping, and high-granularity security filters.

Activating Data Filtering

Configure data filtering when calling db0.init() with the data_filter option:

from contextvars import ContextVar
import dbzero as db0
 
filter_predicate = ContextVar("filter_predicate")
 
db0.init(
    "/var/lib/my-app/dbzero",
    prefix="main",
    data_filter={
        "context_var": filter_predicate,
        "prefix": "main",
    },
)

The data_filter mapping accepts:

  • context_var: a required ContextVar containing the current db0.predicate(...).
  • prefix: an optional prefix, prefix object, or sequence of prefixes where filtering is enabled. Omit it for workspace-wide filtering.
  • mode: an optional mode string. It defaults to "RELEASE". Use "DEBUG" only for development and tests.

Set the predicate for each request, task, or execution context before querying protected data:

tenant_id = "tenant-a"
 
filter_predicate.set(
    db0.predicate(db0.as_tag("TENANT", tenant_id))
)

Then tag protected objects with the same access relation:

document = Document("Invoice", "...")
db0.tags(document).add(db0.as_tag("TENANT", tenant_id))
 
visible_documents = list(db0.find(Document))
assert visible_documents == [document]

Predicates must be built with db0.predicate(...), not db0.find(...). Predicate objects use the same criteria grammar as find(), but they are not directly iterable, countable, truth-testable, indexable, or sliceable. This prevents predicate construction from becoming a separate data leak.

For higher-granularity policies, combine grant and deny predicates:

filter_predicate.set(
    db0.predicate(
        [
            db0.as_tag("GRANT", account),
            db0.as_tag("GRANT", role),
        ],
        db0.no(db0.as_tag("DENY", account)),
    )
)