aeat.application.bucket_maintenance._sandbox module

Sandbox workspace lifecycle: create, activate, and discard an isolated bucket.

A sandbox is an ordinary profile bucket distinguished only by a reserved operator-visible label prefix (SANDBOX_LABEL_PREFIX). It rides the exact same encrypted per-bucket storage substrate every profile uses (SecureObjectRepository via the bucket-scoped runtime wrapper): there is no plaintext scratch directory and no parallel storage backend. Isolation is the pre-existing per-bucket engine guarantee (see test_per_bucket_engine_isolation); this module adds nothing to that guarantee, it only composes the existing single-writer primitives behind an experiment-lifecycle vocabulary.

The service delegates every write to an existing primitive (composition-service-no-parallel-write-path):

  • register_active_profile() (via the config profile create / duplicate atomic-create span) provisions the sandbox bucket, optionally seeded from a source profile’s live facts — exactly the same fork config profile duplicate already performs. The source bucket is opened read-only (a lifecycle-service read) and is never written to, so seeding cannot mutate main state.

  • select_profile_with_lifecycle_span() activates a named sandbox as the current session (config switch’s primitive).

  • BucketMaintenanceService.delete composes the existing soft-tombstone-then-hard-erase primitives to discard the sandbox bucket entirely.

  • BucketMaintenanceService.archive composes the soft-tombstone-only primitive to move a sandbox into reversible dormancy (the bucket directory, manifest, and encrypted record survive intact); BucketMaintenanceService().restore is its symmetric inverse.

  • BucketMaintenanceService.browse (via a read-only lifecycle session, not the active bucket) backs preview_discard_sandbox(), so an operator can see what a discard would remove before confirming it.

list_sandboxes() is the read-only enumeration shared by sandbox list and sandbox prune — the composition-verb pattern this module follows: a bulk verb composes the same single-bucket primitives per row rather than re-implementing bucket erasure.

The destructive-action protocol mirrors DeleteBucketCommand: a discard requires confirmed=True, refuses the active bucket (the operator switches away first — the existing BucketMaintenanceService.delete contract), and additionally refuses to discard any bucket whose label is not sandbox-tagged unless the caller explicitly acknowledges bypassing that guard — so an operator (or an LLM agent driving the CLI) cannot accidentally erase a real profile through the sandbox verb.

See also

bucket_maintenance

Public facade that exports the sandbox lifecycle commands and results.

BucketMaintenanceService

Existing bucket-maintenance service composed for discard, archive, restore, browse, and disk-usage operations.

register_active_profile()

Profile-create primitive used when a sandbox bucket is provisioned.

select_profile_with_lifecycle_span()

Profile-switch primitive used when a sandbox becomes active.

_sandbox

CLI command surface that translates operator input into these command models.

TransactionCatalogue

Ledger catalogue promoted by sandbox merges when ledger scope is selected.

SANDBOX_LABEL_PREFIX

Reserved operator-visible label prefix identifying a sandbox bucket.

A profile whose label starts with this prefix is a sandbox: created through create_sandbox(), safe to discard without the non-sandbox confirmation escalation. create_sandbox() is the only path this module exposes for minting one, and discard_sandbox() (via is_sandbox_label()) decides whether a bucket may be discarded without the extra confirmation. The token itself is declared in the light aeat.core.external_constants layer so the state-free CLI surface can read it without importing this heavy module; it is re-exported here as the canonical application-facing name.

is_sandbox_label(label)[source]

Return whether label carries the reserved sandbox prefix.

Return type:

bool

Parameters:

label (str)

sandbox_label(name)[source]

Return the reserved sandbox label for operator-chosen name.

Return type:

str

Parameters:

name (str)

exception SandboxAlreadyExistsError(label)[source]

Bases: Exception

Raised when create_sandbox targets a name that already has a manifest.

Parameters:

label (str)

Return type:

None

exception SandboxSourceNotFoundError(name)[source]

Bases: Exception

Raised when create_sandbox’s seed source names an unknown or non-live profile.

Parameters:

name (str)

Return type:

None

exception SandboxNotFoundError(bucket_id)[source]

Bases: Exception

Raised when a sandbox lookup targets an unknown bucket.

Parameters:

bucket_id (str)

Return type:

None

exception SandboxDiscardRefusedError[source]

Bases: Exception

Raised when discarding a bucket is refused by the sandbox destructive-action gate.

exception SandboxNotArchivedError(bucket_id)[source]

Bases: Exception

Raised when restore_sandbox targets a sandbox that is not currently archived.

Parameters:

bucket_id (str)

Return type:

None

exception SandboxMergeRefusedError[source]

Bases: Exception

Raised when merge_sandbox is refused (missing confirmation, unknown scope target).

class SandboxMergeScope(*values)[source]

Bases: StrEnum

Closed axis of promotable data categories a sandbox merge may select.

LEDGER promotes ledger transactions only (the manual/imported Transaction catalogue, which already carries classification decisions on each row). MODELO promotes the modelo work-unit, calculation-revision, and filing-record catalogues. ALL promotes every one of the above in one call.

Ledger promotion reads the sandbox TransactionCatalogue through the concrete TransactionCatalogueRepository and upserts the rows into the target bucket.

LEDGER
MODELO
ALL
class PreviewDiscardSandboxCommand(**data)[source]

Bases: BaseModel

Operator request to preview what a sandbox discard would remove.

Read-only counterpart to DiscardSandboxCommand: it never opens the target bucket for write and never calls BucketMaintenanceService().delete. allow_non_sandbox mirrors the discard command’s escape hatch so a preview against a non-sandbox bucket reports the same “not a sandbox” refusal a real discard would, rather than silently previewing an erase the real verb would never allow.

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • allow_non_sandbox (bool)

bucket_id: BucketId
allow_non_sandbox: bool
class PreviewDiscardSandboxResult(**data)[source]

Bases: BaseModel

Read-only preview of what discarding a sandbox would remove.

namespaces is the same per-namespace row-count inventory BucketMaintenanceService().browse returns for the active bucket, computed here for a bucket that need not be active. is_active flags whether the target is the current session — a real discard of it would be refused until the operator switches away.

Parameters:

data (Any)

bucket_id: BucketId
label: str
is_active: bool
namespaces: tuple[SandboxNamespaceInventoryRow, ...]
class SandboxNamespaceInventoryRow(**data)[source]

Bases: BaseModel

One namespace row in a sandbox discard preview.

Parameters:
  • namespace (str)

  • row_count (int)

namespace: str
row_count: int
class CreateSandboxCommand(**data)[source]

Bases: BaseModel

Operator request to fork an isolated, discardable sandbox bucket.

name is the operator-chosen sandbox name (rendered as sandbox:<name>); from_profile optionally names a live profile whose facts seed the sandbox (an experiment that needs realistic profile data without touching the source). A sandbox is a real profile bucket underneath, so it is bound by the same profile-schema required-field validation config profile create enforces: creating one with no from_profile and no facts fails that validation exactly as an empty config profile create would. Seed from an existing profile to get a realistic, immediately-usable sandbox.

Parameters:
  • name (str)

  • from_profile (str | None)

name: str
from_profile: str | None
class CreateSandboxResult(**data)[source]

Bases: BaseModel

Outcome of a successful sandbox creation. The sandbox becomes the active bucket.

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • label (str)

  • seeded_from (str | None)

bucket_id: BucketId
label: str
seeded_from: str | None
class DiscardSandboxCommand(**data)[source]

Bases: BaseModel

Operator request to permanently erase a sandbox bucket.

confirmed=True is required, mirroring DeleteBucketCommand. allow_non_sandbox must be explicitly set to erase a bucket whose label does NOT carry the reserved sandbox prefix — the default refuses, so a mistaken discard of a real profile is not silently possible.

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • confirmed (bool)

  • allow_non_sandbox (bool)

bucket_id: BucketId
confirmed: bool
allow_non_sandbox: bool
class DiscardSandboxResult(**data)[source]

Bases: BaseModel

Outcome of a successful sandbox discard.

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • previous_label (str)

  • occurred_at (datetime)

bucket_id: BucketId
previous_label: str
occurred_at: datetime
class ArchiveSandboxCommand(**data)[source]

Bases: BaseModel

Operator request to move a sandbox into reversible dormancy.

Unlike DiscardSandboxCommand, archive never removes the sandbox bucket: it composes BucketMaintenanceService().archive (soft tombstone only), so the bucket directory, manifest, and encrypted record all survive intact and restore_sandbox() can bring the same sandbox back. confirmed=True mirrors DiscardSandboxCommand’s boundary contract.

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • confirmed (bool)

  • allow_non_sandbox (bool)

bucket_id: BucketId
confirmed: bool
allow_non_sandbox: bool
class ArchiveSandboxResult(**data)[source]

Bases: BaseModel

Outcome of a successful sandbox archive.

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • label (str)

  • occurred_at (datetime)

bucket_id: BucketId
label: str
occurred_at: datetime
class RestoreSandboxCommand(**data)[source]

Bases: BaseModel

Operator request to bring an archived sandbox back to active status.

Symmetric inverse of ArchiveSandboxCommand. Refuses when the target is not currently archived (tombstoned).

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • allow_non_sandbox (bool)

bucket_id: BucketId
allow_non_sandbox: bool
class RestoreSandboxResult(**data)[source]

Bases: BaseModel

Outcome of a successful sandbox restore.

Parameters:
  • bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • label (str)

  • occurred_at (datetime)

bucket_id: BucketId
label: str
occurred_at: datetime
class MergeSandboxCommand(**data)[source]

Bases: BaseModel

Operator request to promote one data scope from a sandbox into a target profile.

source_bucket_id must carry the reserved sandbox label (the allow_non_sandbox escape hatch mirrors every other sandbox verb’s non-sandbox guard). target_bucket_id names the profile bucket the scope is merged INTO — ordinarily the operator’s main profile, resolved by the CLI layer before this command is built, never the sandbox itself. confirmed=True is required because the merge mutates the target profile’s real records.

Parameters:
  • source_bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • target_bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • scope (SandboxMergeScope)

  • confirmed (bool)

  • allow_non_sandbox (bool)

source_bucket_id: BucketId
target_bucket_id: BucketId
scope: SandboxMergeScope
confirmed: bool
allow_non_sandbox: bool
class MergeSandboxResult(**data)[source]

Bases: BaseModel

Outcome of a successful sandbox merge.

merged_counts reports, per merged typed category, how many rows the merge upserted into the target bucket (a re-run against unchanged sandbox content upserts the same content-addressed ids again — an idempotent no-op write, never a duplicate row).

Parameters:
  • source_bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • target_bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])

  • scope (SandboxMergeScope)

  • merged_counts (dict[str, int])

  • occurred_at (datetime)

source_bucket_id: BucketId
target_bucket_id: BucketId
scope: SandboxMergeScope
merged_counts: dict[str, int]
occurred_at: datetime
create_sandbox(command)[source]

Fork a fresh, isolated sandbox bucket and make it the active profile.

Composes the canonical atomic-create span (profile_create_storage_span() + register_active_profile()) — the exact primitive config profile create and config profile duplicate already use — so the sandbox bucket, its manifest, its encrypted record, and the active-profile pointer land in one all-or-nothing unit of work. When command.from_profile is supplied, the source profile’s live facts are read through a read-only lifecycle session (the source bucket is never opened for write), so seeding cannot mutate main state.

Return type:

CreateSandboxResult

Returns:

CreateSandboxResult describing the new sandbox bucket, which is now the active profile.

Parameters:

command (CreateSandboxCommand)

preview_discard_sandbox(command)[source]

Report what discarding command.bucket_id would remove, without removing it.

Reads the target bucket’s namespace inventory through a read-only lifecycle session (profile_storage_session()) — the identical read-only pattern create_sandbox() already uses to seed from a non-active source profile — so a sandbox need not be the active bucket to be previewed. No write primitive is invoked; the target bucket, its manifest, and its records are untouched.

Applies the same non-sandbox refusal discard_sandbox() applies, so a preview can never suggest an erase the real verb would refuse.

Return type:

PreviewDiscardSandboxResult

Returns:

PreviewDiscardSandboxResult describing the bucket’s current contents.

Parameters:

command (PreviewDiscardSandboxCommand)

list_sandboxes()[source]

Return every (bucket_id, label) pair currently carrying the sandbox prefix.

Read-only enumeration shared by sandbox list and sandbox prune.

Return type:

tuple[tuple[str, str], ...]

discard_sandbox(command)[source]

Permanently erase the sandbox bucket identified by command.bucket_id.

Composes BucketMaintenanceService.delete — the same soft-tombstone-then-hard-directory-removal primitive config profile delete uses — after confirming the target either carries the reserved sandbox label or the caller explicitly set allow_non_sandbox=True. Refuses (via the composed service) to discard the currently active bucket; the operator must switch away first. For a reversible alternative that never erases the bucket, see archive_sandbox().

Return type:

DiscardSandboxResult

Returns:

DiscardSandboxResult describing the erased sandbox.

Parameters:

command (DiscardSandboxCommand)

archive_sandbox(command)[source]

Move the sandbox bucket identified by command.bucket_id into reversible dormancy.

Composes BucketMaintenanceService.archive — the same soft-tombstone-only primitive that leaves the bucket directory, manifest, and encrypted record intact — after confirming the target either carries the reserved sandbox label or the caller explicitly set allow_non_sandbox=True. Refuses (via the composed service) to archive the currently active bucket; the operator must switch away first. Unlike discard_sandbox(), the archived sandbox can be brought back with restore_sandbox().

Return type:

ArchiveSandboxResult

Returns:

ArchiveSandboxResult describing the archived sandbox.

Parameters:

command (ArchiveSandboxCommand)

restore_sandbox(command)[source]

Bring the archived sandbox bucket identified by command.bucket_id back to active.

Composes BucketMaintenanceService.restore — the symmetric inverse of archive_sandbox() — after confirming the target either carries the reserved sandbox label or the caller explicitly set allow_non_sandbox=True. Refuses when the target is not currently archived.

Return type:

RestoreSandboxResult

Returns:

RestoreSandboxResult describing the restored sandbox.

Parameters:

command (RestoreSandboxCommand)

merge_sandbox(command)[source]

Promote command.scope from a sandbox bucket into command.target_bucket_id.

Composes the SAME typed-catalogue repositories and domain upsert primitives the portable-bundle import path (deserialize_profile_bundle()) uses for ledger/work-unit/calculation-revision/filing-record restore (composition-service-no-parallel-write-path): this function does not reimplement a write path, it selects which of those existing upserts to run for the requested scope. Every upsert keys on the row’s own content-addressed or natural id, so re-running a merge against unchanged sandbox content is an idempotent no-op write (the target ends up with the identical row it already had, never a duplicate).

Refuses unless command.confirmed is True. Refuses when command.source_bucket_id is not sandbox-labelled unless the caller explicitly sets command.allow_non_sandbox, mirroring every other sandbox verb’s non-sandbox guard. Refuses when the source and target buckets are identical (nothing to promote).

Return type:

MergeSandboxResult

Returns:

MergeSandboxResult reporting the per-category row counts merged into the target bucket.

Parameters:

command (MergeSandboxCommand)