This handbook covers deployment, encrypted Feishu credentials, quota tuning, incident diagnosis, and Release rollback for the multi-tenant Published-Agent platform.
For application developers integrating through the Agent API, see the Chinese Published Agent API external integration guide.
Operators should preserve these invariants during every deployment:
agent_idis the stable public identity. External routes, Agent API Keys, Feishu bindings, and Conversation mappings attach to it, not to a Release.- A Release is immutable. Publishing creates a new Release and atomically moves
current_release_id; rollback only moves that pointer. - Saving a draft never changes an already published runtime.
- External Runs are memory-free and cannot select an internal Release, model, Skill revision, Connector grant, owner, or runtime configuration.
- Platform limits are hard caps. Owner and Key overrides may only tighten them.
- SQL stores only Feishu
secret_refvalues. Plaintext credentials must never appear in database dumps, application logs, audit responses, or support tickets.
Published Agents require application persistence. SQLite is suitable for a single-node deployment; PostgreSQL is the production choice when the rest of the application requires multi-node database access.
# config.yaml — single node
database:
backend: sqlite
sqlite_dir: .deer-flow/data# config.yaml — PostgreSQL
database:
backend: postgres
postgres_url: $DATABASE_URLFor PostgreSQL, install the postgres backend extra and inject
DATABASE_URL through the deployment secret manager. Gateway startup applies
the Alembic migration chain automatically. Back up the database before an
upgrade and verify that startup reaches the current migration head before
accepting authoring or external traffic.
database.backend: memory does not provide a durable Published-Agent control
plane and must not be used for production publishing.
The complete hard-cap configuration is:
publishing:
platform_quota:
max_concurrent_runs_per_agent: 8
max_input_bytes: 262144
max_run_seconds: 600
max_tokens_per_run: 200000
inbound_rps: 20
daily_runs_default: 1000
daily_tokens_default: 2000000
model_costs:
model-name-from-config:
input_usd_per_million_tokens: 2.50
output_usd_per_million_tokens: 10.00All values must be positive integers. A platform quota change requires a Gateway restart. Existing immutable Releases retain their owner overrides, but the resolver applies the current platform hard cap to every new Run.
model_costs is optional and keyed by the configured model name. The
operations view estimates terminal Run cost from these deployment rates. An
unconfigured model reports zero cost rather than inventing a price. Pricing
changes affect dashboard aggregation from the next Gateway restart.
Published Feishu bindings require a stable Fernet key:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"Store the result in the deployment secret manager as
DEER_FLOW_SECRET_STORE_KEY. Do not commit it to .env, config.yaml, or a
container image.
The first version permits exactly one Gateway process to own the Published Feishu Supervisor. It acquires:
${DEER_FLOW_HOME:-.deer-flow}/published-feishu-supervisor.lock
Stop the old Supervisor process cleanly before starting its replacement. Do not run multiple independent Gateway replicas with Published Feishu enabled until distributed Supervisor leasing is implemented.
Agent Key rotation overlap defaults to 24 hours and can be changed with:
AGENT_API_KEY_ROTATION_OVERLAP_SECONDS
Use 0 only for emergency rotation where immediate predecessor expiry is
intentional.
After startup:
- Open the owner Gallery and confirm each tenant sees only its own Agents.
- Open one published Agent and confirm Release history, quota policy, usage, operations metrics, and rejection audit load.
- Create a short-lived Agent Key and store its plaintext immediately.
- Create a test Conversation through the stable public API.
- If Feishu is enabled, run Test connection, then Start, and wait for
status=activewithhealth=healthy. - Revoke the short-lived test Key.
Public API smoke test:
export DEER_FLOW_URL="https://deerflow.example.com"
export AGENT_ID="pa_..."
export AGENT_API_KEY="dfa_..."
curl --request GET \
"$DEER_FLOW_URL/api/v1/agents/$AGENT_ID/capabilities" \
--header "Authorization: Bearer $AGENT_API_KEY"
curl --request POST \
"$DEER_FLOW_URL/api/v1/agents/$AGENT_ID/conversations" \
--header "Authorization: Bearer $AGENT_API_KEY" \
--header "Content-Type: application/json" \
--data '{"external_conversation_id":"deployment-smoke"}'
export CONVERSATION_ID="conversation-id-from-the-response"
curl --request POST \
"$DEER_FLOW_URL/api/v1/agents/$AGENT_ID/conversations/$CONVERSATION_ID/runs/wait" \
--header "Authorization: Bearer $AGENT_API_KEY" \
--header "Content-Type: application/json" \
--data '{"message":"Reply with deployment-ok"}'The capabilities response contains sorted frozen Skill technical names,
localized display names, Chinese-preferred descriptions, and at most one
currently usable model. Localized Skill fields come from the immutable
SKILL.md snapshot and fall back to the technical name or English description.
The model entry exposes only its DeerFlow config alias, display name, and
thinking/reasoning/vision flags; it does not make the model selectable in Run
requests. If the Release model is no longer present in the owner's effective
configuration, models is empty.
Public responses must not contain release_id, provider model IDs/config,
internal instructions, Skill content or revision IDs, Connector details,
credential IDs, or thread IDs.
The local encrypted store lives under:
${DEER_FLOW_HOME:-.deer-flow}/secret-store/feishu/
Each SQL binding row contains an opaque reference such as
secret://feishu/<random-id>. Ciphertext files are sharded below the directory.
.pending/ contains non-secret ownership metadata used to recover a crash
between ciphertext creation and database transfer.
Back up the following as one consistency group:
- the application database;
- the complete Feishu SecretStore directory;
- the deployment reference to
DEER_FLOW_SECRET_STORE_KEY.
The key itself should remain in the external secret manager, not in the backup archive.
The current release does not provide an in-place master-key re-encryption
command. Changing DEER_FLOW_SECRET_STORE_KEY without re-encrypting every
existing ciphertext makes all existing Feishu bindings unreadable.
If master-key rotation is required:
- Schedule a maintenance window and stop the Gateway.
- Take a consistent database and ciphertext backup.
- Keep the old key available for rollback.
- Use an audited re-encryption procedure or recreate every binding credential before retiring the old key.
- Start the Gateway with the new key and test every binding.
- Retire the old key only after all bindings are healthy and the rollback window has closed.
If the key is lost, SQL secret_ref values cannot recover the plaintext.
Provision new Feishu credentials and replace each affected binding.
Use the Studio Rotate credentials action. It writes a new encrypted bundle,
advances the runtime generation, stops the old transport, and restarts the
binding when appropriate. Never edit secret_ref or ciphertext files manually.
After rotation:
- confirm the binding health returns to
healthy; - send one private-chat and one group-chat smoke message;
- confirm no secret value appears in logs or the owner audit view;
- retain the old application credential only for the organization's approved rollback window.
Quota resolution is a monotonic minimum:
platform hard cap → Release owner override → Agent Key override
A blank owner or Key field means “inherit the previous bounded value.” It never means unlimited. The seven editable fields are:
| Field | Meaning |
|---|---|
max_concurrent_runs |
Simultaneous Runs for the Agent or credential |
daily_runs |
UTC-day Run admissions |
daily_tokens |
UTC-day settled token budget |
max_run_seconds |
Per-Run wall-clock limit |
max_tokens_per_run |
Per-Run model-token ceiling |
max_input_bytes |
Maximum admitted text plus attachment bytes |
inbound_rps |
One-second inbound request rate |
max_tokens_per_run is additionally bounded by the effective
daily_tokens value.
Owner quota edits are draft state and take effect only after publishing the next Release. Key quota edits apply to subsequent requests without republishing.
- Review 7-, 30-, and 90-day usage in Studio, split by API/Feishu and Key.
- Check recent metadata-only rejection events:
429indicates quota admission;401indicates authentication;403indicates capability policy.
- Decide whether the limit is a platform safety boundary, an Agent-wide product limit, or a customer/Key-specific limit.
- Raise only the narrowest appropriate layer. Lower limits gradually.
- For owner changes, save the draft, review the Release diff, and publish.
- Repeat the smoke test and watch error rate, latency, concurrent reservations, and token settlement.
Quota rejection occurs before Run creation. Retrying the same idempotent API request or Feishu event cannot create a duplicate Run or duplicate usage row.
The owner dashboard is a metadata-only operational read model. It includes:
- Run, Token, configured cost, and terminal status by UTC day;
- quota rejection and
max_concurrent_runssaturation counts; - active/unhealthy Feishu binding counts and Feishu event-ingress-to-Run-dispatch average/P95 latency (model/Run execution time is excluded);
- Connector execution failures and authorization denials;
- Run/error counts and error rate for the Agent's current Release.
Cost is calculated and displayed as integer micro-USD at the API boundary to avoid floating-point currency drift. Release IDs are used only inside the trusted owner operations response; external Agent API responses still never expose them. The dashboard does not expose messages, prompts, model output, actor identities, IP hashes, credentials, or Connector request payloads.
Studio's Sandbox tab calls:
POST /api/published-agents/{agent_id}/draft/sandbox-runs
The endpoint freezes the saved database draft revision before creating the Run.
It does not resolve the current Release, create a Published quota reservation,
or write a Published usage record, and its response is marked billable=false.
Use it to verify unpublished instructions, model selection, Skills, and
Connector selection. Saving or running a draft never changes the live
current_release_id.
Opening the sandbox conversation loads its owner-only scope from:
GET /api/published-agents/draft/sandbox-threads/{thread_id}
The Thread also retains the Agent slug and display name so chat history can
route it back to the Agent chat and show an icon/name badge beside its title.
The composer displays only the frozen Skill names and Connector instances.
Every follow-up Run restores the same server-owned capability map; client
Thread metadata and Run bodies cannot widen it. If the saved draft revision
changes, the old conversation fails closed with
draft_sandbox_revision_stale (HTTP 409). Start a new sandbox Run to test the
new revision.
Agent publish state and binding health are deliberately separate. A broken Feishu application does not unpublish the Agent or interrupt its API Keys.
Use this sequence:
- Confirm the Agent is published and not suspended or archived.
- Confirm
DEER_FLOW_SECRET_STORE_KEYis present and unchanged. - Open Integrations, run Test connection, and record only the redacted health detail.
- Confirm the Feishu application has the expected App ID, event permissions, verification token, and WebSocket/event-delivery configuration.
- If credentials changed, use Rotate credentials.
- Run Start or Restart and wait for
healthy. - Send a new event with a new Feishu event ID; repeated delivery of the same event is expected to be deduplicated.
| Symptom | Interpretation and action |
|---|---|
Binding routes return 503 |
SecretStore key is missing/invalid or the Supervisor did not start. Fix configuration and restart the single owner process. |
status=inactive, health=unknown |
The binding exists but is not requested to run. Test it, then start it. |
status=active, health=starting |
Startup is still converging. Wait through the normal startup deadline before restarting. |
health=unhealthy |
Use the redacted detail to correct credentials, permissions, or transport configuration, then test and restart. |
409 during start/restart/delete |
A fenced lifecycle or cleanup operation is still converging. Do not create a duplicate binding; retry after the existing owner finishes. |
| Private users share a Conversation | Treat as a mapping invariant incident. Stop the binding and preserve database evidence. Private chats must be isolated by user. |
| Group replies split unexpectedly | Check whether Feishu topic IDs differ. Group topics intentionally map to separate Conversations. |
| One binding fails while peers stay healthy | Expected failure isolation. Diagnose only the failed binding; do not restart all Agents. |
Never paste app_secret, verification_token, encrypt_key, Agent Key
plaintext, raw user content, IP hashes, or actor hashes into a ticket. Use
Agent ID, binding ID, request/correlation ID, status, health, action, and
timestamp.
Use rollback for a bad instruction, model, Skill snapshot, tool group, quota override, or Connector grant introduced by a Release.
- Suspend the Agent only if new traffic must stop immediately. Suspending is a lifecycle action and does not delete Releases, Keys, bindings, or mappings.
- In Studio Publish, compare the current Release with the target historical Release.
- Confirm the target has the intended instructions, pinned Skill revisions, model, tool groups, Connector grants, and owner quota.
- Select Roll back, review the stable-identity warning, and confirm.
- Verify
current_release_idnow points to the selected historical Release. - Run an API smoke request and, if configured, a Feishu private/group smoke request.
- Resume the Agent if it was suspended.
Rollback preserves:
- Agent ID, slug, and
/api/v1/agents/{agent_id}route; - existing Agent API Keys and their quota overrides;
- Feishu binding identity and encrypted credentials;
- existing API and Feishu Conversation mappings;
- all historical Releases and the current mutable draft.
Do not revoke Keys or recreate Feishu bindings solely because of a Release rollback. If the bad Release exposed a credential or otherwise caused a security incident, rotate/revoke the affected credential as a separate incident action.
Rollback does not rewrite the draft. After service is restored, correct the draft and publish a new Release; do not try to mutate or delete the historical bad Release.
The final acceptance gate maps directly to design §19:
cd backend
uv run pytest tests/test_acceptance_multi_tenant.py -q
cd ../frontend
pnpm exec playwright test tests/e2e/multi-tenant-acceptance.spec.tsBefore production rollout, also run:
cd backend
make test
cd ../frontend
pnpm check
pnpm test
pnpm test:e2eThe acceptance suite covers tenant isolation, instruction combinations, draft isolation, Skill revision pinning, least-privilege Connector grants, late Key and Feishu setup, Conversation isolation, stable identity across publish and rollback, Release non-disclosure, multi-Key lifecycle, pre-Run quota rejection, API/Feishu idempotency, and cross-Agent failure isolation.