Connectors and credentials¶
How Axon connects to external services (Gmail, Telegram, CRM, drives, webhooks) and where the credentials live. This manual answers: what a "connector" and a "credential" are and why they're not the same thing; who can do what; what the
AllowlistandAdd instancebuttons in the Connectors section do; how to walk the full path from a new project to a working integration.
1. What it is and why¶
In Axon, a single integration with an external service splits along two orthogonal axes:
- Connector — this is code ("how to talk to the service"): an adapter class (
GmailConnector,TelegramConnector…) that can call the service API, normalize inbound events, verify webhook signatures. Plus a declarative description of the credential format (CredentialType). Connectors are the same for all projects; they're not in the DB — they're deployed with the platform. - Credential — this is the data of a specific account ("with whose authority"): the encrypted secret (OAuth token, API key) + its lifecycle (active / expired / revoked, when it was last refreshed, when it expires). A credential is project-scoped, lives in the DB (
app.connector_credentials), and is created and managed via the Console.
Between them there is an intermediate entity — the connector instance ("a configured connection") = adapter + credential + non-secret settings. A workflow references instances (via named slots), not the connector code and not the credential directly.
Why split them (instead of "one entity, an integration"): - the adapter code and the token have different lifecycles — the code changes per release, the token expires/gets revoked/refreshed all the time; - security boundary: app-api works freely with the adapter, but never sees the plaintext secret — decryption is only possible in the worker (invariant I-1); - 1:N: one credential (one Gmail account) can feed several instances with different settings; one adapter — many instances; - project isolation: a credential is strictly bound to a project, can't be shared across projects; the adapter is shared; - audit and governance: every touch of the secret is a separate audit-log entry, separate permissions, a separate approval gate.
A 60-second analogy:
CredentialType= the ID-card blank (which fields exist). Credential = your actual ID card in the safe (the number is encrypted; on the spine you see "valid until", "check OK"). Connector (adapter) = the skill of driving this service's car. ConnectorInstance = "this particular car I drive with this ID, with the mirrors adjusted for me". BindingProfile = the dispatcher's note "for this run, take that car". The safe (the worker) is opened with its own key; the dispatcher (app-api) sees "the ID exists, valid, bound to car #3" — but not the digits themselves.
2. Roles and access¶
Source: core/models/auth.py. owner / admin are instance-scoped (act across all projects). manager / operator / reviewer / read_only are project-scoped (only in projects from their project_scopes). system — service actors (auto-refresh, health, retention); these rights are not granted to humans (invariant I-9: HUMAN ∩ SYSTEM_ONLY = ∅).
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| View connectors / instances / health | read* |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| View credentials (redacted) | credential:read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Allowlist toggle (enable/disable a connector for a project) | project_connector:manage |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Add instance / update / delete connector instance | connector:manage |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Configure a workflow binding profile (slot → instance) | workflow:configure_bindings |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| New credential / OAuth connect | credential:create |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Update a credential (metadata/payload) | credential:update |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Revoke a credential | credential:revoke |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Test connection | credential:test |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Use a credential in a run | credential:use |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Approve a high-stakes credential proposal | approve |
✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
| Start a workflow (uses the bound instances) | start_workflow |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | — |
| Reauthorize OAuth / refresh-rotate | credential:rotate |
✅† | ✅† | ✅† | ❌ | ❌ | ❌ | ✅ |
| Auto-healing / maintain | credential:maintain |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Hard-purge / GDPR erasure | credential:purge ‡ |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Webhook ingress verifier upsert/disable | ingress_verifier:manage |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
* there is no separate connector:read permission in core/models/auth.py — viewing connectors/instances/health is gated by read + having project access (the canonical query-route docs sometimes write connector:read as shorthand; the RBAC layer covers it via read).
† credential:rotate is held by owner/admin (via HUMAN_PERMISSIONS), by manager (via _CREDENTIAL_MANAGER_PERMISSIONS) and by system. Automatic OAuth refresh (refresh_oauth_credential) runs only as system (handler-level allowlist (command_type, actor_id, source)); manual reauthorize (complete_oauth_credential in reauthorize mode) — manager+. There is no separate rotate_credential command in the current version.
‡ credential:purge (and credential:maintain) — in SYSTEM_ONLY_PERMISSIONS, not granted to humans (I-9). The purge_credential command itself (and delete_credential with deletion_reason) is not yet implemented — that's the beyond-Stage-3 backlog (CONCEPT-CONNECTORS.md §0.1); the GDPR DDL columns (deleted_at/deletion_reason/purge_after/purged_at) already exist.
The key point: manager is not "view-only". A manager in their project can create credentials, create connector instances, toggle the allowlist, configure binding profiles, and use all of it in runs. Truly "view-only" are reviewer and read_only. operator — view + test + use + start workflows, but does not create credentials/instances and does not touch the allowlist. Creating projects and users (create_project, manage_users) is owner / admin only.
credential:use ≠ the right to decrypt. It's a "project membership capability" — needed so the runtime can use the credential on the actor's behalf. The plaintext is still decrypted only in the worker; the Console shows redacted data (no tokens).
2.1 Architect / Engineer — a role outside RBAC¶
"Architect" (the programmer) is not one of the core/models/auth.py roles; it's a persona that works at the git + deploy level, not in the Console. The "code ↔ Console" boundary in this domain:
Done in code (modules/, IDE, git) |
Done in Console (as manager+) |
|---|---|
Writes connector adapters — modules/connectors/{gmail,telegram,...}/ (subclasses of ConnectorBase: API + MCP tools + normalize() + verify_signature() + action manifest) |
— |
Writes CredentialType — modules/credential_types/*.py (field format, auth_inject, OAuth2 metadata, test_request, extends) → CredentialTypeRegistry |
— |
Writes WorkflowDefinition with ConnectorRequirement[] — named slots + logical types + required_actions |
Publishes the definition (publish_definition) — if they have a Console role of manager+; otherwise a separate manager publishes |
| — | Connects credentials, creates connector instances, configures the allowlist and binding profiles |
So "a connector as a capability" and "the credential format" appear in code (architect + deploy), while "a specific credential", "an instance", "the allowlist", "the binding to a workflow" — in the Console (manager+). All adapters and CredentialTypes are shared by all projects and are not stored in the DB.
3. Where it is in Console¶
Two adjacent sections in the sidebar under the Administration group:
3.1 The "Connectors" section — /{project_id}/connectors¶
Consists of three blocks.
The "Project connector allowlist" block — a table "which connector keys are allowed in this project".
| Column / element | What it is |
|---|---|
Connector |
connector_key — a concrete connector key (e.g. gmail_oauth2_test2). Block caption: "Concrete connector keys only" — these are concrete keys here, not logical types. |
Credential |
Which credential is associated with this key directly (or No active credential if the binding lives only in a connector instance below). |
Allowlist (toggle) |
Enable/disable the connector for the project. ON = the connector is allowed (inbound webhooks on this key are accepted, the binding resolver sees the connector). OFF = fail-closed: webhooks are rejected (generic 401, no event created), the connector is unavailable for bindings. Caption: "Credentials stay intact when a connector is disabled" — disabling does not touch credentials and instances; it's a "kill switch", not "deletion". Available to manager+ (project_connector:manage); under the hood — the set_project_connector_enabled command, writes a row in app.project_connector_allowlist. |
Updated |
When the toggle was last changed. |
New credential button (top of the block) |
Jumps into the credential-creation flow (see §3.2 / §5). |
The "Connector instances" block — a table of configured connections (app.connector_instances).
| Column / element | What it is |
|---|---|
Instance |
Display name + connector_key as a subtitle (e.g. gmail_oauth2 (migrated) / gmail_oauth2_test2). The (migrated) tag — the instance was carried over from the old file-mount flow. |
Credential |
Which credential the instance references (credential_id is mandatory — an instance without a credential makes no sense). |
Adapter |
adapter_key — the concrete adapter factory (gmail, telegram, bitrix24…). |
Status |
The instance lifecycle status: active / paused / error. Below in small print — health_status: healthy / degraded / down / unknown (operational health; unknown until the runtime has reported a status). |
Actions |
The adapter's action manifest (D45): which concrete operations this instance publishes (search_email, send_email…). The workflow validator checks a step's required actions against this list. |
Manage (...) |
Menu: change display_name / config; pause / resume (status change via update_connector_instance); delete. credential_id / connector_type / adapter_key / connector_key are immutable after creation (update_connector_instance mutates only display_name/config/status) — to change the credential or adapter, create a new instance. Delete = soft-delete/tombstone (D41): the row stays for audit/replay; blocked if the instance is referenced by a binding profile or an active/pending run snapshot. The UI offers "find-and-replace" before deletion. |
Add instance button (top of the block) |
Create a new connector instance — see §5, flow 3. |
The "Connector health" block — a per-connector operational-health table. It appears after a connector has reported a runtime status (probe / actual use). Empty for now — "No connector health rows / Health appears after a connector reports runtime status". Written by the system via the internal update_connector_health command (internal, 403 from external) → app.connector_health_snapshots + connector_instances.health_status. Auto-pause of an instance on sustained down — an auto-healing-monitor feature (designed, see CONCEPT-CONNECTORS.md §0.1 backlog); in the current version a human sets the pause via update_connector_instance (status='paused').
3.2 The "Credentials" section — /{project_id}/credentials¶
A list of the project's credentials (redacted — no secrets) + the create button.
| Column / element | What it is |
|---|---|
| Display name | A human-readable name (Sales Inbox). Unique within (project, credential_type_key) among non-deleted rows. |
| Type | CredentialType (Gmail (OAuth 2.0), Generic HMAC Webhook, …) with an icon. |
| Status | active / expired / revoked / error. |
| Last test | When and with what result the last test_credential ran (success / failure). On failure — a compact error message (≤500 chars, no token leak). |
| Last used / Last refresh | When the credential was last used / the last successful OAuth refresh. As refresh_failure_count > 0 accumulates, the credential is flagged "distressed". |
| Expires | If known (refresh tokens often don't expire). |
| Actions | Test connection; Reauthorize (for OAuth — a fresh consent); Revoke (soft tombstone — takes the credential out of circulation; dependent connector instances then become unusable per D42; blocked if the credential is referenced by an active/paused workflow or an active/pending run that uses a dependent instance). A full "delete"/GDPR purge — beyond Stage 3, not yet implemented. |
New credential button |
The creation wizard — see §5, flow 2. |
4. Concepts (mental model)¶
A layered model of five entities:
CODE LAYER (shared by all projects, not in the DB)
├─ Connector adapter modules/connectors/{gmail,telegram,...}/
│ ConnectorBase: API + MCP tools + normalize() + verify_signature()
│ identity: connector_key (legacy) / adapter_key (concrete factory)
└─ CredentialType modules/credential_types/*.py → CredentialTypeRegistry
field format, auth_inject, OAuth2 metadata, test_request
inheritance: extends: ["generic_oauth2"] (a new OAuth provider ~50-100 lines)
DATA LAYER (project-scoped, app.*, mutations only via CommandEnvelope)
├─ Credential app.connector_credentials id: cred_<ulid>
│ encrypted secret (envelope: DEK under KEK, AES-256-GCM)
│ + lifecycle: status, last_test_*, last_used_at, last_refresh_at,
│ refresh_failure_count, expires_at, soft-delete + GDPR (purge_after/purged_at)
├─ ConnectorInstance app.connector_instances id: ci_<ulid>
│ = adapter_key + credential_id (NOT NULL) + connector_type (logical)
│ + connector_key (runtime registry key, unique per project)
│ + config (ONLY non-secret: polling_interval_seconds, webhook_url,
│ label_ids, allowed_chat_ids) + status + health_status
├─ project_connector_allowlist (project_id, connector_key) → enabled (SoT toggle)
└─ connector_ingress_verifiers secret projection for webhook verification (D54)
WORKFLOW-BINDING LAYER
ConnectorRequirement[] (in WorkflowDefinition, written by the engineer: "need email + crm",
named slots + logical type + required_actions)
→ WorkflowBindingProfile (admin/manager, once: main_email → ci_gmail_sales)
→ workflow_run_bindings_snapshot (pinned per run, D17/D36)
Three different "names" of a connector (don't confuse them):
- connector_type — a logical capability: email, crm, storage. This is what the workflow declares ("I need email").
- adapter_key — a concrete provider/factory: gmail, imap, smtp, bitrix24. This is what the admin picks when creating an instance.
- connector_key — the runtime registry key of a specific instance (gmail_sales, gmail_support). Unique within the project among non-deleted rows (connector_instances_unique_active_key_idx). The allowlist operates on connector_key.
The connector_type ≠ adapter_key decoupling (decision D35) means: the workflow is not tied to Gmail — it asks for "email", and the admin can satisfy that with Gmail, or IMAP, or SMTP. Action compatibility is not derived from the type: the adapter must publish an action manifest, and the validator checks a step's required_actions against it (D45) — an email connector without send_email won't satisfy a send_email step's requirement.
1:N. One credential → many instances: one Gmail account → instance gmail_sales (labels "Sales") and instance gmail_support (labels "Support"), each with its own connector_key and its own config but one credential_id. That's why connector_instances is a separate table with an FK to credential_id (the composite FK (project_id, credential_id) prevents cross-project leaks at the DB level).
Three override layers at run start (D17): profile.bindings ← case.connector_overrides ← run.binding_overrides. The profile — the baseline (90% of cases); a case override — a rare exception (multi-bot Telegram, BYOK SaaS); a run override — test/one-off runs.
5. Flows: step-by-step scenarios¶
Flow 1 — The full path: from a new project to a configured integration¶
| Phase | What's done | Who (role / permission) |
|---|---|---|
| 0. Platform code | Write a connector adapter (modules/connectors/...) and a CredentialType (modules/credential_types/...); deploy. This is shared by all projects, not done in the Console. |
Platform engineer (git + deploy) |
| 1. Create the project | An app.projects row, a basic ProjectConfig. |
owner / admin (create_project) |
| 2. Provision people | Create users, grant roles and project_scopes (e.g. manager on this project — they'll configure the rest). |
owner / admin (manage_users) |
| 3. Publish workflow definitions | The engineer writes a WorkflowDefinition in code with ConnectorRequirement[] (named slots + logical types + required_actions). Publishing via the API. On the first publish, if the project already has exactly one usable instance per required type → a binding profile is auto-created (status=complete, auto_configured=true, "Review" banner). |
Engineer (code) → publish: manager+ (publish_definition) |
| 4. Connect credentials | Console → Credentials → New credential → pick a CredentialType → display name → "Connect with Google" (OAuth consent → callback) → auto test_credential → credential saved (status=active). Repeat for each account. For high-stakes types (requires_approval=true) — a credential_proposal is created; the credential materializes only after approve. |
manager+ (credential:create); approval — reviewer/manager+ (approve) |
| 5. Create connector instances | Console → Connectors → Add instance → adapter (gmail) + credential (Sales Inbox) + non-secret config (label_ids, polling_interval_seconds, webhook_url) + display name → the create_connector_instance command → an app.connector_instances row + outbox events (connector_instance.created, and where needed an ingress-verifier projection and provider webhook registration — asynchronously in the worker). Repeat for each instance. |
manager+ (connector:manage) |
| 6. Enable connectors in the allowlist | Console → Connectors → Allowlist toggle ON for each connector_key → the set_project_connector_enabled command. Without it webhooks are fail-closed and the connector is unavailable for bindings. |
manager+ (project_connector:manage) |
| 7. Configure the binding profile | Console → Workflows → <definition> → Bindings → "Auto-map" (ConnectorMapper proposes instances by type) or pick an instance per slot manually → the set_workflow_bindings command → profile status=complete. Set-once-use-many. |
manager+ (workflow:configure_bindings) |
| 8. Run workflows | Console → Run Wizard → pick a case → bindings are pulled from the profile automatically; optionally override for this run. The run pins workflow_run_bindings_snapshot (D36, system). On a connector step the worker resolves the instance → takes the credential → decrypts it in worker memory → policy gate / approval / audit / idempotency → side effect. |
operator+ (start_workflow + credential:use) |
| 9. Operations | Automatic OAuth refresh (system, refresh_oauth_credential); reauthorize on a refresh failure (deeplink alert via Telegram → manager/admin → complete_oauth_credential); health monitoring (system → update_connector_health); revoke a credential / pause-resume-delete an instance (manager+). GDPR tooling (delete_credential with deletion_reason, hard-purge sweeper on purge_after) — beyond Stage 3, not yet implemented. |
system + manager+ |
The minimal "happy path": owner creates the project and provisions a manager → manager: New credential → Add instance → Allowlist ON → Bindings → operator: Run. If the project has one instance per type — the Bindings step is automated.
Flow 2 — Connect Gmail in ~90 seconds (manager+)¶
- Console → Credentials →
New credential. - Category "Email" → "Gmail (OAuth 2.0)".
- Display name:
Sales Inbox. Connect with Google→ Google consent screen → allow → callback to app-api → the code goes to the worker, gets exchanged for tokens, the tokens are encrypted (envelope) → anapp.connector_credentialsrow (cred_<ulid>,status=active).- Auto
test_credential(worker-side probe) →last_test_result=success→ ✅. - Done. The credential is available for creating Gmail connector instances in this project.
If the
CredentialTypehasrequires_approval=true— after step 4 acredential_proposalis created; the credential appears only afterapproveby a reviewer/manager+ (needed for production CRM, payment providers).
Flow 3 — Create a connector instance (Add instance, manager+)¶
- Console → Connectors →
Add instance. Adapter— pick the concrete adapter (gmail).Credential— pick the project's credential (Sales Inbox). Validated: same project, usable (status='active', not soft-deleted, not purged, encrypted payload/DEK present — the D42 condition);adapter_keyis supported by thisCredentialType.Display name(Sales Inbox Gmail); aconnector_keyis formed (e.g.gmail_sales) — unique within the project.Config— non-secret parameters only:label_ids(Gmail),allowed_chat_ids(Telegram),polling_interval_seconds,webhook_url. No secrets here —webhook_secret, API keys, HMAC secrets live in the encrypted credential payload; for webhook verification the system separately projectsconnector_ingress_verifiers.- Save → the
create_connector_instancecommand → anapp.connector_instancesrow + outbox:connector_instance.created, optionally…ingress_verifier_projection_requested(the worker projects the signature verifier), optionally…webhook_registration_requested(the worker callssetWebhookon the provider — for Telegram-like ones). Provider APIs are not called inline.
Flow 4 — One connector + two different credentials in one workflow¶
Scenario: a workflow sends mail both from the admin mailbox and from the support mailbox.
- The engineer declares two named requirements of the same type in the
WorkflowDefinition:ConnectorRequirement(name="admin_mailbox", connector_type="email")andConnectorRequirement(name="support_mailbox", connector_type="email"). - The workflow steps reference the right slot (
connector_requirement/connector_instance_idinstep.config). manager+: create two credentials (Admin Inbox,Support Inbox) and two instances (gmail_admin,gmail_support— bothadapter_key=gmail, bothconnector_type=email, differentconnector_key, each with its owncredential_id).manager+: in the workflow's Bindings, mapadmin_mailbox → gmail_admin,support_mailbox → gmail_support. (You can even use different adapters:admin_mailbox → Gmail,support_mailbox → IMAP, as long as both satisfy theemailtype and publish the needed actions — D45.)- At run time each step resolves its own instance → its own credential.
Constraint: you cannot bind two different credentials to the same slot in one run — a slot → exactly one
connector_instance→ exactly one credential (Mode XOR, D27). Need two mailboxes → use two slots.
Flow 5 — Reauthorize an expired OAuth (manager / admin)¶
- On a refresh failure the worker sends a deeplink alert to the owner/admin via Telegram.
- Console → Credentials → find the credential in
error/distressed state →Reauthorize. - A fresh Google consent → callback → the
complete_oauth_credentialcommand (reauthorize mode;worker_apply_then_signal, permissioncredential:rotate) → new payload (newpayload_version) → dependent ingress verifiers and caches are invalidated automatically →status=active,refresh_failure_count=0.
Flow 6 — Temporarily disable a connector (manager+)¶
Console → Connectors → the allowlist block → toggle Allowlist OFF for the needed connector_key. Effect: inbound webhooks on this key are rejected (fail-closed), the connector is unavailable for new bindings. Credentials and instances stay untouched — toggle it back ON and everything works again.
6. Options reference¶
6.1 Credential (app.connector_credentials)¶
| Field | What it does | Default / constraints |
|---|---|---|
display_name |
A human-readable name | required; unique within (project, credential_type_key) among non-deleted rows |
credential_type_key |
Which CredentialType (gmail_oauth2, …) |
required; must exist in CredentialTypeRegistry |
encrypted_data / payload_nonce / payload_auth_tag / encrypted_dek / dek_nonce / dek_auth_tag / kek_version |
The encrypted secret (envelope: payload under DEK, DEK under KEK, AES-256-GCM) | filled only in the worker; not visible in the Console; NOT NULL before purge, NULL after purge (tombstone) |
status |
active / expired / revoked / error |
changed only by commands; RAW UPDATE is forbidden |
last_test_at / last_test_result / last_test_error |
The test_credential result |
last_test_error ≤ 500 chars, no token leak |
last_used_at |
Last use | updated only by the coalesced record_credential_usage command (no more often than ~1/60s) |
last_refresh_at / refresh_failure_count / expires_at |
OAuth refresh state | refresh_failure_count resets on success; > 0 = distressed |
deleted_at / deletion_reason / purge_after / purged_at |
Soft-delete + GDPR (schema ready, commands partial) | revoke_credential sets deleted_at (soft tombstone); deletion_reason ∈ {user_request, project_archived, gdpr_erasure, incident}; delete_credential-with-deletion_reason and the hard-purge sweeper on purge_after (purged_at zeroes payload/DEK) — beyond Stage 3, not yet implemented |
version |
Optimistic concurrency | all mutating commands require expected_version |
6.2 Connector instance (app.connector_instances)¶
| Field | What it does | Default / constraints |
|---|---|---|
connector_type |
A logical capability (email/crm/storage) — matches ConnectorRequirement.connector_type |
required; email ≠ gmail |
adapter_key |
The concrete adapter factory (gmail/telegram/bitrix24) |
required; must be supported by the credential's CredentialType |
connector_key |
The runtime registry key (gmail_sales) |
unique within the project among non-deleted rows |
credential_id |
Which credential to use | NOT NULL; composite FK (project_id, credential_id) |
display_name |
The name in the UI | required |
config (JSONB) |
Non-secret parameters only | polling_interval_seconds, webhook_url, label_ids (Gmail), allowed_chat_ids (Telegram), … Secrets here are forbidden (the handler runs display_name/config through credential-scan guards) |
status |
active / paused / error |
changed via update_connector_instance (connector:manage). credential_id/connector_type/adapter_key/connector_key — immutable after creation |
health_status |
healthy / degraded / down / unknown |
written by the system via the internal update_connector_health command |
deleted_at / deletion_reason |
Soft-delete/tombstone (D41) | deletion_reason ∈ {user_request, project_archived, migration}; delete_connector_instance is blocked if referenced by a binding profile or an active/pending run snapshot; no restore command |
When an instance is considered "usable" (D42): not tombstoned (deleted_at IS NULL) + status='active' + the backing credential is in the same project + credential.status='active' + not soft-deleted + not purged + encrypted payload/DEK present. Any validator that accepts a connector_instance_id checks this composite condition, not just ci.status.
6.3 Project connector allowlist (app.project_connector_allowlist)¶
| Field | What it does |
|---|---|
(project_id, connector_key) |
Natural key — which connector key the toggle applies to |
enabled (bool) |
The SoT flag "the connector is allowed in the project". The DB row beats the legacy YAML enabled_connectors. No row + LEGACY_YAML_CONNECTORS=false → the connector is considered disabled (fail-closed) |
Changed by the set_project_connector_enabled command (sync_apply, public, project_connector:manage); read via the read.project_connector_allowlist projection (the Console never reads app.* directly).
6.4 Related commands (CommandEnvelope)¶
Column values are taken from core/api/commands/execution_paths.py (path), core/api/commands/policy.py (side-effect class), _COMMAND_PERMISSION_MAP there (permission). "secret-bearing" is a separate note that the handler decrypts/handles secret material (hence worker_apply_then_signal).
| Command | Execution path | Side-effect class | Permission | Access | Purpose |
|---|---|---|---|---|---|
create_credential |
worker_apply_then_signal (secret-bearing) |
internal |
credential:create |
manager+ | Create a credential; encryption/provider — only in the worker |
update_credential |
dual-path: sync_apply (metadata-only) / worker_apply_then_signal (if the payload has credential_payload_ref — secret-bearing) |
internal |
credential:update |
manager+ | Update metadata / payload; connector_key immutable |
revoke_credential |
sync_apply |
internal |
credential:revoke |
manager+ | Soft tombstone (sets deleted_at + retention purge_after), encrypted payload retained; dependent CIs become unusable (D42); blocked while an active/paused workflow or an active/pending run on a dependent CI exists |
test_credential |
worker_apply_then_signal (secret-bearing) |
external_low |
credential:test |
operator+ | Provider probe (adapter.health_check()) — only in the worker |
complete_oauth_credential |
worker_apply_then_signal (secret-bearing) |
external_low |
credential:create (purpose=create) / credential:rotate (purpose=reauthorize) |
internal-only (dispatched by the OAuth callback; the user's grant is re-checked in the worker) | Complete the OAuth callback / reauthorize |
refresh_oauth_credential |
worker_apply_then_signal (secret-bearing) |
external_low |
credential:rotate |
internal-only (system actor allowlist) | Auto-refresh the OAuth token |
mark_credential_distressed / mark_credential_expired / record_credential_usage |
sync_apply |
internal |
credential:maintain |
internal-only (system actor allowlist) | Lifecycle/telemetry (D40); record_credential_usage coalesced ≈1/60s |
create_connector_instance / update_connector_instance / delete_connector_instance |
sync_apply |
internal |
connector:manage |
manager+ | Instance CRUD; update mutates only display_name/config/status; delete = soft-tombstone (blocked if referenced by a binding profile or an active/pending run snapshot); create emits outbox events (does not call the provider inline) |
update_connector_health |
sync_apply |
— (not in the credential side-effect registry) | edit_project |
internal-only (403 from external) | Health snapshot (D40) |
set_project_connector_enabled |
sync_apply |
internal |
project_connector:manage |
manager+ (public) | Allowlist toggle |
set_workflow_bindings / clear_workflow_bindings / auto_map_workflow_bindings |
sync_apply |
internal |
workflow:configure_bindings |
manager+ | Binding profile |
upsert_connector_ingress_verifier |
worker_apply_then_signal (secret-bearing) |
internal |
ingress_verifier:manage |
internal-only (system actor allowlist) | Webhook verifier projection (D54) — decrypts the durable credential payload |
disable_connector_ingress_verifier |
sync_apply |
internal |
ingress_verifier:manage |
internal-only (system actor allowlist) | Disable a verifier (D54) |
pin_workflow_run_bindings |
sync_apply |
internal |
workflow_run:pin_bindings |
internal-only (sub-step of start_workflow dispatch) |
Pins the run's binding snapshot (D36) |
Not implemented in the current version (beyond Stage 3 / permission reserved):
delete_credential(soft-delete withdeletion_reason),purge_credential(hard-purge sweeper, permissioncredential:purge), a separaterotate_credential, a separatepause_connector_instance(pause is viaupdate_connector_instance). SeeCONCEPT-CONNECTORS.md§0.1 backlog.
7. Lifecycle and maintenance¶
Credential status machine: active → (expired | revoked | error). revoked/expired immediately makes all dependent connector instances unusable (D42), even if referenced by explicit ID or a run override. The CI itself is not mutated — pausing an instance (if needed) is done by a separate update_connector_instance command (status='paused').
OAuth refresh (automatic): system only (handler-level actor allowlist; permission credential:rotate), the refresh_oauth_credential command. Success → last_refresh_at, refresh_failure_count=0. Failure → refresh_failure_count++; at a threshold (see credential_refresh_dispatcher) → mark_credential_distressed + a deeplink alert via Telegram → a manual Reauthorize (Flow 5, complete_oauth_credential).
Health monitoring: scheduled probe → update_connector_health → connector_health_snapshots + connector_instances.health_status (audit is written only on a status transition). Auto-pause of an instance on sustained down — an auto-healing-monitor feature (designed, see CONCEPT-CONNECTORS.md §0.1 backlog); in the current version a human sets the pause.
Taking out of circulation:
- a credential — revoke_credential (soft tombstone, status→revoked, deleted_at set, encrypted payload retained for audit/possible re-enable; dependent CIs become unusable per D42). Blocked if the credential is referenced by an active/paused workflow or an active/pending run that uses a dependent instance — finish/cancel those first;
- a connector instance — delete_connector_instance (soft/tombstone, D41); the row stays for audit/replay; blocked if referenced by a binding profile or an active/pending run snapshot; the D54 verifier is disabled before deletion; the UI offers find-and-replace;
- a full delete_credential (with deletion_reason, cascade-disable of D54 verifiers) and a hard-purge sweeper on purge_after (purged_at zeroes payload/DEK, the tombstone row stays) — beyond Stage 3, not yet implemented (DDL columns already exist).
Encryption-key rotation: versioned (kek_version); re-encrypt — a maintenance CLI (worker-side), the runbook is operator-driven. (Full rotation orchestration — beyond-scope, see CONCEPT-CONNECTORS.md §0.1 / §26.)
8. Troubleshooting¶
| Symptom | Cause | What to do |
|---|---|---|
In the allowlist block a connector shows No active credential |
No credential is directly associated with the connector_key in the allowlist table — the binding lives in a connector instance |
This is fine if a connector instance with the needed credential_id exists. Check the "Connector instances" block. |
An inbound webhook returns 401 (generic) |
The connector is disabled in the allowlist / no ingress verifier / the verifier is stale (credential_secret_version ≠ payload_version) / the signature didn't match |
Set Allowlist ON; check the credential is active and not distressed; Reauthorize if needed (that re-projects the verifier via outbox). The specific cause is in the audit (it does not leak into the response body). |
Add instance rejects the chosen credential |
The credential is from another project / not usable (expired/revoked/soft-deleted/purged) / adapter_key is not supported by this CredentialType |
Use an active credential from the same project; check adapter_key is in the type's supported_adapter_keys. |
| A workflow won't start — "binding profile incomplete" | Not all ConnectorRequirement slots are mapped to usable instances |
Console → Workflows → <definition> → Bindings → "Auto-map" or pick an instance per slot manually; make sure instances are usable (D42) and the connectors are Allowlist ON. |
Revoke of a credential fails ("credential_in_use") |
It is referenced by an active/paused workflow or an active/pending run that uses a dependent instance | Finish/cancel those workflows/runs, then Revoke. Idle instances do not block revoke — they just become unusable per D42. (A full "delete"/GDPR purge is not yet implemented — use Revoke to take a credential out of circulation.) |
| Can't delete a connector instance | It is referenced by a binding profile or an active/pending run snapshot | Re-bind the slot in the binding profile to another instance (the UI offers find-and-replace); wait for active runs to finish. |
A credential is in error state, OAuth isn't refreshing |
refresh_failure_count crossed the threshold (distressed) |
Reauthorize (a fresh consent). See Flow 5. A deeplink alert should arrive via Telegram. |
A send_email step fails "action not available" |
The instance under the slot doesn't publish this action (the action manifest, D45) | Map the slot to an adapter instance that publishes send_email (visible in the Actions column). |
9. Constraints and invariants¶
- App-api never sees the plaintext secret (I-1). The durable KEK
AXON_CREDENTIAL_ENCRYPTION_KEYis mounted only in app-worker / the maintenance CLI; in app-api this variable is forbidden. Secret-bearing credential commands go viaworker_apply_then_signal—handler.apply()(decryption/provider) runs only in the worker. App-api gets only a transient secret-ref key (AXON_TRANSIENT_SECRET_REF_KEY) and the public OAuth descriptor. - The OAuth
client_secretis also worker-side (I-20, Strategy A). It is not in the Console or in logs. - The webhook ingress verification key is a separate root (
AXON_INGRESS_VERIFICATION_KEY), mounted in app-api + app-worker, used only for theconnector_ingress_verifiersprojection (HKDF SHA-256 subkeys per RFC 5869). It does not intersect with the durable KEK and does not decrypt the credential payload (invariant I-DOMAIN-CONNECTORS-STAGE2-INGRESS-KEY). - Secrets are never in
connector_instance.configor instep.config. Only in the encrypted credential payload / a credential proposal. - Project-scope hard isolation. A credential can't be shared across projects. Need one Gmail in two projects → two credential rows. The composite FK
(project_id, credential_id)prevents cross-project leaks at the DB level. - Mode XOR (D27). In a connector-backed
step.config, exactly one ofconnector_requirement/connector_instance_id/connector_key. More than one → publish-time 422 + runtimeValueError. - Fail-closed. Connector disabled in the allowlist / verifier missing / verifier stale / signature mismatch → the webhook is rejected, no
inbound_eventsrow is created, generic 401, the reason is only in the audit. maintain/purge/ingress_verifier:manage— system-only (I-9). Auto-healing and hard-purge are system behaviours by design; these permissions are not granted to humans (HUMAN ∩ SYSTEM_ONLY = ∅).- All mutations go via CommandEnvelope. RAW UPDATE on
app.connector_*is forbidden; operator YAML edits of connectors (legacy file-mount) bypass the invariant and are scheduled for removal (the V6.4 GA window). - Ambiguous ingress binding → reject, not "a random project". If one inbox/bot is bound to several projects and the hints don't resolve it — the event is written with
project_id=NULL,resolution_status='ambiguous'; an operator resolves it manually. Cross-project data leakage is forbidden. - Connector-as-Workflow (conceptually "everything is a workflow") is not done in V6.2 — ConnectorInstance stays a separate entity; see
CONCEPT-CONNECTORS.md§26.
10. Related manuals and canon¶
- Roles-And-Permissions.md — the full role/permission matrix.
- Workflows.md —
ConnectorRequirement[], binding profiles, Run Wizard, overrides. - Security-And-Audit.md — handling secrets, audit log, explainability.
- First-Project-Walkthrough.md — the new-project end-to-end scenario (includes Flow 1 of this manual).
- Canon:
ARCHITECTURE-V6.md§11 (Connectors and Tools);CONCEPT-CONNECTORS.md§1-§5/§10/§22 (long-form architecture);CONCEPT-CONNECTORS-3-STAGES.md(stages);CONCEPT-CONNECTORS-SECOND.md(Tier 2+, coverage expansion);OWNERSHIP-MAP.md(zones 1/4/5);core/models/auth.py(RBAC).