Skip to content

feat(actions): disclose requiredPlan on action schemas and document /api/features - #2362

Open
subheeksh5599 wants to merge 2 commits into
KeeperHub:stagingfrom
subheeksh5599:fix/2279-required-plan-disclosure
Open

feat(actions): disclose requiredPlan on action schemas and document /api/features#2362
subheeksh5599 wants to merge 2 commits into
KeeperHub:stagingfrom
subheeksh5599:fix/2279-required-plan-disclosure

Conversation

@subheeksh5599

Copy link
Copy Markdown
Contributor

Fixes #2279.

What

An agent building a workflow over REST or MCP cannot see which actions require a paid plan until POST /api/workflows/create rejects the finished workflow with upgrade_required. The action catalog's wire projection (ActionSchema) carried no plan field, and the /api/features snapshot that would answer the question was undocumented with no pointer from any agent-facing page.

lib/action-schemas/builder.ts - ActionSchema now carries requiredPlan: string | null. Both write sites populate it through resolveActionFeature, not the static FEATURES table, so the egress-derived catch-all gate is included (a plugin like blockscout that lets the user point at a destination of their choosing is pro-gated with no registry entry naming its action types):

  • transformPluginAction (plugin actions): resolveActionFeature(actionType)?.requiredPlan ?? null
  • system actions: each SYSTEM_ACTIONS entry is copied and enriched with its resolved requirement (the shared constant is not mutated; ungated system actions like Condition / For Each resolve to null)

The field emits the static requirement, never the caller's plan, so GET /api/mcp/schemas stays anonymous and publicly cacheable (same precedent as app/api/workflows/public/route.ts which already returns requiredPlan to unauthenticated callers).

lib/mcp/tools.ts - search_protocol_actions compact projection now carries requiredPlan through (its local type dropped unknown fields, so this is the one place the new field would otherwise vanish).

docs/api/workflows.md - documents requiredPlan on the action-schema response and documents GET /api/features (the org feature snapshot: plan, enabled feature ids, full registry) for the first time.

specs/api-coverage.json regenerated (the new /api/features section is now fence-owned by workflows.md).

Test

tests/unit/action-schema-required-plan.test.ts: explicitly gated plugin action (code/run-code -> pro), egress-gated plugin action with no FEATURES entry (blockscout -> pro, the case a table-only lookup misses), ungated plugin action (null), gated system actions (HTTP Request / Database Query -> pro), ungated system actions (null). 5 tests; related suites (features-egress-invariant, action-output-fields, action-config-helpers) pass.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

About the build check on this pull request

This pull request comes from a fork, so GitHub does not pass it the credentials build normally uses for our image registry cache and staging build configuration. The build still runs and still compiles the image, so a red build here is real; it just takes longer than on team branches.

Every workflow run on a pull request from a fork also waits for a maintainer to approve it, so checks can sit at "awaiting approval" for a while after each push. Nothing is needed from you for either of these.

@joelorzet joelorzet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The shape is right. Resolving through resolveActionFeature rather than the static FEATURES table is the detail that makes this correct, because a table-only lookup misses every plugin gated by the egress catch-all. Blockscout is exactly that case, pro-gated with no registry entry naming its action types, and your test covers it.

Emitting the static requirement rather than the caller's plan is also the right call, and it keeps /api/mcp/schemas anonymous and cacheable. The precedent holds: app/api/workflows/public/route.ts already returns requiredPlan to unauthenticated callers.

Copying SYSTEM_ACTIONS instead of mutating it matters too, since the workflow validator and the builder UI share that constant.

One blocker, inline. Fix that and this merges.

import { eq } from "drizzle-orm";
import { db } from "@/lib/db";
import { chains, explorerConfigs } from "@/lib/db/schema";
import { resolveActionFeature } from "@/lib/features/action-egress";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This import breaks an existing test suite, and test-integration is red because of it.

Eight cases in tests/integration/action-schemas-route.test.ts now fail during setup with the same error:

Error: [vitest] No "findActionById" export is defined on the "@/plugins/registry" mock.
Did you forget to return it from "vi.mock"?

Pulling in resolveActionFeature brings findActionById into that module graph, and the suite's vi.mock("@/plugins/registry") does not stub it. Every test in the file dies before it runs, so the route they cover is currently unexercised rather than merely failing.

Extend that mock to return findActionById, or break the import chain so the builder does not reach the registry. Either is fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Triage: reviewed, changes needed from the contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plan gating is invisible until workflow create, and it gates the two general-purpose actions

2 participants