RailCall Teams

Connect stations into a team: delegated approvals co-signed on your teammate's machine, credentials shared without the secret ever traveling, workflows offloaded to an always-on worker — all over outbound-only HTTPS, all offline-verifiable.

The trust model in four sentences

A team is a signed document, not a database row: a manifest of member station pubkeys + roles, signed by an offline root seed that only the owner holds. The RailCall Relay distributes messages between stations but can never forge one — every envelope is Ed25519-signed end-to-end and every station verifies against its own copy of the manifest. Payloads that carry real data travel as sealed boxes (ChaCha20-Poly1305) only the addressee can open. No station ever accepts an inbound connection — everything is outbound polling, so joining a team changes nothing about your firewall.

Create a team (2 min)

shell
railcall team create "Acme Ops" "Your Name"
# → team tm_9f2c…  · manifest v1
# → ROOT SEED printed ONCE — write it down OFFLINE and never store it digitally
The root seed is the team

It signs every membership change and every policy bundle. It is shown once, stored nowhere, and never touches a browser. Anyone holding it controls the team; losing it freezes the member list permanently (the team keeps operating on its last manifest). Treat it like the DRM issuer seed: paper, safe, offline.

Teammates join in two steps — they adopt, you add:

shell
# teammate's machine
railcall team join tm_9f2c…      # adopts the roster; "NOT a member yet"
railcall team show               # prints their station pubkey

# your machine (asks for the root seed)
railcall team add <their-pubkey> "Dave" approver,operator

# teammate again
railcall team sync               # → "you are a member — approver, operator"

Roles: owner (holds the seed ceremony), approver (may co-sign), operator (may request + use), worker (may run offloaded jobs). Removal is railcall team remove — a signed new manifest version; the removed station is refused everywhere on the next sync, and its grants die with it.

Delegated & quorum approvals

Policy can demand a teammate's signature before an action executes — per provider, per action class, with a quorum (k-of-n). The staged action waits; the request appears in your approvers' Studio Team tab; each approval is a signature made on that approver's machine over the exact action bytes.

json
// WS/team/approval_policy.json (or pushed team-wide via a policy bundle)
{
  "providers":      { "stripe": { "role": "approver", "quorum": 1 } },
  "action_classes": { "payment": { "role": "approver", "quorum": 2 } },
  "default":        null
}

The receipt embeds the co-signatures plus the team manifest they were verified under, so railcall verify receipt.json proves the whole chain offline, forever: ✓ TEAM APPROVED — Dave (1-of-1 quorum). Guarantees baked in: an approval binds to one request for one set of action bytes (no transplants, no re-use), self-approval never counts toward quorum, any explicit deny fails the request, and a timeout is a recorded expiry — never a silent hang. If the requirement can't be satisfied (no team, quorum unreachable, relay down), the action is blocked, not bypassed.

Shared credentials — the secret never travels

1Password shares your secrets; RailCall shares your capabilities. The company Stripe key stays in one vault. Teammates receive a signed grant — a verb allowlist plus an optional daily spend cap — and their uses travel encrypted to the holder station, which authorizes from its own copy of the grant and executes through its own airlock. Results come back sealed, and receipts on both sides carry on_behalf_of.

shell
# holder station (dual-control APPROVE code applies when enabled)
POST /api/team/share/create
{ "credential_name": "stripe", "member_pubkey": "<dave>",
  "verbs_allow": ["stripe.charge.*"], "max_spend_cents_per_day": 20000 }

Why this beats copying keys around: revocation is real. Revoke the grant (one click in the Team tab) or remove the member from the manifest — access dies instantly, with no provider key rotation, because the member never had the key. A member editing their local copy of a grant changes nothing — the holder's copy authorizes. And if the holder's own policy demands a human for a write, the teammate's use lands in the holder's Sends queue and the teammate is told — governance never relaxes because a request came from a friend.

Compute offload & cloud stations

Offer a workflow run to a station holding the workerrole — typically your team's always-on box — and it executes through the worker's own governed DAG engine: live-execution gates, plan-pin, incremental watermarks, signed run receipts. Your laptop can sleep.

shell
POST /api/team/jobs/offer
{ "to": "<worker-pubkey>", "workflow_id": "backlog_to_discord",
  "context": {"channel": "ops"}, "max_spend_cents": 500, "dry_run": false }
# sealed result lands back async → GET /api/team/jobs

v1 scope, stated plainly: jobs reference a workflow the worker already has installed(the worker only runs code it installed — same stance as credentials); live jobs target exactly one named worker (no fan-out double-execution); and a job whose spend cap is below the installed workflow's ceiling is refused — never run above what the requester authorized. To provision a VPS as your team node: bash ~/.railcall/station/scripts/provision-team-station.sh — headless systemd boot, mesh polling on, self-verifying.

Policy bundles

The owner publishes a policy bundle signed by the root seed; every station adopts it on sync and applies it to its approval gate; every receipt stamps team_policy_bundle: 3— so "which policy governed this action" has a signed answer. Bundles can only tighten: the schema carries approval requirements and nothing that could switch a member's local protections off.

shell
POST /api/team/policy/publish   # asks for the root seed
{ "requires_team_approval": { "providers": { "stripe": {"role":"approver","quorum":1} } },
  "note": "all stripe writes co-signed, effective policy v3" }

The Team tab

Studio's Team tab is the daily surface: the roster (with a Sync button), incoming approvals awaiting your signature (Approve/Deny with reason; the dual-control terminal code applies to approvals when enabled), your requests with live quorum progress, shared credentials both directions with one-click revoke, and an activity feed of every team fact this station knows — each row traceable to a signed artifact.

Security posture, condensed

End-to-end Ed25519 on every team fact; sealed boxes on every payload; the Relay can delay but never forge; replay dies at monotonic manifest/bundle versions + consume-once envelope caches; the MCP channel (Claude Desktop) is refused on all team endpoints — a model-facing proxy can never see a root seed or drive the ceremony. Full design: threat model.