Skip to main content

Framework-agnostic AI agent runtime with first-class Django integration

Project description

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:

pip install hivecat

The currently implemented optional extras are independent and selectable:

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

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. 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. Accepted ownership, security, and compatibility decisions are indexed in docs/adr/.

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:

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:

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 and its additive Model extension in ADR 0032, followed by the Tool-free Agent contract in ADR 0033.

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:

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 and its lifecycle extension in ADR 0035, followed by the durable-state decision in ADR 0036.

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.

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.

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 and ADR 0026.

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.

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.

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.

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.

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.

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.

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.

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 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 for the exact policy boundary and 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.

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 and ADR 0014, and ADR 0023.

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 and ADR 0015, and ADR 0017, and ADR 0018.

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.

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:

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:

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, ADR 0009, ADR 0016, and ADR 0017, ADR 0018, and ADR 0019, and ADR 0024, and ADR 0025, and ADR 0026, and ADR 0027, and ADR 0028.

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:

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, ADR 0008, and ADR 0011. 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:

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:

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.

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

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:

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:

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.

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.

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.

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.

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.

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, 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 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 and ADR 0020.

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 and ADR 0021.

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 and ADR 0022.

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:

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 for the lock/role contract and ADR 0011 for the worker boundary, and ADR 0012 for dispatch ambiguity, and ADR 0013 for cancellation/revoke, and ADR 0018 for protected result commit, ADR 0019 for optional worker result finalization, and ADR 0020 for immutable reload and required-result enforcement, and ADR 0021 for confirmed non-retry failure intent and reconciliation, and ADR 0022 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.

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:

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.

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 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:

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:

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

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

pip install "hivecat[django]"
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.

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 for identity-key, schema-ownership, and deferred Admin/DRF guidance.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hivecat-0.1.0a1.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hivecat-0.1.0a1-py3-none-any.whl (486.8 kB view details)

Uploaded Python 3

File details

Details for the file hivecat-0.1.0a1.tar.gz.

File metadata

  • Download URL: hivecat-0.1.0a1.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for hivecat-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 32c0f533b9913ffaaa8355ff27cd329429be305c2da91ad744b2cbb084970ab0
MD5 10c4b5da8829f362e8c0a87b950f97ca
BLAKE2b-256 eb59b0bcfe05bc965d31d801c448d8bae8b7ef99d0c1d192405771250127f1ec

See more details on using hashes here.

File details

Details for the file hivecat-0.1.0a1-py3-none-any.whl.

File metadata

  • Download URL: hivecat-0.1.0a1-py3-none-any.whl
  • Upload date:
  • Size: 486.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for hivecat-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 a3c880f1331ab4d44b165177fae8b77078a1014ac66f1ad39595233c8df26e4a
MD5 f319923216135d7dd07b748933b1a85a
BLAKE2b-256 1cb9b23e77603c4310a833a15f009138514ed34e3ec854b32a4b8175262c081d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page