paperclipai/paperclip · error · RailwayError

railway_action_blocked

railway_action_blocked

Error message

This Railway action cannot bind its effects to an approved target. Use redeploy, restart, or rollback for an existing deployment.

What it means

`call()` refuses tools listed in RAILWAY_BLOCKED_TOOLS (e.g. deploy-revision, railway-agent, accept-deploy) before any schema lookup, throwing `railway_action_blocked` with HTTP 403. These Railway actions cannot be bound to a pre-approved repository+revision target, so Paperclip blocks them and directs callers to redeploy/restart/rollback on an existing deployment instead.

Solutions

  1. Switch to paperclip-railway-redeploy on an existing eligible deployment (check canRedeploy first) instead of deploy-revision
  2. Use restart for a no-rebuild recovery, or rollback to a previous known-good deployment
  3. Update the agent's tool catalog to the current RAILWAY_TOOLS list which no longer advertises blocked tools

Example fix

// before
await client.call("paperclip-railway-deploy-revision", args);
// after
const d = await client.call("paperclip-railway-deployment-status", { projectId: p, environmentId: e, serviceId: s, deploymentId: id });
if (d.canRedeploy) await client.call("paperclip-railway-redeploy", { projectId: p, environmentId: e, serviceId: s, deploymentId: id });
Defensive patterns

Strategy: validation

Validate before calling

import { isRailwayToolBlocked } from "../services/railway.js";
if (isRailwayToolBlocked(toolName)) throw new Error(`use redeploy/restart/rollback instead of ${toolName}`);

Try / catch

try {
  return await client.call(name, args);
} catch (e) {
  if (isRailwayError(e) && e.code === "railway_action_blocked") {
    return client.call("paperclip-railway-redeploy", { projectId: args.projectId, environmentId: args.environmentId, serviceId: args.serviceId, deploymentId: args.deploymentId ?? currentDeploymentId });
  }
  throw e;
}

Prevention

When it happens

Trigger: Invoking `paperclip-railway-deploy-revision`, `accept-deploy`, or `railway-agent` (any casing/separator form, since names are normalized before the blocklist check), or an old catalog entry that maps to a blocked tool.

Common situations: An agent picking deploy-revision from stale tool documentation; a client built against an older catalog that still exposes deploy-revision; attempting a fresh deploy through the direct bridge instead of using an existing deployment.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/c21003080eb100c2. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/railway.ts:252

      throw new RailwayError("railway_target_mismatch", "The service and environment do not belong to the selected Railway project.", 403);
    }
    return data.serviceInstance;
  }

  async function validateDeployment(args: Record<string, any>) {
    const data = await query(RAILWAY_QUERIES.deployment, { deploymentId: args.deploymentId });
    const d = record(data.deployment);
    if (d.id !== args.deploymentId || d.projectId !== args.projectId || d.environmentId !== args.environmentId || d.serviceId !== args.serviceId) throw new RailwayError("railway_target_mismatch", "The deployment does not belong to the selected Railway target.", 403);
    return d;
  }

  return {
    async probe(workspaceId: string) {
      if (!id.safeParse(workspaceId).success) throw new RailwayError("railway_workspace_required", "Choose an authorized Railway workspace before checking API access.", 400);
      await query(RAILWAY_QUERIES.projects, { workspaceId, first: 1 });
    },
    async call(name: string, parameters: unknown): Promise<unknown> {
      if (isRailwayToolBlocked(name)) throw new RailwayError("railway_action_blocked", "This Railway action cannot bind its effects to an approved target. Use redeploy, restart, or rollback for an existing deployment.", 403);
      const operation = name.slice(RAILWAY_TOOL_PREFIX.length) as Operation;
      if (!name.startsWith(RAILWAY_TOOL_PREFIX) || !Object.hasOwn(schema, operation)) throw new RailwayError("railway_unknown_tool", "Unknown Railway operation.", 400);
      const parsed = schema[operation].safeParse(parameters);
      if (!parsed.success) throw new RailwayError("railway_invalid_arguments", "Invalid Railway operation arguments. Use the exact IDs and limits in the action schema.", 400);
      const args = parsed.data as Record<string, any>;
      let result: unknown;
      if (operation === "list-projects") result = await query(RAILWAY_QUERIES.projects, args);
      else if (operation === "list-services" || operation === "list-environments") result = await query(operation === "list-services" ? RAILWAY_QUERIES.services : RAILWAY_QUERIES.environments, args);
      else {
        const instance = await validateTarget(args);
        const deployment = args.deploymentId ? await validateDeployment(args) : null;
        switch (operation) {
          case "service-status": result = instance; break;
          case "deployment-status": result = deployment; break;
          case "list-deployments": result = await query(RAILWAY_QUERIES.deployments, { input: { projectId: args.projectId, environmentId: args.environmentId, serviceId: args.serviceId }, first: args.first, after: args.after }); break;
          case "read-logs": {
            if (args.startDate && args.endDate && Date.parse(args.startDate) > Date.parse(args.endDate)) throw new RailwayError("railway_invalid_arguments", "Log start time must precede end time.", 400);
            const data = await query(args.kind === "build" ? RAILWAY_QUERIES.buildLogs : RAILWAY_QUERIES.runtimeLogs, { deploymentId: args.deploymentId, limit: args.limit, startDate: args.startDate, endDate: args.endDate, filter: args.filter });

View on GitHub (pinned to 3f1d897a7c)