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
- Switch to paperclip-railway-redeploy on an existing eligible deployment (check canRedeploy first) instead of deploy-revision
- Use restart for a no-rebuild recovery, or rollback to a previous known-good deployment
- 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
- Enumerate tools only from the exported RAILWAY_TOOLS list, which already excludes blocked tools
- Never hand-write tool names from older docs or hosted-MCP catalogs
- Prefer redeploy on an existing deployment for fresh builds of approved code
- Refresh the tool catalog after upgrading Paperclip
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)