Publisher FAQ

Straight answers to the questions contest publishers actually ask. Every entry here started as a real support thread — this page just makes the answers reachable without one.

Missing a question? Email sami@railcall.ai — good ones land on this page.
Hit a 400 on publish or seeing commands: 0? Every lint code from the pre-publish quality gate is documented at Publish rejections.

The one hard rule

Every filesystem write, network call, or state change your module promises the user MUST flow through the RailCall airlock: preview → human approval → execute → signed Ed25519 receipt. This is not negotiable and it is the entire product. A module that writes files or hits third-party APIs outside the airlock fails review, no exceptions.

My module calls vault_get("provider") and gets None even though I saved the credential in Studio Integrations.

Use __rc_helpers__["vault_get"]to resolve credentials. Configure the provider in Studio Integrations and check the module's declared provider and credential requirements. The helper is scoped to the module's allowed credentials; a missing value can mean either missing configuration or a request outside that scope. Do not read vault files directly.

My module loads in Studio but its MCP tools are missing or blocked

The MCP server discovers registered module commands. Tool names are normalized for client compatibility; use the names returned bytools/list rather than constructing names from a slug. Discovery mode can expose a subset of the registry.

Read-only execution requires the station's MCP read permission and, for externally capable commands, the operator's command-specific trust. A blocked call returns an actionable pointer. Follow its gate diagnostics in Studio Settings and Modules; do not assume a blocked tool is uninstalled. Writes still require the exact-payload human approval path in Studio.

What's the correct shape for input_schema in module.json?

Publishers today write a flat, name-keyed shape (matches Studio's internal arg_specs):

json
"input_schema": {
  "content": {
    "type": "string",
    "required": true,
    "help": "Message body (max 2000 chars)"
  },
  "channel": {
    "type": "string",
    "required": false,
    "placeholder": "#deploys"
  }
}

Station v0.30+ normalizes this into valid MCP JSON Schema automatically on every tools/list. Real JSON Schema (has type or properties at the top level) is also accepted and passed through. Either shape works.

Per-field keys the normalizer honors:

  • typestring, number, integer, boolean, object, array. text is normalized to string.
  • requiredtrue adds the field to required[] at the schema top level.
  • help and label — surface as the property description.
  • options — surface as JSON Schema enum.
  • default — passed straight through.

Rule of thumb: if the field name isn't self-explanatory to an LLM, add a help. If it's constrained to a set of values, add options. Both surface to MCP hosts driving your tool automatically.

Can my module launch a local UI (Electron / browser) that helps the user prep inputs?

Yes — with one hard line you cannot cross: the UI can never write files or hit external APIs directly. Every side effect goes through the airlock via a module command.

Two acceptable shapes:

Shape A (recommended): UI is a Studio companion

User launches your UI (Electron, Tauri, a local server your module spawns). UI helps them pick a file, runs analysis, shows detected issues + proposed repairs. When they hit "apply", UI POSTs the repair plan to Studio's /api/commands/preview endpoint. Studio renders the airlock card in Sends, the operator approves there, receipt mints. Your UI can poll for the receipt id and show a success screen.

Shape B: module handler spawns the UI on demand

Command shopify.repair_csv_interactive fires the UI as a subprocess from an authorized module invocation in Studio, where declared subprocess capabilities permit it. UI collects parameters, hands them back (stdin / localhost socket / temp file), handler stages the airlock write. Cleaner for CLI users; more moving parts.

Default to Shape Aunless there's a specific reason for Shape B.

Five things to avoid:
  1. Do not have the UI write files or call APIs directly.
  2. Provide a reproducible station command path with test inputs and expected receipts. Document any UI prerequisites.
  3. module.sig covers the module dir only. A separately- installed UI is trusted on its own reputation — say so in the description.
  4. No silent phone-home from the UI. Every side effect is an airlocked command.
  5. input_schema must reflect what the CLI accepts, not a UI-trimmed shape.

What does module.sig actually cover?

The Ed25519 signature covers the exact bytes of module.json plus handlers/handler.py in the module directory. It does NOT cover:

  • External files your handler reads from the user's disk.
  • Subprocess binaries your handler spawns.
  • Separately-installed helper apps or UIs.
  • Runtime state (vault contents, receipts). Those are signed independently by Studio's install-time keypair.

The trust chain answers "did the publisher who claims to have shipped this bundle actually sign it?" — a strong guarantee, but scoped to the bundle itself.

How do I test my module before publishing?

Put your module under ~/.railcall/station/modules/<your-slug>/ with the required files (module.json, handlers/handler.py, module.sig). Studio's loader picks it up on startup + on every /api/modules/reload.

The five-step loop we recommend:

  1. Sign your bundle: railcall market module sign path/to/module_dir.
  2. Drop it under ~/.railcall/station/modules/. Open the Studio Modules tab — you should see it load green with all commands registered.
  3. Run the command from Studio's palette with test inputs. Inspect the preview, approve any consequential action through Sends, and open the resulting receipt in Studio's Receipts view.
  4. Fire the same command via MCP Inspector against railcall mcp. Check its declared schema and policy behavior: a blocked command returns a pointer; an authorized read may execute.
  5. railcall market publish path/to/module_dir. Check the returned listing status; review time is not guaranteed.

Why did my module get rejected on install?

Studio's Modules tab shows the exact rejection reason on every rejected card. The five main classes:

  • Signature failuremodule.sigdoesn't verify against the publisher pubkey in module.json. Re-sign with the correct key.
  • Trust: publisher not in allowlist— the operator has strict trust mode on and your publisher pubkey isn't on their list. They run railcall trust add <pubkey> once and your module registers.
  • License: not activated / expired / bound to wrong install — for paid modules. The Modules tab surfaces "Buy license" + "Activate license" CTAs.
  • Command <cid>: no callable _h_<name> in handler.py module.json declares a command whose handler function is missing or misnamed.
  • manifest parse errormodule.json isn't valid JSON.

When do I need to bump the module version?

On every publish. The marketplace refuses a publish that reuses an already-published version number for your slug. Bump the version field in module.json (semver: major.minor.patch). SemVer guidance:

  • Patch: bug fix, docs, no behavior change for existing callers.
  • Minor: new command added, or a new optional field on an existing command.
  • Major: a breaking change — an existing command removed, a required field added, a receipt shape changed.

How do reviewers test my module?

Reviewers install your module on a fresh Station install and:

  1. Verify the signature loads clean (green ✓ in Modules tab).
  2. For each declared command, use Studio's command palette with the required inputs from your input_schema.
  3. Approve in Studio Sends → verify a signed receipt lands.
  4. Inspect the receipt and logs for secrets or private content before sharing them.
  5. Run your provided smoke test script if one is documented.

Use the same station approval and receipt path that customers use. A custom UI must not bypass it.

How does licensing work for paid modules?

Set license_required: true in module.json. Choose a price + tier when publishing (one-time or subscription). Buyers hit Stripe Checkout on the marketplace listing; on completion, our issuer signs an Ed25519 license bound to their install pubkey and drops it under ~/.railcall/station/.railcall_workspace/module_licenses/ via the railcall license activate <license.json> flow. Check activation in Studio before running a paid command.

Your handler doesn't verify anything — the loader gates access before your handler runs. If the license is missing/expired/foreign, the module lands in the rejected list with a "Buy license" CTA and your _h_* functions never register. Free modules skip this entirely.

Subscriptions auto-renew via Stripe's invoice.paid webhook — a fresh license file overwrites the old one before it expires. A grace period covers short-term Stripe outages so a valid subscriber never has their module go dark from a webhook delay.

Does my module handler run in a sandbox?

Opt-in as of Station v0.33. Declare capabilities in your module.json:

json
{
  "requires": {
    "network": ["api.linear.app", "*.stripe.com"],
    "subprocess": false,
    "filesystem_writes": ["/tmp/**"]
  }
}

The loader monkey-patches your handler namespace before exec so any attempt outside the declared capabilities raises SandboxViolation:

  • network — fnmatch-style host allowlist. Empty list = no egress at all. Wraps urllib.request.urlopen, http.client.HTTPConnection, and socket.socket.connect.
  • subprocess — boolean. False replaces subprocess.Popen/run/call/check_output + os.system/popen/exec*/spawn* with a raise-immediately shim.
  • filesystem_writes — glob allowlist for absolute paths. Wraps open(mode=w|a|x|+) + os.remove/unlink/rename/replace/rmdir/mkdir/makedirs. Reads are unrestricted.

Modules WITHOUT a requires block behave exactly as before — no forced migration, no breaking change. Modules WITH one get gated per declaration.

Not container-strong. A determined attacker with Python-import-level access can bypass anything at the language layer:import ctypes to reach libc, low-level _socketimports, raw file descriptors. The publisher-trust allowlist is still the primary defense; this layer is "declared capabilities + fail-loud on violation." It can catch mistakes such as "oops the AI-drafted module tried to shell out" and "this module quietly started talking to a domain it didn't announce." It is not sufficient isolation against a determined adversary.

Studio's Modules tab surfaces the declared capabilities per module. A module with no requiresblock gets a visible "unrestricted" banner so operators aren't misled into thinking a legacy module is sandboxed when it isn't.

What happens if two modules declare the same command_id?

The loader refuses to register a command already owned by another loaded module. It reports the collision instead of allowing the later module to take over. Remove a duplicate development copy or rename your command IDs, then reload and inspect the registration result. Built-in command overrides are a separate supported behavior; do not rely on module load order to replace another publisher's command.

Namespace your command ids under your slug (myco.action rather than bare action) to avoid accidental collisions with other publishers' modules.

Will my module keep working when Station upgrades?

The __rc_helpers__ surface Studio injects into handlers is API-stable — vault_get, jload, jsave, safe_namewon't break. Handlers that use only these keep working across Station upgrades.

Handlers that reach into Studio's internals (importing from workbench.*, reading undocumented state files) may break — those aren't public surface. If you need something that isn't in __rc_helpers__, open an issue and we'll add it.

Where do I report bugs or request features?

Email sami@railcall.ai with:

  • Your Station version (railcall version).
  • The module slug + version you're testing.
  • The specific command / MCP call that failed.
  • The Studio log excerpt (stderr) around the failure — never full receipts (may contain PII).

Include a minimal reproducible example and the registration or execution error. Support and review turnaround depend on the issue and current capacity.