Skip to content

docs(workflow-executor): document FOREST_EXECUTOR_ENCRYPTION_KEY#1725

Merged
hercemer42 merged 1 commit into
mainfrom
docs/prd-367-pr4-executor-encryption-key
Jul 7, 2026
Merged

docs(workflow-executor): document FOREST_EXECUTOR_ENCRYPTION_KEY#1725
hercemer42 merged 1 commit into
mainfrom
docs/prd-367-pr4-executor-encryption-key

Conversation

@hercemer42

@hercemer42 hercemer42 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Documents FOREST_EXECUTOR_ENCRYPTION_KEY in the workflow-executor README — a new "OAuth-protected MCP connectors" section covering:

  • what it encrypts (the OAuth credentials the executor stores, AES-256-GCM at rest)
  • how to generate it (openssl rand -hex 32) and to keep it separate from FOREST_AUTH_SECRET
  • using the same value across instances that share a database
  • its lazy behaviour (while unset, credential storage returns 503 and those connectors stay unavailable; the executor still boots, with a startup warning)
  • no managed rotation — changing it forces affected users to reconnect

It complements the one-line entry already in .env.example.

Why

PRD-626 (the docs half of PRD-367): operators deploying the executor with OAuth-protected MCP connectors need a README reference for this variable.

Base / merge order

Based on the PRD-692 branch (feature/prd-692-harden-executor-oauth-runtime-clear-idempotency-phase-on, #1724) — the top of the OAuth-MCP executor stack, where FOREST_EXECUTOR_ENCRYPTION_KEY and the code that reads it already live. Targeting that branch keeps this diff to just the README change. Intended to merge last, once PR1 (#1619), PR2 (#1665) and PRD-692 (#1724) are in.

Replaces docs-site PR ForestAdmin/docs#3 (now closed) — the executor's own README, next to the code and .env.example, is the right home for an operator-facing deployment variable (and "executor" isn't a concept on the public docs site).

Refs: PRD-626

🤖 Generated with Claude Code

Note

Document FOREST_EXECUTOR_ENCRYPTION_KEY in workflow-executor README

Adds an 'OAuth-protected MCP connectors' section to README.md explaining how the executor encrypts per-user OAuth credentials at rest using FOREST_EXECUTOR_ENCRYPTION_KEY. Covers key generation, the requirement to use a separate secret from FOREST_AUTH_SECRET, that the key is only needed when OAuth-protected connectors are in use, and that all instances sharing a database must use the same key.

Macroscope summarized 1087b4a.

@linear-code

linear-code Bot commented Jun 30, 2026

Copy link
Copy Markdown

PRD-367

PRD-626

@qltysh

qltysh Bot commented Jun 30, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@Scra3 Scra3 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs review (PRD-367 PR4). The section is accurate against the implementation; one small suggestion.

Comment thread packages/workflow-executor/README.md Outdated

| Variable | Description |
| --- | --- |
| `FOREST_EXECUTOR_ENCRYPTION_KEY` | At-rest encryption key (AES-256-GCM) for the OAuth credentials the executor stores. Generate with `openssl rand -hex 32`. Use a **separate** secret from `FOREST_AUTH_SECRET`. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: unlike FOREST_ENV_SECRET, the key's length/format is not validated by the code — worth noting operators must use the exact openssl rand -hex 32 output, since a weak value is accepted silently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed against the code: deriveKey() only throws on an empty value and otherwise feeds the secret straight into hkdfSync, so length/format aren't validated and a weak value is accepted silently.

Added a note to the entry: "The value isn't validated, so use a genuinely random secret."

I framed it around entropy rather than the exact openssl rand -hex 32 output, since the code accepts any non-empty secret (HKDF), not only hex — so the real requirement is a high-entropy value. Thanks for the review!

Base automatically changed from feature/prd-692-harden-executor-oauth-runtime-clear-idempotency-phase-on to feat/prd-367-pr2-executor-oauth-runtime July 6, 2026 09:29
Base automatically changed from feat/prd-367-pr2-executor-oauth-runtime to feat/prd-367-pr1-executor-oauth-credentials July 6, 2026 09:30
@qltysh

qltysh Bot commented Jul 6, 2026

Copy link
Copy Markdown

All good ✅

Comment thread packages/workflow-executor/src/executors/mcp-step-executor.ts Outdated
Comment thread packages/workflow-executor/src/stores/database-mcp-oauth-credentials-store.ts Outdated
@hercemer42 hercemer42 force-pushed the feat/prd-367-pr1-executor-oauth-credentials branch from 0783047 to d3dbde6 Compare July 6, 2026 09:52
return links;
}

export function isMcpAuthError(error: unknown): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/mcp-auth-error.ts:42

isMcpAuthError returns true for an error whose message matches /unauthorized/i even when a later link in the cause chain carries an explicit 403 status. For example, new Error('Unauthorized scope', { cause: Object.assign(new Error('forbidden'), { status: 403 }) }) is classified as refreshable auth, because .some() returns at the outer message match before the inner 403 is inspected. The comment states an explicit status is authoritative, but the implementation lets a message match short-circuit before any later status is checked. Consider scanning the full chain for an explicit non-refreshable status before falling back to message matching.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/ai-proxy/src/mcp-auth-error.ts around line 42:

`isMcpAuthError` returns `true` for an error whose message matches `/unauthorized/i` even when a later link in the cause chain carries an explicit `403` status. For example, `new Error('Unauthorized scope', { cause: Object.assign(new Error('forbidden'), { status: 403 }) })` is classified as refreshable auth, because `.some()` returns at the outer message match before the inner `403` is inspected. The comment states an explicit status is authoritative, but the implementation lets a message match short-circuit before any later status is checked. Consider scanning the full chain for an explicit non-refreshable status before falling back to message matching.

Comment thread packages/workflow-executor/src/oauth/token-service.ts
Comment thread packages/workflow-executor/src/oauth/token-service.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

createWorkflowExecutor.start() calls runner.start() before server.start(), so the polling loop can invoke DatabaseMcpOAuthCredentialsStore.get() — which issues SELECT against ai_mcp_oauth_credentials — before ExecutorHttpServer.start() runs mcpOAuthCredentialsStore.init() to create the table. On startup with a pending OAuth-backed step, the first poll cycle fails with a missing-table error instead of executing the run. Consider awaiting the store migration before runner.start(), or calling init() explicitly in buildDatabaseExecutor prior to starting the runner.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/workflow-executor/src/build-workflow-executor.ts around line 198:

`createWorkflowExecutor.start()` calls `runner.start()` before `server.start()`, so the polling loop can invoke `DatabaseMcpOAuthCredentialsStore.get()` — which issues `SELECT` against `ai_mcp_oauth_credentials` — before `ExecutorHttpServer.start()` runs `mcpOAuthCredentialsStore.init()` to create the table. On startup with a pending OAuth-backed step, the first poll cycle fails with a missing-table error instead of executing the run. Consider awaiting the store migration before `runner.start()`, or calling `init()` explicitly in `buildDatabaseExecutor` prior to starting the runner.

Comment thread packages/workflow-executor/src/oauth/refresh-grant.ts Outdated
Base automatically changed from feat/prd-367-pr1-executor-oauth-credentials to main July 7, 2026 07:37
Comment thread packages/workflow-executor/src/oauth/refresh-grant.ts
Comment thread packages/workflow-executor/src/oauth/token-service.ts
@hercemer42 hercemer42 force-pushed the docs/prd-367-pr4-executor-encryption-key branch 2 times, most recently from 4305503 to 1dc94e2 Compare July 7, 2026 08:18
Document the FOREST_EXECUTOR_ENCRYPTION_KEY environment variable in the workflow-executor README: what it encrypts, how to generate it, using the same value across instances sharing a database, its lazy behaviour, and that it has no managed rotation. Complements the .env.example entry.

Refs: PRD-626
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hercemer42 hercemer42 force-pushed the docs/prd-367-pr4-executor-encryption-key branch from 1dc94e2 to 1087b4a Compare July 7, 2026 08:21
@hercemer42 hercemer42 merged commit b0b2b18 into main Jul 7, 2026
37 checks passed
@hercemer42 hercemer42 deleted the docs/prd-367-pr4-executor-encryption-key branch July 7, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants