Metadata-Version: 2.4
Name: hivecat
Version: 0.1.0a1
Summary: Framework-agnostic AI agent runtime with first-class Django integration
Author: Hivecat contributors
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: pydantic<3,>=2.10
Provides-Extra: django
Requires-Dist: django<6.1,>=5.2; extra == 'django'
Provides-Extra: postgres
Requires-Dist: alembic<2,>=1.18.5; extra == 'postgres'
Requires-Dist: asyncpg<0.32,>=0.31; extra == 'postgres'
Requires-Dist: sqlalchemy[asyncio]<2.1,>=2.0.51; extra == 'postgres'
Provides-Extra: sqlite
Requires-Dist: aiosqlite<0.23,>=0.22.1; extra == 'sqlite'
Requires-Dist: alembic<2,>=1.18.5; extra == 'sqlite'
Requires-Dist: sqlalchemy[asyncio]<2.1,>=2.0.51; extra == 'sqlite'
Description-Content-Type: text/markdown

# hivecat

Hivecat is a framework-agnostic, typed AI agent runtime with first-class Django
integration. The project is currently pre-alpha.

The first implementation milestone provides:

- canonical multimodal request and response models;
- capability-aware model routing with priority failover;
- typed Python tools with validation and idempotency support;
- framework-neutral principals and agent-scoped authorization;
- an async-first agent runtime and in-memory state store;
- an isolated `HivecatApp` composition root with async lifecycle ownership;
- secret-free typed configuration for the currently implemented adapters;
- strict trusted-code app loading and a lifecycle-only CLI check;
- a per-application, metadata-first `PluginManager` with exact deployment allowlisting;
- a Core-only canonical Definition draft/published-version foundation for Agent, Workflow, Model,
  Model Route, and Tool identities with checksummed exact dependencies;
- a parallel, Tenant-required general resource authorization service with exact grants;
- an authoritative Tenant membership builder for User/Service actor and Group/Service subjects;
- a subject-free Tenant authorization service backed by aggregate in-memory, durable SQLite, or
  PostgreSQL RLS-protected policy snapshots;
- a trusted bounded Tenant identity bootstrap port with atomic in-memory, SQLite, and
  function-only PostgreSQL implementations;
- membership-generation history, provenance-scoped grants, and an authorized Tenant actor
  first-join/safe-rejoin port with atomic in-memory, SQLite, and function-only PostgreSQL
  implementations;
- operation-specific authorized Tenant membership suspend, resume, terminal revoke, and
  non-widening expiry commands with exact-current CAS across in-memory, SQLite, and function-only
  PostgreSQL implementations;
- generation-tracked Group/Service subject assignment, explicit regrant, suspend, resume,
  terminal revoke, and non-widening expiry commands across the same three Store boundaries;
- an explicit User-only `tenant.administrator` policy with trusted opt-in initialization,
  zero-anchor recovery, role-plus-exact-grant governance, and structural last-anchor protection;
- a freshly authorized exact-Agent managed-grant query with bounded revision-fenced pagination,
  opaque minimal summaries, and memory, SQLite, and separately credentialed PostgreSQL readers;
- additive async exact Definition storage and active AgentVersion read ports, with managed SQLite
  and hardened PostgreSQL persistence for immutable Agent -> Route -> Model chains and the
  authorized Tenant-wide activate/rebind/deactivate/reactivate lifecycle;
- a write-only, bounded exact-resource/grant provisioning port with atomic in-memory, SQLite, and
  dedicated PostgreSQL implementations;
- an atomic authorized bound-Run launch port with managed SQLite and dedicated PostgreSQL
  implementations that couple fresh source-policy evaluation, exact Run/result policy creation,
  and the complete initial Run aggregate;
- optional async SQLAlchemy SQLite and PostgreSQL authorization/coordination Stores with packaged
  Hivecat-owned Alembic migrations;
- an identifier-only `WorkerRuntime` with token-free heartbeat, explicit retry outcomes, and an
  opt-in protected Artifact-result path that keeps receipts internal and returns only status;
- an optional durable non-retry failure terminalization Store whose confirmed intent fences
  lease-expiry redispatch and supports idempotent database reconciliation;
- Store-prepared broker delivery identities, append-only outcome evidence, and ambiguity-safe
  identified-backend dispatch;
- Tenant-scoped durable Run cancellation with fenced Node/dispatch invalidation and a separately
  leased, best-effort broker revoke outbox;
- opaque immutable Artifact references, private digest-verifying local object storage, and
  staged/ready/quarantined Tenant-scoped SQL metadata;
- additive trusted bound-Run creation with immutable initiating actor, source action/resource,
  Run/result ownership/resource identities, and explicit legacy-unbound lookup;
- protected optional per-Node Artifact result commits that freshly reauthorize the immutable Run
  binding and atomically fence completion and successor scheduling;
- opt-in immutable predecessor-result binding that reconstructs fresh handler arguments from the
  Run input snapshot and exact direct-predecessor `ArtifactRef` identities without widening queues.
- exact current-viewer Node-result reads that re-evaluate current User/Service, Group, grant, deny,
  resource, and expiry state before returning one completed `READY` `ArtifactRef`.

## Installation

When a release is available from PyPI:

```bash
pip install hivecat
```

The currently implemented optional extras are independent and selectable:

```bash
pip install "hivecat[django]"
pip install "hivecat[sqlite]"
pip install "hivecat[postgres]"
```

`sqlite` provides durable exact Definition versions and Tenant-scoped AgentVersion binding
lifecycle state, local Tenant identity bootstrap, authorized actor first join/safe
rejoin, authorized membership and subject-assignment lifecycle, explicit Tenant-administrator
policy governance, authorization, and Artifact metadata, fenced
node-command/outbox state, immutable Run access bindings and Definition/Input execution snapshots,
atomic authorized bound-Run launch, Store/database-enforced optional or required per-Node Artifact
results, exact-claim predecessor `ArtifactRef` binding, durable Node failure-terminalization intents,
append-preserving broker delivery and revoke evidence, Tenant-scoped cancellation, and migration
resources. Its coordination Store can
be consumed explicitly through the Core `WorkerRuntime`, including its optional protected-result
Store seam, but it is not composed into `WorkflowEngine` or `HivecatApp`; it does not claim
complete durable Workflow execution, production Tenant isolation, or exactly-once delivery.
`postgres` provides RLS-protected exact Definition versions and separately credentialed
function-only AgentVersion registration, resolution, binding lifecycle, and active reads, plus the
read-only composite authorization Store and trusted function-only
fresh-Tenant identity-bootstrap, existing-Tenant actor-onboarding, membership-lifecycle,
subject-assignment-lifecycle, separated Tenant-administrator initialization, recovery, and
authorized-governance adapters, exact function-only managed Tenant Agent access writes and a
separately credentialed function-only managed-access query, separate
resource/grant provisioning, and authorized bound-Run launch adapters,
PostgreSQL 16-18 RLS migrations, an `asyncpg` engine boundary, durable run/dispatch coordination,
Tenant-scoped cancellation, RLS-protected Artifact metadata and Run access bindings, and narrow
worker/dispatcher/revoker functions. A dedicated result adapter links an exact `READY` Artifact
under fresh policy, lease, claim, cancellation, and Artifact-lifecycle fences, and an explicit
WorkerRuntime can compose it without broadening the separate worker/result credentials. A separate
exact-claim loader can expose fresh isolated copies of a durably immutable Definition/Input
snapshot and a complete direct-predecessor result projection only to trusted versioned execution
code. A separate function-only terminalization adapter can prepare and reconcile confirmed
non-retry failures without widening the worker credential. Membership administration now covers
exact first join/safe rejoin plus atomic suspend, resume, terminal revoke, and non-widening expiry;
Group/Service assignment administration adds generation-tracked first assignment, explicit
regrant, suspend, resume, revoke, and expiry. Broader principal and grant lifecycle, complete
Run/Audit
state, canonical Run output, and
app/Django adoption remain outside this slice. A separate exact
read-only adapter exposes one currently authorized completed Node's `READY` `ArtifactRef`; it does
not expose bytes or list Run results.
Celery, Taskiq, Provider SDKs, and observability integrations are not advertised as extras until
their adapters and clean-install contract tests pass the roadmap acceptance gates.

## Development

```bash
uv sync --extra django --extra sqlite --extra postgres
uv run ruff format --check src tests
uv run ruff check src tests
uv run mypy src
uv run pytest -q
uv run python -m django check --settings=tests.django_settings
uv run python -m django makemigrations --check --dry-run --settings=tests.django_settings
```

`uv sync` installs this checkout into the development environment and registers the
`hivecat` console script. That is why `uv run hivecat` works before a PyPI release; the
current CLI exposes help, version information, and an application construction/lifespan check.

## Documentation

Start with the [`docs/` index](docs/README.md). It links the detailed architecture and
roadmap as well as the repository-local engineering harness used for multi-file features,
migrations, providers, backends, and release preparation.

The resolved framework-neutral runtime, backend, model, tenancy, BYOK, sandbox, and PyPI
delivery roadmap is in [`implementation_plan.md`](implementation_plan.md).
Accepted ownership, security, and compatibility decisions are indexed in
[`docs/adr/`](docs/adr/README.md).

## Core API compatibility

New code should use `hivecat.backends.InlineBackend`, `hivecat.workflow.DagPlanner`, and
`hivecat.providers.TextGenerationProvider`. The historical `LocalBackend`,
`workflow.scheduler.WorkflowScheduler`, and `ModelProvider` names remain available as compatible
0.x shims; the change is additive and emits no hot-path deprecation warning.

## Immutable Definition versions

`hivecat.definitions` provides the shared publication primitive required before durable workers
can safely load exact Agent, Workflow, Model, Model Route, or Tool versions. A
`DefinitionDraft` owns a bounded canonical JSON document and exact same-Tenant dependency
references. Publishing it creates an immutable `DefinitionVersion` whose checksum binds the
Tenant, kind, ID, version, dependencies, and document. `InMemoryDefinitionVersionRegistry`
accepts identical replay, rejects a conflicting same-version document, and resolves only a
complete `DefinitionVersionRef`.

The low-level example below demonstrates only the generic canonical envelope. It is not a
domain-valid Workflow definition and must not be passed directly to a domain runtime:

```python
from hivecat.definitions import (
    DefinitionDraft,
    DefinitionKind,
    InMemoryDefinitionVersionRegistry,
)

draft = DefinitionDraft.create(
    tenant_id="tenant-1",
    kind=DefinitionKind.WORKFLOW,
    definition_id="raw-workflow-envelope",
    revision=1,
    document={"note": "generic envelope only"},
)
version = draft.publish(version=1)

registry = InMemoryDefinitionVersionRegistry()
reference = registry.register(version)
loaded = registry.resolve(reference)
assert loaded.reference == reference
```

The explicit publication modules add closed Model, Model Route, and Agent schemas. A Route pins
each candidate by its complete Model version reference, and an Agent pins its default Route. Both
dependency layers validate through an exact catalog:

```python
from hivecat.agent.publication import AgentVersionDraft
from hivecat.definitions import InMemoryDefinitionVersionRegistry
from hivecat.models import Modality
from hivecat.models.publication import (
    ModelDefinitionDraft,
    ModelOperation,
    ProviderPluginRef,
    PublishedModelCapabilities,
)
from hivecat.routing.publication import (
    ModelRouteCandidateSpec,
    ModelRouteDraft,
    ModelRouteFailoverPolicy,
    ModelRouteRequirements,
)

catalog = InMemoryDefinitionVersionRegistry()
model = ModelDefinitionDraft.create(
    tenant_id="tenant-1",
    definition_id="support-model",
    revision=1,
    operation=ModelOperation.TEXT_GENERATION,
    provider_plugin=ProviderPluginRef(
        entry_point_group="hivecat.providers.v1",
        entry_point_name="text-provider",
        api_version=1,
    ),
    provider_model_name="support-model-v1",
    capabilities=PublishedModelCapabilities(
        input_modalities=(Modality.TEXT,),
        output_modalities=(Modality.TEXT,),
    ),
).publish(version=1)
model_ref = catalog.register(model.definition_version)

route = ModelRouteDraft.create(
    tenant_id="tenant-1",
    definition_id="support-route",
    revision=1,
    operation=ModelOperation.TEXT_GENERATION,
    requirements=ModelRouteRequirements(
        input_modalities=(Modality.TEXT,),
        output_modalities=(Modality.TEXT,),
    ),
    candidates=(ModelRouteCandidateSpec(model=model_ref, priority=1),),
    failover=ModelRouteFailoverPolicy(max_total_attempts=1),
).publish(version=1, catalog=catalog)
catalog.register(route.definition_version)

agent = AgentVersionDraft.create(
    tenant_id="tenant-1",
    agent_id="support-agent",
    revision=1,
    name="Support",
    system_instruction="Be concise.",
    max_turns=1,
    default_model_route=route,
).publish(version=1, catalog=catalog)
agent_ref = catalog.register(agent.definition_version)
```

Publication does not discover or load a Provider plugin, resolve a Connection or Credential,
authorize an actor, activate or bind an Agent for a Tenant, choose a live Model, or execute a Run.
The legacy Agent runtime and Router do not consume these versions implicitly. The additive
`AsyncDefinitionVersionStore` persists exact immutable envelopes, while
`StoredAgentVersionResolver` reloads and typed-validates a stored Agent -> Route -> Model chain in
a fresh local registry. Managed SQLite and hardened PostgreSQL implementations are available from
the storage extras; they do not turn trusted registration into end-user publish authority.

Workflow and Tool domain publishers and runtime selection remain follow-up work. Durable storage
and the authorized lifecycle described below do not make publication itself an authorization
boundary. A system instruction is private Agent configuration but is not encrypted secret storage.
Existing 0.x definition types and registries remain compatible and are not silently treated as
published versions. Hosts must keep credentials, personal information, and operational secrets out
of Definition documents.
See [`ADR 0031`](docs/adr/0031-canonical-definition-version-foundation.md) and its additive Model
extension in [`ADR 0032`](docs/adr/0032-model-and-route-publication.md), followed by the Tool-free
Agent contract in [`ADR 0033`](docs/adr/0033-agent-version-publication.md).

## Published Agent binding, lifecycle, and safe discovery

The explicit in-memory binding catalog lets trusted host composition adopt one exact registered
Agent version for a stable Tenant Agent identity. Registration re-resolves the Agent through the
same Definition catalog and revalidates its complete Route/Model chain:

```python
from hivecat.agent.version_binding import InMemoryTenantAgentVersionBindingCatalog

bindings = InMemoryTenantAgentVersionBindingCatalog(definition_catalog=catalog)
bindings.register(agent_version=agent)

binding = bindings.get(tenant_id="tenant-1", agent_id="support-agent")
assert binding.agent_version_ref == agent_ref
```

`TenantAgentVersionService` is available from the explicit
`hivecat.access.tenant_agent_version_service` module. After the stable Agent authorization
resource and User/Group grants have been provisioned, its `list_agents()` and `get_agent()` methods
freshly require `agent.view` and return only `(agent_id, name, version)` summaries. It authorizes
each list candidate before resolving its Definition, so denied bindings cause no Agent document
probe. `admit_run()` freshly requires `agent.run`, revalidates the exact Agent -> Route -> Model
chain, and returns only `None`.

This catalog remains an in-memory trusted first-registration seam. Exact replay is accepted, while
a different version for the same stable Agent conflicts. The separate explicit
`hivecat.access.tenant_agent_version_lifecycle` module provides an authorized lifecycle without
changing that trusted catalog. Its local
`InMemoryAuthorizedTenantAgentVersionBindingStore` requires an effective current User
`tenant.administrator` and fresh exact `agent.manage` on the stable Agent resource, then performs
the allow decision and binding CAS while holding the same authorization aggregate lock. The role
does not grant `agent.manage`, deny still wins, and `agent.manage` is not one of the `agent.view`/
`agent.run` actions that the managed-access API may delegate.

First activation, rebind, deactivation, and reactivation retain bounded append-only binding
history with exact binding-ID/generation/version evidence. Reactivation is an explicit rebind from
the inactive current record. Only the current active record appears through the existing binding
catalog, so `TenantAgentVersionService` keeps the same safe-summary and non-capability preflight
contract across transitions. The selected version is shared by the Tenant's stable Agent identity;
it is not a per-user version or ownership setting. User and Group `agent.view`/`agent.run` grants
still decide who can discover or pass preflight for that shared active version.

The same lifecycle has managed SQLite and hardened function-only PostgreSQL adapters. Durable state
uses append-only exact history, one current pointer, and a revision scoped to the Tenant;
restarting a process does not require a latest-version fallback or legacy Django backfill. The
safe service can consume the additive async Definition and active-binding readers and still
reauthorizes every request. PostgreSQL registration, Definition resolution, lifecycle, and active
reads use separate exact deployment credentials; migrations create no login or grant.

The optional request-bound Django facade derives its manager and Tenant from trusted request
context and adds no Django model, migration, or dual-write. Physical delete, personal
ownership/private drafts, publish authority, and AgentVersion execution remain deferred. No
summary includes the system instruction, canonical document, checksum, dependency references, or
lifecycle history, and no result is a reusable capability. See
[`ADR 0034`](docs/adr/0034-tenant-agent-version-discovery.md) and its lifecycle extension in
[`ADR 0035`](docs/adr/0035-agent-version-binding-lifecycle.md), followed by the durable-state
decision in [`ADR 0036`](docs/adr/0036-durable-agent-version-state.md).

## General resource authorization

`ResourceRef`, `AccessRequest`, `AuthorizationDecision`, and `AuthorizationService` provide a
framework-neutral foundation for exact, Tenant-scoped authorization. The service reads an
authoritative resource-and-grant snapshot on every decision, applies explicit deny before
matching allows, and exposes generic outward denials without treating a previous decision as a
reusable capability.

The lower-level `AccessRequest` constructor still treats additional Group or Service subjects as a
trusted-host input. `TenantMembershipAuthority` adds a stricter producer: it accepts a
Tenant-neutral `PrincipalIdentityRef` and an exact resource, then derives the actor and complete
subject set from versioned Tenant, principal, membership, and assignment records. Callers cannot
pass a subject list to that builder. Hivecat does not infer roles, ownership, administrator status,
nested groups, or wildcard access. New general resources always require an explicit Tenant;
tenantless standalone behavior remains on the existing Agent compatibility path.

Memberships now keep immutable replacement order through `generation` and an exact predecessor.
Only one generation is current for a Tenant/actor. `ResourceGrant.subject_scope` makes matching
provenance explicit: `ACTOR_MEMBERSHIP` binds a direct User/Service grant to one exact membership
ID, `ASSIGNED_SUBJECT` matches only an active Group/Service assignment, and `PRINCIPAL_DENY` is a
generation-independent explicit deny. Existing rows are `LEGACY`: in membership-evidence-bearing
composite paths their allows work only for generation one, while their denies remain fail-closed
after rejoin. The low-level omitted-evidence call shape remains a 0.x compatibility path and must
not be used to implement rejoin. Direct and assignment-derived Service subjects are therefore
never interchangeable.

`TenantAuthorizationService` is the subject-free high-level boundary. Its public `decide()` and
`require()` methods accept only a Tenant-neutral User/Service actor, an exact action, and an exact
`ResourceRef`. Each call asks one `TenantAuthorizationStore` for a complete, consistent snapshot of
membership, assignments, target principals, resource state, and exact-action grant candidates,
then evaluates all expiry using one trusted timestamp. The caller cannot supply subjects, grants,
an `AccessRequest`, a decision, or an evaluation time. The complete contract and aggregate
`InMemoryTenantAuthorizationStore` are exported from `hivecat.access`; the service and decision are
also available from the package root.

The in-memory implementations remain development and contract-test foundations. The optional
`SQLAlchemyTenantAuthorizationStore` persists the same composite policy in SQLite and reads each
decision from one explicit database transaction. The optional
`PostgreSQLTenantAuthorizationStore` reads the same complete snapshot in one read-only
repeatable-read transaction after an independently trusted request Tenant has been matched and
installed as transaction-local RLS context. A low-level `AccessRequest` and every composite
decision are still ordinary evidence for one instant, never reusable capabilities. These
snapshot Stores do not make authorization atomic with a later mutation. `TenantAgentService` now
consumes the composite service for fresh discovery, direct view, and a run preflight, but no Agent
execution, Django, worker, or HTTP runtime consumes it yet. The separately composed launch Store
below remains the first bounded SQL mutation consumer of the policy tables.

`TenantIdentityBootstrapSubject`, `TenantIdentityBootstrapPlan`,
`TenantIdentityBootstrapReceipt`, and `TenantIdentityBootstrapStore` add the trusted global
administration boundary that prepares one fresh Tenant and one initial User or Service actor. A
plan may include up to 128 unique Group or Service assignments. New identity rows are active,
non-expiring version-one records; relation is derived from subject kind, and the plan cannot supply
lifecycle, versions, resources, grants, or Django objects. One material create advances the global
bootstrap fence once and initializes the Tenant policy revision at `1`. An exact replay returns an
unchanged receipt. For an existing Tenant, exactness covers only the planned actor membership
graph and requires no extra current or historical assignment row on that planned membership;
unrelated other-actor memberships, resources, and grants remain untouched and are ignored. The
call never repairs or appends missing planned rows. Bootstrap grants no action, so all resource
access remains explicit and default-deny.

`TenantActorOnboardingPlan`, `TenantActorOnboardingSubject`,
`TenantMembershipGenerationRef`, and `AuthorizedTenantActorOnboardingStore` add the bounded
existing-Tenant administration path. The Store always authorizes the administrator against
`ResourceRef(tenant_id, "tenant_identity", tenant_id)` and exact action `actor.onboard` inside the
same lock or transaction as the write. There is no owner, role, Django permission, or superuser
bypass. Administrator and target must be distinct User/Service identities, and requested
Group/Service subjects are bounded to 128 unique entries and must already exist as active
principals. Bootstrap creates neither the
Tenant control resource nor this grant; trusted administration must provision both explicitly
before onboarding can succeed.

```python
from hivecat.access import (
    PrincipalIdentityRef,
    PrincipalKind,
    TenantActorOnboardingPlan,
    TenantActorOnboardingSubject,
)
from hivecat.storage.sqlalchemy import SQLAlchemyAuthorizedTenantActorOnboardingStore

onboarding = SQLAlchemyAuthorizedTenantActorOnboardingStore(engine)
receipt = await onboarding.onboard_tenant_actor(
    TenantActorOnboardingPlan(
        tenant_id="tenant-1",
        administrator=PrincipalIdentityRef(
            namespace="host", subject_id="admin-1", kind=PrincipalKind.USER
        ),
        actor=PrincipalIdentityRef(
            namespace="host", subject_id="user-2", kind=PrincipalKind.USER
        ),
        membership_id="membership-user-2-g1",
        subjects=(
            TenantActorOnboardingSubject(
                assignment_id="assignment-user-2-group-1",
                subject=PrincipalIdentityRef(
                    namespace="host", subject_id="group-1", kind=PrincipalKind.GROUP
                ),
            ),
        ),
    )
)
```

A first join has no predecessor. Safe rejoin instead supplies `replaces=` with the exact current
`TenantMembershipGenerationRef` and fresh membership/assignment IDs; only a revoked membership or
an active membership already expired at Store time can be replaced. An active non-expired or
suspended membership, stale predecessor, fork, collision, partial graph, or self-rejoin conflicts
without mutation. Rejoin copies no old assignment or grant, and one material call advances the
policy revision once; exact replay freshly reauthorizes and advances it zero times. Other
principal, assignment, and grant lifecycle, bulk onboarding, identity-provider synchronization,
and Django/app wiring remain deferred.

`TenantMembershipLifecyclePlan`, `TenantMembershipLifecycleOperation`, and
`AuthorizedTenantMembershipLifecycleStore` add the separate exact-current lifecycle boundary. The
Store derives the same Tenant identity control resource and one operation-specific action:
`actor.suspend`, `actor.resume`, `actor.revoke`, or `actor.expire`. Administrator and target must
be distinct User/Service identities, and authorization is re-evaluated inside the same lock or SQL
transaction before any target membership probe.

```python
from hivecat.access import (
    PrincipalIdentityRef,
    PrincipalKind,
    TenantMembershipGenerationRef,
    TenantMembershipLifecycleOperation,
    TenantMembershipLifecyclePlan,
)
from hivecat.storage.sqlalchemy import SQLAlchemyAuthorizedTenantMembershipLifecycleStore

lifecycle = SQLAlchemyAuthorizedTenantMembershipLifecycleStore(engine)
receipt = await lifecycle.transition_tenant_membership(
    TenantMembershipLifecyclePlan(
        tenant_id="tenant-1",
        administrator=PrincipalIdentityRef(
            namespace="host", subject_id="admin-1", kind=PrincipalKind.USER
        ),
        actor=PrincipalIdentityRef(
            namespace="host", subject_id="user-2", kind=PrincipalKind.USER
        ),
        membership=TenantMembershipGenerationRef(
            membership_id="membership-user-2-g1", generation=1, version=1
        ),
        operation=TenantMembershipLifecycleOperation.SUSPEND,
    )
)
```

Suspend and resume move only unexpired active/suspended membership. Revoke is terminal and works
regardless of expiry. `SET_EXPIRY` preserves status and can add or shorten, but never remove or
extend, expiry. A material command increments the membership version and policy revision once;
an already-requested exact-current state is unchanged, while retrying the old pre-mutation
reference conflicts. Assignments, grants, generation, lineage, and the current pointer are never
changed. See the detailed [authorization guide](docs/authorization.md) and
[`ADR 0026`](docs/adr/0026-authorized-tenant-membership-lifecycle.md).

`TenantSubjectAssignmentPlan`, `TenantSubjectAssignmentLifecyclePlan`, and
`AuthorizedTenantSubjectAssignmentStore` provide post-onboarding Group/Service administration.
The Store derives one exact action for first assignment, regrant, suspend, resume, revoke, or
expiry and always authorizes against the Tenant identity control resource before probing the
target. Administrator and target are distinct User/Service identities; the relation is derived
from an existing active Group/Service subject.

```python
from hivecat.access import (
    PrincipalIdentityRef,
    PrincipalKind,
    TenantMembershipGenerationRef,
    TenantSubjectAssignmentOperation,
    TenantSubjectAssignmentPlan,
)
from hivecat.storage.sqlalchemy import SQLAlchemyAuthorizedTenantSubjectAssignmentStore

assignments = SQLAlchemyAuthorizedTenantSubjectAssignmentStore(engine)
receipt = await assignments.assign_tenant_subject(
    TenantSubjectAssignmentPlan(
        tenant_id="tenant-1",
        administrator=PrincipalIdentityRef(
            namespace="host", subject_id="admin-1", kind=PrincipalKind.USER
        ),
        actor=PrincipalIdentityRef(
            namespace="host", subject_id="user-2", kind=PrincipalKind.USER
        ),
        membership=TenantMembershipGenerationRef(
            membership_id="membership-user-2-g1", generation=1, version=1
        ),
        assignment_id="assignment-user-2-group-1",
        subject=PrincipalIdentityRef(
            namespace="host", subject_id="group-1", kind=PrincipalKind.GROUP
        ),
        operation=TenantSubjectAssignmentOperation.FIRST_ASSIGN,
    )
)
```

First assignment creates generation one. Regrant requires a fresh ID and the exact current revoked
assignment predecessor, then creates the next generation. Regrant has its own permission because
it can make unchanged `ASSIGNED_SUBJECT` grants effective again; the Store never copies or edits a
grant. Lifecycle transitions use exact assignment ID/generation/version CAS, terminal revoke, and
finite non-widening expiry. The fixed administrator role does not change this assignment
boundary's exact-grant delegation. See
[ADR 0027](docs/adr/0027-authorized-tenant-subject-assignment-lifecycle.md).

`TenantAdministratorInitializationPlan`, `TenantAdministratorRecoveryPlan`, and
`TenantAdministratorTransitionPlan` provide a separate opt-in governance boundary. The only role
is `tenant.administrator` definition version 1, only a User may hold it, and the role is an
eligibility/anchor marker rather than a permission bundle. Normal appointment, reappointment, or
revocation therefore requires both an effective administrator role and the operation's exact
grant on the Tenant identity control resource; explicit deny still wins.

```python
from hivecat.access import (
    PrincipalIdentityRef,
    PrincipalKind,
    TenantAdministratorInitializationPlan,
    TenantMembershipGenerationRef,
)

# `administrator_store` is an in-memory or SQL implementation, and the Tenant,
# User principal, and exact current non-expiring membership already exist.
receipt = await administrator_store.initialize_tenant_administrator(
    TenantAdministratorInitializationPlan(
        tenant_id="tenant-1",
        actor=PrincipalIdentityRef(
            namespace="host", subject_id="admin-1", kind=PrincipalKind.USER
        ),
        membership=TenantMembershipGenerationRef(
            membership_id="membership-admin-1-g1", generation=1, version=1
        ),
        binding_id="tenant-admin-admin-1-g1",
    )
)
```

Initialization never creates an identity, resource, or grant and is never inferred from
bootstrap, existing grants, Django ownership, permissions, or `is_superuser`. Existing Tenants
remain uninitialized until this trusted call succeeds. Once active, supported role, membership,
principal, and Tenant writes cannot leave zero structural anchors. Recovery is trusted and works
only when the coherent anchor count is already zero; it cannot force-replace a surviving anchor.
The policy does not guarantee a usable external login or exact grant, so operators repair grant/
deny lockout through the separate trusted provisioning boundary. See
[`ADR 0028`](docs/adr/0028-tenant-administrator-policy.md).

`TenantAgentAccessCreatePlan` and `TenantAgentAccessRevokePlan` add a bounded User/Group
Agent-access lifecycle. The acting User must be a current effective `tenant.administrator`, have
exact `agent.assign` on the already-provisioned Agent resource, and have every `agent.view` or
`agent.run` action being delegated as ALLOW. The role grants no Agent permission by itself, deny
still wins, and a User administrator cannot grant access to itself.

```python
from hivecat.access import (
    GrantEffect,
    PrincipalIdentityRef,
    PrincipalKind,
    TenantAgentAccessCreatePlan,
)

receipt = await agent_access_store.create_tenant_agent_access(
    TenantAgentAccessCreatePlan(
        tenant_id="tenant-1",
        administrator=PrincipalIdentityRef(
            namespace="host", subject_id="admin-1", kind=PrincipalKind.USER
        ),
        agent_id="support-agent",
        grant_id="support-agent-user-2-access-1",
        subject=PrincipalIdentityRef(
            namespace="host", subject_id="user-2", kind=PrincipalKind.USER
        ),
        actions=frozenset({"agent.view", "agent.run"}),
        effect=GrantEffect.ALLOW,
    )
)
```

User ALLOW is bound to the exact current membership, Group ALLOW follows current Group assignment,
and managed DENY uses principal scope. Create is fresh version one; revoke is exact-current and
terminal. Revision `0019_agent_access_management` preserves append-only managed transition/action
history while the existing grant tables remain the authorization projection. Trusted provisioning
remains a separate authority: it can intentionally change that projection, after which managed
revoke detects the history mismatch and fails closed.

`TenantAgentAccessQuery` is a separate read contract for one exact Agent. Every page freshly
requires the current User's effective administrator role and exact `agent.assign` permission
before it checks the cursor or managed history. Results contain only exact revoke references,
opaque User/Group identities, actions, effect, expiry, and stored active state. They exclude
legacy or trusted-provisioned grants, display names, authorization evidence, and totals. Grant IDs
use deterministic keyset order; a cursor binds the current policy revision, so a policy write
between pages requires a restart.

```python
from hivecat.access import PrincipalIdentityRef, PrincipalKind, TenantAgentAccessQuery

page = await agent_access_query_store.query_tenant_agent_access(
    TenantAgentAccessQuery(
        tenant_id="tenant-1",
        administrator=PrincipalIdentityRef(
            namespace="host", subject_id="admin-1", kind=PrincipalKind.USER
        ),
        agent_id="support-agent",
        limit=64,
    )
)
for access in page.summaries:
    print(access.grant, access.subject, access.actions, access.active)
```

`InMemoryTenantAgentCatalog` binds each code-defined Agent to an explicit Tenant.
`TenantAgentService.list_agents()` applies fresh `agent.view`; `get_agent()` and `admit_run()`
authorize before catalog lookup and hide missing versus denied IDs. `get_agent()` returns a copied
definition after view authorization. `admit_run()` is a preflight-only check that returns `None`;
it returns no definition or reusable capability and does not call a model, choose a
Route/Credential, execute a Tool, or create a Run. A later execution boundary must freshly
authorize again. See
[`ADR 0029`](docs/adr/0029-tenant-agent-access-management.md).

The SQLite `SQLAlchemyTenantAgentAccessStore` implements the separate query protocol alongside its
managed writer. PostgreSQL keeps `PostgreSQLAuthorizedTenantAgentAccessStore` and
`PostgreSQLAuthorizedTenantAgentAccessQueryStore` on different exact function-only credentials.
Both PostgreSQL adapters accept independently trusted Tenant context; packaged migrations create
no login, role, or deployment grant. Apply the packaged Alembic head explicitly and follow
[`docs/storage.md`](docs/storage.md) before use.

The optional Django extra now has a separate adapter-only bridge to this authority.
`DjangoPrincipalIdentityMapper` maps the configured custom User and Django auth Group to
opaque Tenant-neutral Core identities using an explicit deployment key and HMAC-SHA256, while
`DjangoTenantAgentService`, `DjangoTenantAgentAccessManager`,
`DjangoTenantAgentAccessReader`, `DjangoTenantAgentVersionService`, and
`DjangoTenantAgentVersionManager` delegate to injected Core services and Stores. Every call reads
the authenticated User and Tenant from one injected `DjangoRequestContextProvider`; the public
methods accept neither a caller-selected actor/administrator nor `tenant_id`. User/Group target
PKs are canonicalized without an ORM lookup and raw PKs are never placed in Core identity records.
The bridge never interprets `owner`, `is_staff`, `is_superuser`, Django permissions,
`user.groups`, or the legacy Django grant tables as Core authority. Core principals, memberships,
Group assignments, Agent resources, and administrator permissions must already have been
explicitly provisioned.

`TenantResourceProvisioningPlan`, `TenantResourceProvisioningReceipt`, and the write-only
`TenantResourceProvisioningStore` add one deliberately narrow mutation boundary. A plan targets
exactly one already-provisioned Tenant, contains at least one record, and is limited to 128 exact
resources and 128 exact grants; every record must use that Tenant and duplicate logical keys are
rejected. Plans own and sort their inputs, and plans and receipts hide their values from `repr`.
A material batch advances the policy revision exactly once, while an identical replay returns an
unchanged receipt without advancing it. The in-memory Store applies the batch under one lock,
SQLite uses one write transaction, and PostgreSQL uses a separate RLS-bound provisioning Store.
This port does not create a Tenant, principal, membership, assignment, implicit owner grant, or
public administration surface.

`AuthorizedBoundRunLaunchPlan`, `AuthorizedBoundRunLaunchReceipt`, and
`AuthorizedBoundRunLaunchStore` add a narrower current-policy launch boundary under
`hivecat.coordination`. The plan supplies a `RunPlan`, immutable `RunAccessBinding`, an exact
`launch_action`, and one provisioning plan containing exactly the two fresh active version-one
Run/result resources and exactly one active, non-expiring, version-one owner `ALLOW` grant with
nonempty exact actions for each target. Both owner grants use `ACTOR_MEMBERSHIP` and bind to the
freshly authorized initiator membership ID. The launch action authorizes the source resource now; it
is deliberately separate from the binding's `result_commit_action`, which the protected-result
Store re-evaluates at the later result boundary. The launch Store evaluates current source policy
before looking for a Run or target collision, provisions the exact policy, advances the policy
revision once, and
creates the Run, binding, Nodes, edges, and first dispatch in one transaction. Denials are generic
and every denial, conflict, or write failure leaves no partial launch state.

`ProtectedNodeResultCommand`, `NodeResultRecord`, `ProtectedNodeResultReceipt`, and
`ProtectedNodeResultStore` add the result mutation boundary under `hivecat.results`. The command
contains only the exact `NodeLease` and exact `ArtifactRef`. The Store reloads the immutable Run
binding, freshly evaluates its stored actor/action/source tuple while holding the current Tenant
policy fence, and requires the exact active claim/lease, non-cancelled running Run, and same-Tenant
exact-digest `READY` Artifact. One transaction inserts the immutable result, completes the Node,
queues eligible successors, inserts their dispatch rows, and advances or completes the Run.
Missing, unbound, denied, stale, cancelled, duplicate, or Artifact-ineligible state is one generic
`ProtectedResultRejectedError`.

The explicit Tenant Agent service now consumes the general authorization authority for discovery
and run admission. The optional Django bridge can supply its authenticated identity and trusted
Tenant context, but `AgentRuntime`, the tenantless `AgentService`, `HivecatApp`, and the legacy
Django grant models still use the existing Agent-specific authorization path.
Exact current-viewer Node-result exposure is available through a separate Core Store and SQL
adapters. Actual Agent launch/execution, Route/Credential/Tool enforcement, broader role/limit
evaluation, identity/group lifecycle synchronization, managed-grant Admin, and
migration of the remaining runtime/Django boundaries remain roadmap work. See
[`docs/authorization.md`](docs/authorization.md) for the exact policy boundary and
[`docs/storage.md`](docs/storage.md) for SQL installation, migrations, role requirements, and
operational limits.

## Artifact foundation

`hivecat.artifacts` separates an immutable content identity from its private bytes and durable
metadata. `ArtifactRef` contains only an opaque non-path ID and lowercase SHA-256; it is not a
download capability and never carries a local path, object key, URL, credential, or payload.
`LocalFilesystemObjectStore` streams to a private staging file, enforces the declared byte count,
verifies SHA-256, and publishes without replacing an existing object.
Its configured root must already exist and be private to the service account. On POSIX, the root
must have no Group/other permission bits; shard directories and files are created as `0700` and
`0600`. On Windows, provision a service-only root ACL before startup. POSIX directory `fsync` and
Windows write-through no-replace publication make the returned receipt the local durability
barrier.

The SQL repositories keep exact Tenant, owner, media type, size, storage namespace, lifecycle,
version, and Store-owned timestamps. Content starts `STAGED`, then an exact stored-object receipt
can move it to `READY`; trusted inspection can move staged or ready metadata to
`QUARANTINED`. SQL and ObjectStore writes are intentionally separate, so staged records and
private orphan objects are explicit recovery/GC inputs rather than hidden atomicity claims.

```python
from collections.abc import AsyncIterator
from hashlib import sha256
from os import environ
from pathlib import Path
from secrets import token_hex

from hivecat.access import PrincipalIdentityRef, PrincipalKind
from hivecat.artifacts import (
    ArtifactRef,
    ArtifactReservation,
    ArtifactTenantContext,
    LocalFilesystemObjectStore,
)
from hivecat.storage.sqlalchemy import (
    SQLAlchemyArtifactRepository,
    create_sqlite_engine,
    upgrade_database,
)

payload = b"private artifact bytes"
artifact = ArtifactRef(
    artifact_id=token_hex(16),
    content_sha256=sha256(payload).hexdigest(),
)
tenant = ArtifactTenantContext(tenant_id="tenant-1")
engine = create_sqlite_engine("sqlite+aiosqlite:///hivecat.db")
await upgrade_database(engine)
repository = SQLAlchemyArtifactRepository(engine)
artifact_root = Path(environ["HIVECAT_PRIVATE_ARTIFACT_ROOT"])
# Provision artifact_root before startup: POSIX 0700 or a service-only Windows ACL.
objects = LocalFilesystemObjectStore(artifact_root)

record = await repository.reserve(
    tenant,
    ArtifactReservation(
        artifact=artifact,
        owner=PrincipalIdentityRef(
            namespace="django",
            subject_id="user-1",
            kind=PrincipalKind.USER,
        ),
        media_type="application/octet-stream",
        declared_size=len(payload),
        storage_namespace="local",
    ),
)

async def chunks() -> AsyncIterator[bytes]:
    yield payload

stored = await objects.put(
    artifact,
    storage_namespace="local",
    expected_size=len(payload),
    chunks=chunks(),
)
ready = await repository.mark_ready(
    tenant,
    artifact,
    expected_state_version=record.state_version,
    stored=stored,
)
await engine.dispose()
```

This is a trusted internal foundation, not a public upload/download service. It does not yet
authorize a caller to upload or read, scan malware, create signed URLs, provide S3, or run
retention/GC. The protected Node-result Store can now link an already-`READY` exact Artifact while
freshly checking current authorization, the exact Node lease/claim fence, and Run cancellation.
The separate current-viewer result Store can expose that completed result's exact `ArtifactRef`
after fresh authorization and lifecycle checks, but neither Store reads object bytes. See
[`docs/artifacts.md`](docs/artifacts.md) and
[`ADR 0014`](docs/adr/0014-artifact-identity-and-private-storage.md), and
[`ADR 0023`](docs/adr/0023-current-viewer-node-result-exposure.md).

## Immutable Run access binding

`RunAccessBinding` records the minimum stable identities needed to form a current-
authorization query: the initiating User/Service, exact source resource and result-commit action,
and explicit Run/result owners and resources. Under revision `0008` semantics, both owners must
equal the initiator. Ownership is attribution, not an implicit grant; no separate binding-version
field is stored.

The additive `BoundRunCommandStore.create_bound_run_with_nodes()` path writes the Run, binding,
Nodes, edges, and initial dispatch in one transaction. The original
`RunCommandStore.create_run_with_nodes()` remains compatible and creates a deliberately
`LEGACY_UNBOUND` Run. There is no late binding or inferred backfill. The protected-result writer
rejects unbound Runs instead of deriving an actor from a Django Agent owner, Artifact, worker
lease, or legacy Workflow field.

The binding has no dedicated username/email/display, prior-decision, credential, queue-capability,
or payload field. Host identity adapters must supply a stable opaque subject ID and must not embed
personal data or credentials in any reference because Core only validates bounded syntax. Queue
messages remain exactly one `node_run_id`. A binding snapshot is not authorization. Trusted
callers that use the lower-level `BoundRunCommandStore` remain responsible for launch policy;
callers that require the atomic boundary use `AuthorizedBoundRunLaunchStore`. Its SQLite
implementation is `SQLAlchemyAuthorizedBoundRunLaunchStore`, and the PostgreSQL implementation is
`PostgreSQLAuthorizedBoundRunLaunchStore`, both imported lazily from
`hivecat.storage.sqlalchemy`. `SQLAlchemyProtectedNodeResultStore` and
`PostgreSQLProtectedNodeResultStore` implement the separate protected result port and freshly
evaluate the binding's `result_commit_action` in the same transaction as the fenced mutation.
Exact current-viewer Node `ArtifactRef` exposure is available through the separate result Store.
Canonical Run output, result listing/download, delegated ownership, and Django/app composition
remain deferred. See
[`docs/run-access-binding.md`](docs/run-access-binding.md) and
[`ADR 0015`](docs/adr/0015-immutable-run-access-binding.md), and
[`ADR 0017`](docs/adr/0017-atomic-authorized-bound-run-launch.md), and
[`ADR 0018`](docs/adr/0018-protected-optional-node-artifact-result.md).

## Protected optional Node results

`hivecat.results` deliberately models one optional immutable Artifact result for one Node. A
`ProtectedNodeResultCommand` carries only a claimed `NodeLease` and exact `ArtifactRef`; it does
not carry an actor, authorization decision, Artifact status, result bytes, or clock. The returned
`ProtectedNodeResultReceipt` owns the fresh allowed decision, exact `NodeResultRecord`, and exact
`NodeCompletion` produced by the same transaction.

This port does not require every Node to produce an Artifact. Existing resultless completion stays
valid for legacy Runs and outputless control Nodes. `NodeExecutionOutcome.succeeded(artifact=...)`
opts a configured `WorkerRuntime` into the protected Store using its newest private lease; the
executor context and queue remain identifier-only, and the receipt is validated internally rather
than exposed through `WorkerExecutionResult`. It also has no exact replay ledger: a duplicate or
stale commit is rejected generically, so an uncertain acknowledgement must not be reconciled by
blindly replaying a side-effecting command. Canonical terminal Run output, multi-Artifact
manifests, result listing/download, and default app/CLI/Django/HTTP composition remain separate
work.

## Current-viewer exact Node results

`CurrentViewerNodeResultStore` is the framework-neutral read boundary for one exact Node result.
The authenticated host supplies a User or Service `PrincipalIdentityRef`; the configured Store
owns the exact view action and derives the authorization resource only from the immutable Run
binding. It re-evaluates current direct, Group, and Service grants with deny precedence and expiry
before probing result eligibility. Only an exact `COMPLETED` Run/Node whose linked Artifact still
has the committed identity, state version, and `READY` lifecycle is exposed.

```python
from hivecat.access import PrincipalIdentityRef, PrincipalKind
from hivecat.coordination import TenantContext
from hivecat.results import CurrentViewerNodeResultQuery
from hivecat.storage.sqlalchemy import SQLAlchemyCurrentViewerNodeResultStore

result_reader = SQLAlchemyCurrentViewerNodeResultStore(
    engine,
    view_action="run_result.view",  # trusted application configuration
)
artifact = await result_reader.get_node_result(
    TenantContext(tenant_id="tenant-1"),
    CurrentViewerNodeResultQuery(
        viewer=PrincipalIdentityRef(
            namespace="django",
            subject_id="user-1",
            kind=PrincipalKind.USER,
        ),
        workflow_run_id="run-1",
        node_run_id="node-1",
    ),
)
```

Missing, unbound, denied, incomplete, resultless, staged, and quarantined states share one static
authorization denial so they do not reveal existence. The returned `ArtifactRef` is not a download
capability: it contains no bytes, path, URL, credential, policy evidence, lease, or receipt. Hosts
remain responsible for authenticating the viewer and adapting it to the Core identity; the Store
has no Django dependency. PostgreSQL deployments use
`PostgreSQLCurrentViewerNodeResultStore` with a separately audited `SELECT`-only credential and
trusted Tenant context. The result reader itself adds no migration; the packaged head is now
`0021_durable_agent_version_state`.

## Durable SQLite authorization

Install `hivecat[sqlite]`, apply the packaged schema explicitly, and then inject the Store into
the existing subject-free service:

```python
from sqlalchemy.ext.asyncio import AsyncEngine

from hivecat.access import TenantAuthorizationService
from hivecat.storage.sqlalchemy import (
    SQLAlchemyTenantAuthorizationStore,
    create_sqlite_engine,
    upgrade_database,
)


async def create_authorization() -> tuple[
    TenantAuthorizationService,
    AsyncEngine,
]:
    engine = create_sqlite_engine("sqlite+aiosqlite:///hivecat.db")
    await upgrade_database(engine)
    store = SQLAlchemyTenantAuthorizationStore(engine)
    return TenantAuthorizationService(store), engine
```

Application startup never upgrades the schema automatically. The same optional module also
provides `migration_history()`, `get_migration_status()`, and `check_database()`. Dispose the
engine during application teardown. A migration-role-aware DB CLI remains deferred.

## PostgreSQL authorization with RLS

Install `hivecat[postgres]`, apply the packaged schema with a separate migration credential, and
inject a read-only non-owner runtime role plus an authenticated host context provider:

```python
from contextvars import ContextVar

from sqlalchemy.ext.asyncio import AsyncEngine

from hivecat.access import TenantAuthorizationService
from hivecat.storage.sqlalchemy import (
    PostgreSQLTenantAuthorizationStore,
    create_postgres_engine,
    upgrade_database,
)

_request_tenant: ContextVar[str | None] = ContextVar("request_tenant", default=None)


class RequestTenantContext:
    def current_tenant_id(self) -> str | None:
        return _request_tenant.get()


async def create_postgres_authorization(
    migration_url: str,
    runtime_url: str,
) -> tuple[TenantAuthorizationService, AsyncEngine]:
    migration_engine = create_postgres_engine(migration_url)
    try:
        await upgrade_database(migration_engine)
    finally:
        await migration_engine.dispose()

    runtime_engine = create_postgres_engine(runtime_url)
    store = PostgreSQLTenantAuthorizationStore(runtime_engine, RequestTenantContext())
    return TenantAuthorizationService(store), runtime_engine
```

The host sets and resets `_request_tenant` only after authenticating the caller and independently
verifying membership; the resource query never supplies trusted Tenant context. The runtime role
must be `NOSUPERUSER`, `NOBYPASSRLS`, non-owner, and SELECT-only on the snapshot tables. Hivecat
does not create roles or grants. Its credential and engine must be dedicated to this Store and
must never be exposed to user code, Tools, plugins, or arbitrary SQL: a holder of the credential
can set PostgreSQL custom settings directly, so RLS is not a defense against credential
compromise. Every policy mutation must update the affected row in
`hivecat_core_access_tenant_policy` in the same trusted administration transaction.

`PostgreSQLTenantIdentityBootstrapStore` is a separate trusted administration adapter and does not
use request Tenant context. Its dedicated direct login has no table, column, sequence, or broad
function authority: it receives only grant-option-free schema usage and execution of
`hivecat_internal.bootstrap_tenant_identity(text, text, text, text, text, text[], text[], text[],
text[])`. The adapter audits the complete role profile before calling that security-definer
helper. Do not reuse a migration, snapshot, provisioning, worker, Tool, plugin, user-code, or
arbitrary-SQL credential. Revision `0013_identity_bootstrap` installs the helper, revokes
`PUBLIC EXECUTE`, and creates no login or deployment grant.

`PostgreSQLTenantResourceProvisioningStore` is a separate write-only adapter and must use its own
engine and direct login credential. It verifies the independently trusted Tenant before checkout,
sets transaction-local RLS context, locks that Tenant's policy row first, validates the complete
batch, and changes resources, grants, normalized actions, and the revision in one transaction.
The adapter rejects a credential unless it is a non-owner `NOINHERIT`, `NOSUPERUSER`,
`NOBYPASSRLS` login with no role memberships or create authority and only the exact documented
table ACLs. Column-only privileges, grant options, `PUBLIC` table/column ACLs, and PostgreSQL 17+
`MAINTAIN` authority are also rejected. Package migrations create no role or grant, and this
capability itself adds no Alembic revision. The packaged head is now
`0021_durable_agent_version_state`.

`PostgreSQLAuthorizedTenantActorOnboardingStore` uses another dedicated function-only direct
login and a trusted Tenant context provider. Its exact non-default authority is grant-option-free
`USAGE` on `public` and `hivecat_internal` plus `EXECUTE` only on
`hivecat_internal.onboard_tenant_actor(text, text, text, text, text, text, text, text, text,
bigint, bigint, text[], text[], text[], text[])`. The adapter audits that the login is non-owner,
direct, `NOINHERIT`, `NOSUPERUSER`, and `NOBYPASSRLS`, with no role membership, relation/column/
sequence authority, create authority, grant options, or other function execution. Revision
`0015_authorized_actor_onboarding` installs the hardened helper and revokes `PUBLIC EXECUTE` but
creates no role or deployment grant. Use neither the bootstrap credential nor any snapshot,
provisioning, launch, worker, Tool, plugin, user-code, or arbitrary-SQL credential for this Store.

`PostgreSQLAuthorizedTenantMembershipLifecycleStore` uses a fourth dedicated function-only
credential and trusted Tenant context. It may execute only
`hivecat_internal.transition_tenant_membership(text, text, text, text, text, text, text, text,
bigint, bigint, text, timestamp with time zone)` and has no table authority. Revision
`0016_membership_lifecycle` installs that hardened helper, revokes `PUBLIC EXECUTE`,
and creates no role or grant. The exact role audit rejects reuse of the bootstrap or onboarding
credential and any broader database authority.

`PostgreSQLAuthorizedTenantSubjectAssignmentStore` uses a fifth dedicated function-only
credential and trusted Tenant context. It may execute only
`hivecat_internal.transition_tenant_assignment(text, text, text, text, text, text, text, text,
bigint, bigint, text, text, text, text, bigint, bigint, text, bigint, bigint, text, timestamp with
time zone)` and has no table authority. Revision `0017_assignment_lifecycle` adds exact assignment
generation/predecessor lineage, installs that hardened helper, revokes `PUBLIC EXECUTE`, and
creates no role or grant. Its exact role audit rejects reuse of every other Hivecat credential and
any broader database authority.

`PostgreSQLAuthorizedBoundRunLaunchStore` uses another dedicated direct-login credential. It
locks the Tenant policy row before evaluating current source membership/resource/grants, rejects
denied requests before any launch-target lookup, then creates only fresh Run/result policy and the
bound coordination aggregate in that same transaction. Its exact-role audit accepts only the
documented union of policy reads, create-only Run/result policy writes, and bound aggregate
inserts, including immutable execution-snapshot creation; it does not accept an authorization
snapshot, provisioning, coordination, migration, worker, or
arbitrary-SQL credential. The role does not own the database and has no database `CREATE`;
grant options for database `CREATE`, `CONNECT`, and `TEMPORARY` are forbidden. The role owns no
non-system schema: `public` permits
`USAGE` only, with neither `CREATE` nor a grant option, and every other non-system schema permits
neither `USAGE` nor `CREATE`. Hivecat creates neither that role nor its grants.

The authorization-snapshot, execution-snapshot, provisioning, authorized-launch, and
protected-result adapters currently support
PostgreSQL 16 through 18. The protected-result adapter uses its own audited direct login and only
the exact `hivecat_internal.lock_ready_artifact(text, text)` helper rather than Artifact table
authority. The same extra separately provides run/dispatch coordination Stores and the exact
current-viewer Node-result reader. Actor onboarding plus operation-specific current-membership and
Group/Service assignment lifecycles are explicit Stores; broader principal/grant lifecycle,
result listing/download, and automatic `HivecatApp`/Django composition remain deferred. See
[`docs/storage.md`](docs/storage.md),
[`ADR 0009`](docs/adr/0009-postgresql-tenant-authorization-rls.md),
[`ADR 0016`](docs/adr/0016-tenant-resource-policy-provisioning.md), and
[`ADR 0017`](docs/adr/0017-atomic-authorized-bound-run-launch.md),
[`ADR 0018`](docs/adr/0018-protected-optional-node-artifact-result.md), and
[`ADR 0019`](docs/adr/0019-worker-protected-result-composition.md), and
[`ADR 0024`](docs/adr/0024-trusted-tenant-identity-bootstrap.md), and
[`ADR 0025`](docs/adr/0025-authorized-tenant-actor-onboarding-and-safe-rejoin.md), and
[`ADR 0026`](docs/adr/0026-authorized-tenant-membership-lifecycle.md), and
[`ADR 0027`](docs/adr/0027-authorized-tenant-subject-assignment-lifecycle.md), and
[`ADR 0028`](docs/adr/0028-tenant-administrator-policy.md).

## Durable coordination kernel

`hivecat.coordination` defines additive `RunCommandStore`, `DispatchStore`,
`RunCancellationStore`, and `RevokeStore` ports; immutable plan, lease, command, and receipt values;
and separate `OutboxDispatcher` and `RevokeDispatcher` processors. The local SQLite implementation
is selected explicitly:

```python
from sqlalchemy.ext.asyncio import AsyncEngine

from hivecat.coordination import NodePlan, RunPlan
from hivecat.storage.sqlalchemy import (
    SQLAlchemyRunCoordinationStore,
    create_sqlite_engine,
    upgrade_database,
)


async def create_run_store() -> tuple[SQLAlchemyRunCoordinationStore, AsyncEngine]:
    engine = create_sqlite_engine("sqlite+aiosqlite:///hivecat.db")
    await upgrade_database(engine)
    store = SQLAlchemyRunCoordinationStore(engine)
    await store.create_run_with_nodes(
        RunPlan(
            tenant_id="tenant-1",
            workflow_run_id="run-1",
            definition_name="example",
            definition_version=1,
            definition_checksum="a" * 64,
            entrypoint_node_run_id="node-run-1",
            nodes=(NodePlan(node_run_id="node-run-1", node_id="first"),),
        ),
    )
    return store, engine
```

Run creation queues the entrypoint and writes its outbox row in the same transaction. Before each
broker call, the Store prepares an immutable high-entropy delivery identity. The dispatcher passes
only `node_run_id` as the body and that identity as task metadata to an
`IdentifiedExecutionBackend`; a worker must reload and claim current state before acting. The
legacy `ExecutionBackend` remains available to the in-memory Workflow path. The current
`WorkflowEngine` and default `HivecatApp` do not consume this Store yet. The additive
`WorkerRuntime` can explicitly consume both resultless control Nodes and an opt-in protected
Artifact result, but this remains coordination evidence and an integration seam rather than a
durable Workflow quick start. See
[`docs/storage.md`](docs/storage.md),
[`ADR 0008`](docs/adr/0008-durable-node-coordination.md), and
[`ADR 0011`](docs/adr/0011-worker-runtime-control-plane.md).
Dispose the returned engine during application teardown.

An explicit local control-plane composition keeps the backend-facing value to one Node ID and the
raw lease inside `WorkerRuntime`:

```python
from hivecat.backends import InlineBackend
from hivecat.coordination import (
    NodeExecutionContext,
    NodeExecutionOutcome,
    OutboxDispatcher,
    WorkerRuntime,
)


async def execute_control_node(
    context: NodeExecutionContext,
) -> NodeExecutionOutcome:
    # A SnapshotNodeExecutor can reload immutable inputs after this exact claim.
    await context.heartbeat()
    return NodeExecutionOutcome.succeeded()


worker = WorkerRuntime(
    store=store,
    executor=execute_control_node,
    worker_instance_id="inline-worker",
)


async def deliver(node_run_id: str) -> None:
    await worker.execute_node(node_run_id)


backend = InlineBackend(deliver)
dispatcher = OutboxDispatcher(
    store=store,
    backend=backend,
    lease_owner="inline-dispatcher",
)
await dispatcher.dispatch_once()
```

Non-retry failure can opt into a separate durable terminalization boundary. The Store first
persists an exact intent for the newest private lease, then reconciles that intent to the fixed
`execution_failed` target-Node transition, failing the Run only if it is still running. Once
preparation is confirmed, lease-expiry recovery fails the Node from that intent instead of
dispatching the executor again:

```python
from hivecat.coordination import WorkerRuntime
from hivecat.storage.sqlalchemy import SQLAlchemyNodeFailureTerminalizationStore

terminal_worker = WorkerRuntime(
    store=store,
    terminalization_store=SQLAlchemyNodeFailureTerminalizationStore(engine),
    executor=execute_control_node,
    worker_instance_id="inline-terminal-worker",
)
```

This optional Store is used only when the outcome has no explicit `retry_at`. Explicit retry,
successful resultless completion, and successful protected Artifact-result commit keep their
existing paths. The intent and receipt stay private; queue payloads, `NodeExecutionContext`, and
`WorkerExecutionResult` do not change. The guarantee starts only after an exact prepare response is
validated. It does not make executor, Tool, Provider, result commit, or arbitrary external side
effects exactly once. See
[`ADR 0021`](docs/adr/0021-durable-node-failure-terminalization.md).

For a bound Run whose trusted execution already has an exact `READY` `ArtifactRef`, protected
result finalization is an explicit additive composition:

```python
from hivecat.artifacts import ArtifactRef
from hivecat.coordination import NodeExecutionContext, NodeExecutionOutcome, WorkerRuntime
from hivecat.storage.sqlalchemy import SQLAlchemyProtectedNodeResultStore

ready_artifact = ArtifactRef(
    artifact_id="a" * 32,
    content_sha256="b" * 64,
)


async def execute_result_node(context: NodeExecutionContext) -> NodeExecutionOutcome:
    await context.heartbeat()
    return NodeExecutionOutcome.succeeded(artifact=ready_artifact)


result_worker = WorkerRuntime(
    store=store,
    result_store=SQLAlchemyProtectedNodeResultStore(engine),
    executor=execute_result_node,
    worker_instance_id="inline-result-worker",
)
```

The Artifact must already be `READY`; this seam does not publish it. Omitting `result_store` and
returning `succeeded()` preserves the resultless path only for legacy or optional Nodes. A
`snapshot_v1` Node marked `artifact_required` cannot use resultless completion: the Store rejects
it before mutation and revision `0010` independently enforces the same rule in the database.

To opt into restart-safe reload, trusted launch code supplies the compiled Definition payload
without its checksum field plus fully resolved inputs. The Definition document itself hashes to the
existing `WorkflowDefinition.checksum`; handler version and result policy are separate immutable
Node metadata:

```python
from hivecat.coordination import (
    NodePlan,
    NodeResultRequirement,
    PredecessorResultContract,
    RunExecutionSnapshotPlan,
    RunPlan,
)
from hivecat.workflow import step, task, workflow


@task(name="example.snapshot_prompt")
def snapshot_prompt(prompt: str) -> str:
    return prompt


@workflow(name="snapshot-prompt", version=1)
def snapshot_prompt_workflow(prompt: str):
    return step(snapshot_prompt, prompt, node_id="execute")


compiled_definition = snapshot_prompt_workflow.compile()

definition_payload = compiled_definition.model_dump(mode="json", exclude={"checksum"})
execution_snapshot = RunExecutionSnapshotPlan(
    definition=definition_payload,
    inputs={"prompt": "hello"},  # every declared input, with defaults already resolved
)
node = compiled_definition.nodes[0]
snapshot_run = RunPlan(
    tenant_id="tenant-1",
    workflow_run_id="run-with-snapshot",
    definition_name=compiled_definition.name,
    definition_version=compiled_definition.version,
    definition_checksum=compiled_definition.checksum,
    entrypoint_node_run_id="run-with-snapshot-node",
    nodes=(
        NodePlan(
            node_run_id="run-with-snapshot-node",
            node_id=node.id,
            max_attempts=node.max_attempts,
            handler_id=node.handler,
            handler_version=1,
            result_requirement=NodeResultRequirement.ARTIFACT_REQUIRED,
        ),
    ),
    execution_snapshot=execution_snapshot,
    predecessor_result_contract=PredecessorResultContract.ARTIFACT_REF_V1,
)
```

Submit a required-result plan through the existing bound/authorized launch path so protected result
authorization exists. Execution uses a separate exact-claim snapshot Store and an explicit registry;
database handler text is never dynamically imported:

```python
from hivecat.storage.sqlalchemy import SQLAlchemyNodeExecutionSnapshotStore
from hivecat.workflow import SnapshotNodeExecutor, VersionedHandlerRegistry


async def execute_bound_snapshot_result(
    context,
    *,
    arguments,
    keyword_arguments,
    model_route_id,
):
    await context.heartbeat()
    assert arguments == ("hello",)
    assert keyword_arguments == {}
    assert model_route_id is None
    return NodeExecutionOutcome.succeeded(artifact=ready_artifact)


registry = VersionedHandlerRegistry()
registry.register_bound(
    handler_id=node.handler,
    handler_version=1,
    handler=execute_bound_snapshot_result,
)
snapshot_executor = SnapshotNodeExecutor(
    store=SQLAlchemyNodeExecutionSnapshotStore(engine),
    registry=registry,
    lease_owner="snapshot-worker",
)
snapshot_worker = WorkerRuntime(
    store=store,
    result_store=SQLAlchemyProtectedNodeResultStore(engine),
    executor=snapshot_executor,
    worker_instance_id="snapshot-worker",
)
```

`PredecessorResultContract.ARTIFACT_REF_V1` is an explicit Run-level opt-in. Every exact handler
version for that Run is registered with `register_bound()` and receives fresh `arguments`,
`keyword_arguments`, and the immutable Definition's `model_route_id`. Input references resolve
from the immutable Run input snapshot. In a multi-Node Definition, a Node reference must be an exact
direct edge whose source is `artifact_required`; it resolves only to that completed predecessor's
immutable `ArtifactRef`. Optional resultless predecessors may remain control-only edges and are
represented internally as `None`, but cannot be referenced as handler data. In this first contract
version every edge in the opted-in Definition must be unconditional (`condition is None`) and its
`output_mapping` must be empty; conditional or mapped-edge binding is deferred to a later version.

Omitting the predecessor-result contract preserves the existing document-handler path:
`registry.register()` handlers continue to receive fresh `definition` and `inputs` JSON copies.
Neither call shape receives a lease token, fence, Store, object path, URL, credential, or Artifact
bytes. Keep credentials out of snapshot values; store only trusted resolver identifiers. See
[`ADR 0022`](docs/adr/0022-immutable-predecessor-artifact-binding.md).

Custom durable backends must use the caller-supplied task ID exactly for any accepted work. Only
`EnqueueRejectedBeforeAcceptanceError` proves definite non-acceptance and permits immediate
release. A generic exception, timeout, returned-ID mismatch, or acknowledgement uncertainty stays
leased and is recorded as ambiguous; later expiry may prepare another attempt without deleting the
earlier identity. This is not broker deduplication, exactly-once delivery, worker authorization,
and this delivery protocol does not itself perform protected-result fencing, forced task
termination, or external-side-effect rollback. See
[`ADR 0012`](docs/adr/0012-dispatch-delivery-identity-and-ambiguity.md).

Cancellation is a separate trusted internal command. It requires an explicit `TenantContext` and
an opaque, caller-generated `cancellation_id` so an uncertain database response can be retried
safely. One Store transaction marks the Run `CANCELLED`, increments every changed Node's fence and
state version, cancels pending/queued/retrying/running Nodes and pending/publishing dispatches, and
materializes revoke targets for every prepared delivery plus each distinct known legacy backend
task ID. Published dispatch history is preserved. A receipt reports cancelled Node count, revoke
target count, and the number of legacy publishing identities that could not be reconstructed; IDs
are never guessed.

```python
from secrets import token_hex

from hivecat.coordination import (
    RevokeDispatcher,
    RunCancellationCommand,
    TenantContext,
)

receipt = await store.request_run_cancellation(
    TenantContext(tenant_id="tenant-1"),
    RunCancellationCommand(
        workflow_run_id="run-1",
        cancellation_id=token_hex(16),
    ),
)

revoker = RevokeDispatcher(
    store=store,
    backend=backend,
    lease_owner="inline-revoker",
)
await revoker.dispatch_once()
```

`RevocableExecutionBackend` receives only the durable task ID. `request_accepted` and
`task_not_found` complete a revoke target; a typed pre-acceptance rejection retries with exact
evidence, while an ordinary exception, timeout, malformed return, or lost acknowledgement retries
as `outcome_unknown`. Backend I/O is outside Store transactions, and unresolved work is not
silently discarded at an arbitrary attempt count. The first implementation assumes one stable
backend task-ID namespace per Store/dispatcher/revoker composition; multi-backend routing requires
immutable route evidence and is deferred. See
[`ADR 0013`](docs/adr/0013-durable-run-cancellation-and-broker-revoke.md).

For an existing pre-alpha SQL deployment, quiesce pre-0005 dispatchers and resolve or expire their
active `publishing` leases before enabling the new dispatcher. Legacy acknowledgement and release
after migration are rejected when no prepared row exists, so such a lease must expire for new-
dispatcher reclaim. Legacy `backend_task_id` values are not backfilled as prepared delivery
history. Follow the exact upgrade and PostgreSQL grant notes in [`docs/storage.md`](docs/storage.md).

The current `0021_durable_agent_version_state` head adds empty exact Definition and Tenant-scoped
AgentVersion binding storage, hardened function-only PostgreSQL helpers, and SQLite invariants;
it performs no legacy backfill and refuses lossy downgrade while any new row exists. The preceding
`0020_tenant_agent_access_query` adds only the hardened function-only PostgreSQL managed-access
query and revokes `PUBLIC EXECUTE`; SQLite is schema-neutral and downgrade keeps all managed
history. Revision `0019_agent_access_management` adds append-only managed Agent
grant/action history without rewriting or backfilling the existing authorization projection. It
installs a hardened function-only PostgreSQL manager, revokes `PUBLIC EXECUTE`, and refuses
downgrade while managed history exists. Revision `0018_tenant_administrator_policy` adds explicit
administrator-policy and exact binding-history tables without backfilling an owner or role. It
installs the separated hardened
PostgreSQL initialization, zero-anchor recovery, governance, and structural-invariant helpers;
downgrade refuses while any policy or binding history exists. The preceding
`0017_assignment_lifecycle` adds assignment generation/predecessor lineage and installs the
hardened PostgreSQL assignment-lifecycle helper after `0016_membership_lifecycle`.
Revision `0016` installs the membership-lifecycle helper after `0015_authorized_actor_onboarding`,
and revision `0015` installs the actor-onboarding helper after
`0014_membership_generation_scope`. Revision `0014` adds membership
generation/current/predecessor history and grant scope/binding columns in both SQL dialects,
updates PostgreSQL principal visibility and bootstrap semantics, and safely backfills existing
rows as generation-one/current and `LEGACY`. Revisions `0015` and `0016` are schema-neutral on
SQLite. Revision `0017` rebuilds the SQLite assignment table with generation-one backfill and
refuses a lossy downgrade after any successor generation exists. Because pre-0017 replacement
history has no trustworthy ordering evidence, upgrade fails with
`hivecat_assignment_lineage_backfill_ambiguous` when one logical membership/subject already has
multiple rows; back up the database and explicitly remediate that history instead of inferring a
lineage from IDs. Quiesce assignment writers for upgrade or downgrade. These revisions create no
role or deployment grant. Revision `0013_identity_bootstrap` remains the preceding
global fresh-Tenant bootstrap helper, and revision `0012_predecessor_binding` adds the immutable
Run-level predecessor-result contract, exact-claim direct-predecessor projection, and database
guards for Node-result insertion, update, and deletion after `0011_node_failure_intent`. Revision
`0011` adds immutable non-retry
failure intents, exact prepare/reconcile helpers, and intent-aware expired-lease recovery after
`0010_execution_snapshot`. Revision `0010` adds immutable execution snapshots, Node result policy,
and the exact PostgreSQL
`hivecat_internal.load_claimed_node_execution(text, text, text, bigint, text)` helper after
`0009_protected_node_result`. Revision `0009` adds the immutable per-Node result table and exact
`hivecat_internal.lock_ready_artifact(text, text)` helper; `0008_run_access_binding`,
`0007_artifact_foundation`, and
revision `0006_run_cancellation_revoke` precede it. Revision `0006` requires all SQLite database
users to be quiesced while its foreign-key-safe Run/Node/outbox rebuild runs. The managed
migration runner
checks `PRAGMA foreign_key_check` and restores enforcement before returning; if restoration cannot
be proved, including task cancellation during cleanup, it discards that physical connection rather
than returning it to the pool. Downgrade to `0005` is refused after any cancellation or revoke
state exists. For revision `0006`, PostgreSQL operators add only the documented narrow
cancellation/revoke function grants; the Run-aware dispatch function preserves its existing
identity and ACL. Revision `0007` separately requires the documented Artifact-service table DML,
and revision `0008` requires only `SELECT, INSERT` on the binding table for the trusted bound-Run
API role. Existing-Tenant resource/grant provisioning reuses the access tables, per-Tenant policy
row, and FORCE-RLS policies; it requires the separate exact-ACL
provisioning role documented in [`docs/storage.md`](docs/storage.md), not a new migration. Package
migrations create none of these deployment grants. Atomic authorized launch also reuses the
existing schema, but requires its own exact-ACL role across the documented access and
bound-aggregate tables; do not reuse or broaden the narrower API, snapshot, or provisioning role.

Revision `0009` creates no runtime role or grant. Operators must provision a dedicated result
login with exactly the documented policy/binding/coordination/result table privileges, schema
`USAGE` only on `public` and `hivecat_internal`, and `EXECUTE` only on
`lock_ready_artifact(text, text)`. It receives no Artifact table ACL and no result `UPDATE` or
`DELETE`. Existing launch, worker, dispatcher, revoker, recovery, and Artifact roles receive no
new authority. Downgrade to `0008` is refused unless an unfiltered check proves the result table
empty; an RLS-hidden row fails closed. See [`docs/storage.md`](docs/storage.md) for the exact grant
matrix and upgrade sequence.

Revision `0010` also creates no role or grant. The authorized-launch role adds only
`SELECT, INSERT` on `hivecat_core_run_execution_snapshots`. A separate direct-login snapshot loader
gets `USAGE` on `public` and `hivecat_internal` plus `EXECUTE` only on
`load_claimed_node_execution(text, text, text, bigint, text)` and no Hivecat table ACL. Existing
result, coordination, dispatcher, revoker, recovery, and Artifact roles gain no snapshot read.
Migrate and update those exact grants before enabling snapshot Runs; downgrade is refused while any
snapshot, non-legacy Run, versioned handler, or non-optional result policy exists. See
[`docs/storage.md`](docs/storage.md) and
[`ADR 0020`](docs/adr/0020-immutable-execution-snapshot-required-result.md).

Revision `0011` likewise creates no role or grant. A dedicated terminalization login gets `USAGE`
on only the `public` and `hivecat_internal` schemas, plus `EXECUTE` only on the exact
prepare/reconcile functions and no Hivecat table access or schema `CREATE`.
The adapter audits a direct `NOINHERIT`, `NOSUPERUSER`, `NOBYPASSRLS`, `NOCREATEDB`,
`NOCREATEROLE`, `NOREPLICATION` login with no memberships, Hivecat/internal ownership, relevant
grant options, sequence authority, or extra Hivecat/internal privileges; unrelated host
application objects remain a
deployment isolation concern. Mismatch is the static
`postgres_node_failure_terminalization_role_required` error.
Existing service, worker, result, snapshot-loader, dispatcher, revoker, recovery, launch, and
Artifact roles are not broadened. The recovery function keeps its existing identity and deployment
ACL while learning that a valid confirmed intent must fail rather than redispatch an expired Node.
Deploy the migration and intent-aware recovery before enabling `terminalization_store`. Downgrade
to `0010` is refused while any intent row exists, including one hidden by RLS. See
[`docs/storage.md`](docs/storage.md) and
[`ADR 0021`](docs/adr/0021-durable-node-failure-terminalization.md).

Revision `0012` creates no role or grant either. New execution-snapshot deployments use a dedicated
direct login with `EXECUTE` only on
`hivecat_internal.load_claimed_node_execution_v2(text, text, text, bigint, text)` and no Hivecat
table, sequence, or unrelated function authority. The v1 helper is retained for rolling old
snapshot loaders, but its old credential must stay separate: granting both helper signatures to
the new role fails the exact-role audit. Migrate, assign the v2 loader and guarded result-insert
function to the documented elevated function owner where required, and install the exact v2 grant
before creating `artifact_ref_v1` Runs. Separately, an existing PostgreSQL coordination-worker
role that can finish an optional Node without a result must add exact `EXECUTE` on
`hivecat_internal.node_result_exists(text, text)`; the helper returns only a Tenant-scoped boolean,
has revoked `PUBLIC EXECUTE`, and grants no result-table access. The result INSERT guard uses
database current time, so backdating `committed_at` cannot revive an expired lease, while
resultless completion rechecks result absence after locking the current Run and Node. Downgrade to
`0011` is refused while any opted-in Run exists, including a Run hidden by RLS. See
[`docs/storage.md`](docs/storage.md) and
[`ADR 0022`](docs/adr/0022-immutable-predecessor-artifact-binding.md).

Only an explicit `NodeExecutionOutcome.failed(retry_at=...)` asks the Store for a retry. Ordinary
executor exceptions become a fixed terminal `execution_failed`; process cancellation propagates
and leaves the lease for expiry recovery. An Artifact outcome with no result Store or a definitive
protected rejection selects the same non-retry failure. With the optional terminalization Store,
the worker first confirms an immutable intent and then reconciles it; acknowledgement uncertainty
after that prepare point cannot become lease-expiry redispatch. Without that Store, the legacy
single fenced failure behavior remains. Prepare uncertainty before confirmation and ordinary
result-Store errors or malformed receipts remain acknowledgement-uncertain and cause no competing
mutation.
The context exposes identifiers, attempt, and heartbeat, not the lease token, fence, state version,
inputs, credentials, Artifact, result Store, or result payload. A trusted document snapshot handler
receives fresh isolated JSON copies of the durably immutable Definition/Input snapshot after
exact-claim validation. An opted-in bound handler instead receives fresh resolved arguments with
only immutable input values and direct predecessor `ArtifactRef` leaves. Neither path receives a
lease capability or credential. This executor is trusted application code, not a sandbox.

Mutation callers do not supply the authorization clock. The SQLite Store reads its own UTC clock
after acquiring the write transaction and uses that single value for lease validity and state
timestamps; only policy scheduling inputs such as `available_at` and `retry_at` remain explicit.

For PostgreSQL, service commands use an independently trusted Tenant context, while worker,
dispatcher, and revoker paths use separate deployment roles and fixed internal functions:

```python
from hivecat.storage.sqlalchemy import (
    PostgreSQLDispatchStore,
    PostgreSQLNodeFailureTerminalizationStore,
    PostgreSQLRevokeStore,
    PostgreSQLRunCoordinationStore,
    create_postgres_engine,
)

def create_postgres_coordination(
    service_url: str,
    worker_url: str,
    dispatcher_url: str,
    revoker_url: str,
    terminalization_url: str,
):
    service_engine = create_postgres_engine(service_url)
    run_store = PostgreSQLRunCoordinationStore(service_engine, RequestTenantContext())

    # Lease-capability methods do not consult request context, but the worker still
    # owns a separate Store and credential with only the deployment's worker grants.
    worker_engine = create_postgres_engine(worker_url)
    worker_store = PostgreSQLRunCoordinationStore(worker_engine, RequestTenantContext())

    dispatcher_engine = create_postgres_engine(dispatcher_url)
    dispatch_store = PostgreSQLDispatchStore(dispatcher_engine)

    revoker_engine = create_postgres_engine(revoker_url)
    revoke_store = PostgreSQLRevokeStore(revoker_engine)

    terminalization_engine = create_postgres_engine(terminalization_url)
    terminalization_store = PostgreSQLNodeFailureTerminalizationStore(
        terminalization_engine
    )
    return (
        run_store,
        service_engine,
        worker_store,
        worker_engine,
        dispatch_store,
        dispatcher_engine,
        revoke_store,
        revoker_engine,
        terminalization_store,
        terminalization_engine,
    )
```

Pass `worker_store` to `WorkerRuntime`; for Artifact outcomes also pass a separately constructed
`PostgreSQLProtectedNodeResultStore` backed by its dedicated result engine. Dispose all returned
engines during application teardown. Production deployments normally use
separate service, worker, dispatcher, revoker, and recovery credentials even when their Store APIs
share the same package; the protected-result adapter uses another dedicated credential and
`PostgreSQLNodeFailureTerminalizationStore` uses its own exact function-only credential. Pass that
`terminalization_store` to the Worker only when revision `0011` and intent-aware recovery are
deployed together.

Run/Node/outbox rows use FORCE RLS. An ID-only worker claim derives the authoritative Tenant inside
a hardened database function, issues a monotonically stronger fence, and records immutable attempt
evidence. Completion serializes readiness within one Run so concurrent predecessors cannot lose a
join wakeup; dispatch, revoke, and recovery use bounded `SKIP LOCKED` claims. Cancellation receipts,
prepared delivery/outcome rows, revoke targets, and revoke events use FORCE RLS and narrow internal
functions. Package migrations create no login roles or deployment grants. The function owner must
be unreachable and RLS-elevated, while ordinary API, result, worker, dispatcher, revoker, and
recovery roles
remain non-owner and `NOBYPASSRLS` with only their exact table/function capabilities. Every internal
function, including the Artifact-lock helper, verifies that owner capability and fails explicitly
instead of returning an apparently empty queue when deployment ownership is wrong.

The coordination, execution-snapshot, and result credentials are trusted infrastructure and must
never be shared with arbitrary SQL, Tools, plugins, or user code. The Stores, WorkerRuntime, and
revoke processor are still not wired into `WorkflowEngine` or `HivecatApp`. An Artifact publisher,
public Django/HTTP/CLI cancellation authorization, Celery/Taskiq adapters, result listing/download,
forced termination, exactly-once delivery, multi-backend routing evidence, and external-side-effect
rollback remain deferred. See
[`ADR 0010`](docs/adr/0010-postgresql-run-coordination.md) for the lock/role contract and
[`ADR 0011`](docs/adr/0011-worker-runtime-control-plane.md) for the worker boundary, and
[`ADR 0012`](docs/adr/0012-dispatch-delivery-identity-and-ambiguity.md) for dispatch ambiguity, and
[`ADR 0013`](docs/adr/0013-durable-run-cancellation-and-broker-revoke.md) for cancellation/revoke,
and [`ADR 0018`](docs/adr/0018-protected-optional-node-artifact-result.md) for protected result
commit, [`ADR 0019`](docs/adr/0019-worker-protected-result-composition.md) for optional worker
result finalization, and
[`ADR 0020`](docs/adr/0020-immutable-execution-snapshot-required-result.md) for immutable reload and
required-result enforcement, and
[`ADR 0021`](docs/adr/0021-durable-node-failure-terminalization.md) for confirmed non-retry failure
intent and reconciliation, and
[`ADR 0022`](docs/adr/0022-immutable-predecessor-artifact-binding.md) for exact predecessor
`ArtifactRef` binding.

## Application composition

`create_app()` builds a fresh, deny-by-default in-memory Core graph. Each call owns separate
Agent grants, Provider and Route registries, Tools, Workflows, run state, backend, plugin manager,
and lifecycle resources, so multiple apps can coexist in one process without a global singleton.

```python
import asyncio

from hivecat import create_app


async def main() -> None:
    app = create_app()
    async with app:
        # Register this deployment's Agents, routes, Providers, Tools, and Workflows on app.
        ...


asyncio.run(main())
```

The optional typed config selects only adapters that currently exist. It is immutable
programmatic metadata and never contains credential values:

```python
from hivecat import HivecatConfig, create_app
from hivecat.app import EnvironmentCredentialStoreConfig

config = HivecatConfig(
    credential_store=EnvironmentCredentialStoreConfig(
        prefix="PROJECT_CREDENTIAL_",
    )
)
app = create_app(config=config)
```

`config.public_view()` returns only the allowlisted adapter kinds and environment-variable
prefix. It does not read environment values or sanitize arbitrary input. SQL, Celery, Taskiq,
Provider, and plugin config selectors are added only with their working adapters. A
`PluginManager` is a runtime dependency injected separately from this secret-free config.

Use `HivecatApp.compose(...)` when a host needs to inject its own implementations of the current
Core ports. The detailed ownership and lifecycle contract is in
[`docs/application.md`](docs/application.md).

## Plugin trust boundary

Every app owns a `PluginManager`, but app construction, startup, trusted app loading, and
`hivecat check` do not discover or load plugins. The current manager accepts only metadata and an
opaque loader explicitly supplied by a trusted host or future package-metadata adapter. It checks
the normalized distribution, versioned entry-point group and name, Hivecat API version, plugin
kind, and complete capability set against an exact deployment allowlist before calling the loader.

Only the four namespaces backed by current Core ports are represented: Provider, Execution
Backend, State Store, and Credential Store. A successful load records an immutable identity and
distribution-version snapshot; repeated success or failure never executes that plugin again in
the same manager. Empty capabilities mean no capabilities, not a wildcard.

This is not live installed-package discovery, Tenant or Agent authorization, a package-signature
verifier, or a sandbox. Approved Python plugin code runs with the process's authority. Actual SDK
adapters and kind-specific typed registrations remain roadmap work. See
[`docs/plugins.md`](docs/plugins.md) for the exact contract and deferred boundaries.

## Application loading and CLI check

`load_app()` resolves one explicit zero-argument factory. It accepts sync, async, and general
awaitable factories, requires a fresh `HivecatApp`, and does not start the app:

```python
import asyncio

from hivecat import load_app


async def main() -> None:
    app = await load_app("myproject.hivecat:create_app")
    async with app:
        ...


asyncio.run(main())
```

The first app-aware CLI command checks construction plus one complete lifespan startup and
teardown:

```bash
uv run hivecat --app myproject.hivecat:create_app check
```

`HIVECAT_APP=myproject.hivecat:create_app` is a lower-precedence alternative to `--app`. There is
no implicit default app. The reference is trusted deployment-operator configuration: importing
it executes Python code with the process's authority. Do not populate it from HTTP requests,
Agent or Workflow definitions, database values, or other final-user input. The strict grammar is
not a sandbox or plugin approval system. Importing `hivecat` alone still discovers or initializes
no application and reads no app environment variable.

## Workflow quick start

```python
import asyncio

from hivecat import system_principal
from hivecat.state import MemoryStateStore
from hivecat.workflow import TaskRegistry, step, task, workflow
from hivecat.workflow.runtime import WorkflowEngine


@task
def normalize(value: str) -> str:
    return value.strip().lower()


@workflow(name="normalize-text", version=1)
def normalize_text(value: str):
    return step(normalize, value, node_id="normalize")


async def main() -> None:
    tasks = TaskRegistry()
    tasks.register(normalize)
    engine = WorkflowEngine(state_store=MemoryStateStore(), tasks=tasks)
    result = await engine.start(
        definition=normalize_text.compile(),
        inputs={"value": "  HELLO  "},
        principal=system_principal(),
    )
    print(result.output)  # hello


asyncio.run(main())
```

## Django integration

```bash
pip install "hivecat[django]"
```

```python
INSTALLED_APPS = [
    # Django apps...
    "hivecat.contrib.django",
]
```

Run `python manage.py migrate` to install the Agent catalog, User/Group grants,
preferences, and access-audit models. Hivecat uses `settings.AUTH_USER_MODEL`, so
custom Django User models are supported.

Those Django models are the tenantless 0.x compatibility path. They are not a projection of the
general Tenant authorization tables and are not read or written by the new bridge. Configure a
stable deployment-specific `HIVECAT_PRINCIPAL_NAMESPACE`, inject a separate stable 32-byte-or-
longer identity HMAC key from a secret manager, ensure User/Group primary keys are never reused,
and inject one request-local User/Tenant provider. Do not reuse Django `SECRET_KEY`; changing the
identity key, namespace, or concrete Django model label changes every mapped identity and requires
explicit reprovisioning. The mapper deliberately accepts only Django's exact built-in
`AutoField`/integer-family, `UUIDField`, `CharField`, `EmailField`, and `SlugField` primary-key
classes. Custom field subclasses and other PK types fail closed; this avoids running model
constructors, field conversion hooks, signals, or ORM queries while authorizing a request.

```python
from contextvars import ContextVar

from hivecat.contrib.django.identity import DjangoPrincipalIdentityMapper
from hivecat.contrib.django.tenant_agents import DjangoTenantAgentService


current_request_tenant: ContextVar[str | None] = ContextVar(
    "current_request_tenant",
    default=None,
)
current_request_user: ContextVar[object | None] = ContextVar(
    "current_request_user",
    default=None,
)


class RequestContext:
    def current_user(self) -> object:
        user = current_request_user.get()
        if user is None:
            raise RuntimeError("no authenticated request user")
        return user

    def current_tenant_id(self) -> str | None:
        # Return only the Tenant bound by trusted host authentication/routing.
        return current_request_tenant.get(None)


request_context = RequestContext()
django_agents = DjangoTenantAgentService(
    service=tenant_agent_service,
    identity_mapper=DjangoPrincipalIdentityMapper(
        identity_key=identity_hmac_key_from_secret_manager,
    ),
    request_context=request_context,
)

visible_agents = await django_agents.list_agents()
await django_agents.admit_run(agent_id="support-agent")
```

`admit_run()` is still preflight-only and returns `None`; it does not execute an Agent.
`DjangoTenantAgentAccessManager` exposes bounded User/Group create and exact-reference revoke
commands through `AuthorizedTenantAgentAccessStore`. Its create methods accept an opaque target
User or Group PK and derive the same HMAC identity without resolving a Django row; the Store still
authorizes the context-bound administrator before probing the Core target. For PostgreSQL, inject
the same trusted request-local provider into the facade and Store.
`DjangoTenantAgentAccessReader.query_access()` exposes the separate bounded Core query without an
administrator or Tenant parameter and returns opaque summaries plus exact revoke references. It
does not reverse-map those identities to Django rows or read legacy grant tables. This adapter is
not a Django Admin grant browser: display-name lookup needs a separate authorized identity
directory, and Admin mutations still need an explicit sync/async and POST/CSRF design. Django M2M
membership or object deletion also does not synchronize Core assignment/membership lifecycle;
perform that lifecycle explicitly until a durable synchronization boundary is implemented.

For published Agent versions, import the request-bound facades from
`hivecat.contrib.django.tenant_agent_versions`. `DjangoTenantAgentVersionService` exposes only the
safe summary list/get and `None`-returning run preflight. `DjangoTenantAgentVersionManager`
constructs exact first-activate, rebind/reactivate, and deactivate plans, but never accepts a
caller-supplied manager or Tenant. The injected Core Store still requires an effective User
`tenant.administrator` and fresh deny-aware exact `agent.manage`; Django staff/superuser state is
not authority. See [`docs/django.md`](docs/django.md) for identity-key, schema-ownership, and
deferred Admin/DRF guidance.
