Skip to main content

Run mecated standalone

mecated runs Mecatl as a standalone service over gRPC and HTTP/SSE. It provides the operator controls needed for authentication, TLS/mTLS, rate limiting, observability, persistence, and graceful shutdown.

Configure providers

Configure providers from local embedded mecatui. These are operator settings for the embedded server or mecated, not for a connected client.

mecatui providers setup
mecatui providers
CommandPurpose
mecatui providers setupRun the guided provider setup.
mecatui providers add NAMEDefine a custom HTTPS provider, API flavor, default model, and authentication.
mecatui providers login NAMEEnter or replace the provider's credential.
mecatui providers logout NAMERemove its local OIDC enrollment.
mecatui providers set-default NAME [MODEL]Select the deployment default. A supplied concrete model ID is saved without a live inventory check; availability is verified when the provider is used.
mecatui providers remove NAMERemove a custom provider.
mecatui providers or mecatui providers statusShow configuration state without credentials.

Custom definitions live under providers in operator settings.yaml. OIDC providers share credential_store.oidc:

providers:
corp:
base_url: https://gateway.example/v1
default_model: corp-model
api_flavor: openai-responses
auth:
method: oidc
oidc:
issuer: https://issuer.example
client_id: mecatl
scopes: [openid, offline_access]
issuer_trust: { policy: public }
gateway_trust: { policy: public }
credential_store:
oidc:
home: /home/operator/.local/state/mecatl/provider-oidc
key:
source: keyring

auth.method accepts api_key, oidc, or none. OIDC requires the openai-responses flavor and credential_store.oidc. To supply the encryption key through the environment, set key.source: environment and key_env. See the configuration reference and credential-store reference.

Supply API keys through the environment or a provider-credentials YAML file. Pass --api-key-file PATH to mecatui or mecated, or set credential_store.api_key.file in operator settings:

providers:
corp:
api_key: <API_KEY>

Do not put secrets in command-line arguments, settings YAML, prompts, or logs.

mecated never opens a browser. Log in through local embedded mecatui before starting or restarting it. mecatui login ADDRESS instead enrolls the client with a remote server and does not configure that server's providers.

ToolHive owns its LLM credentials. Use thv llm for setup and credential management; Mecatl only uses its configured or discovered gateway.

If you are deploying to Kubernetes without persistent volumes, see mecak8s instead. That binary is purpose-built for no-PVC pod deployments, with Redis-backed state when you configure --redis-url and Kubernetes Lease coordination enabled by default.


Quick start

Install with brew install stacklok/tap/mecatl. Then start the server:

mecated serve

A command word is required: mecated serve for the network daemon, mecated acp for the ACP stdio mode. Bare mecated prints the command help and exits with a usage error.

The minimal invocation starts a loopback-only server with in-memory sessions and no authentication.

Authorize a global MCP OAuth profile before serving with mecated mcp login SERVER [--no-browser]. The daemon restores and refreshes the encrypted credential but never opens a browser. See MCP client.

Default addresses:

ListenerDefault
gRPC127.0.0.1:8080
HTTP/SSE127.0.0.1:8081
Prometheus + admin127.0.0.1:9090

A slightly more configured invocation for unattended local operation:

export MECATL_AUTH_TOKEN="$(cat ~/.mecatl/token)"
mecated serve \
--store-dir ~/.local/share/mecatl/sessions \
--posture auto

--log-level accepts debug, info (default), warn, or error. Startup logs the binary version with msg="mecated starting".

--store-dir enables local JSONL persistence. The path and its ancestors must be physical directories, not symlinks. On macOS, use /private/... instead of a path through the /var symlink. --auth-token requires the token on every request and can also read MECATL_AUTH_TOKEN. --posture auto allows unattended runs while retaining child prompt-injection protections.

Before binding a non-loopback address, add TLS and caller authentication. See The trust model.

Server-owned session placement

--workspace sets the server's default workspace. Clients can request the default or the no-fs profile, but cannot submit a path. ListWorktrees returns short-lived selectors for ClearSession and ForkSession; selectors expire when the server restarts. Mecatl stores the exact placement privately and reattaches it before each run. See Execution environments for the shared placement and reattachment model. For the underlying design, see ADR 0291.

Operator-defined providers

Custom provider IDs persist with sessions. Removing or renaming a provider makes those sessions fail instead of selecting another provider. Configure built-in endpoints under provider_overrides; matching --*-base-url flags take precedence. See the configuration reference.

If a self-hosted endpoint lacks required Responses API features, use the openai-chat-completions flavor.

The built-in openai and openrouter providers send prompt-cache hints only through their canonical base URLs. A base URL override disables these hints so a compatible endpoint cannot reject an unsupported cache field.


Flag reference

Flags are grouped by area. All have zero-value defaults that produce a working loopback-only server. Flags not covered here are advanced operator tuning; run mecated serve --help for common flags grouped by task, or mecated serve --help-all for the exhaustive reference.

Server

FlagDefaultNotes
--grpc-addr127.0.0.1:8080gRPC listen address
--http-addr127.0.0.1:8081HTTP/SSE listen address. An empty value disables this and the admin listener; see Hosting a spawned daemon
--metrics-addr127.0.0.1:9090Prometheus + admin listener; empty disables it
--grpc-unix-socket"" (off)Serve gRPC on a UNIX-domain socket instead of a TCP port. Mutually exclusive with a configured --grpc-addr. See Hosting a spawned daemon
--ready-file"" (off)Absolute path to write a JSON readiness document to, atomically, once every listener is up
--lifetime-pipe-fd0 (off)File descriptor of an inherited pipe read end or connected UNIX-domain stream socketpair endpoint; EOF on it stops the daemon gracefully (the parent-crash path)
--lifetime-stdinfalseUse piped stdin for parent liveness; EOF stops the daemon gracefully. Mutually exclusive with --lifetime-pipe-fd
--auth-token"" (off)Bearer token required on every request; also MECATL_AUTH_TOKEN
--tls-cert""PEM server certificate; enables TLS on both listeners when paired with --tls-key
--tls-key""PEM server private key
--client-ca""PEM client-CA bundle; enables mTLS (requires --tls-cert/--tls-key)
--cors-origins"" (off)Allow an exact browser origin to call the HTTP API; repeatable. See Browsers and CORS
--deployment-id""Optional opaque label for this deployment, echoed on GetCompatibilityInfo. Never inferred from the host
--rate-limit0 (off)Sustained per-client request rate in req/s; with OIDC, also limits rejected bearer validation per direct transport peer IP
--rate-burst0 (derived)Token-bucket burst; zero derives a sane default from --rate-limit, including the OIDC rejected-token bucket
--oidc-issuer"" (off)OIDC issuer URL whose tokens identify callers; setting it turns caller identity on. See Caller identity
--oidc-jwks-uri"" (derived)Static JWKS endpoint, short-circuiting OIDC discovery (air-gapped or pinned-key deployments)
--oidc-audience""Audience (aud) this deployment accepts; required with --oidc-issuer
--oidc-max-jwks-staleness1hMaximum age of last-good signing keys during an IdP outage. 0 deliberately disables this bound; negative values are rejected.

Caller identity (OIDC)

--oidc-issuer names the identity provider whose tokens identify your callers. Every request must then present a bearer token, and --oidc-audience is required. Mecatl records the verified (issuer, subject) pair as the owner of sessions, schedules, teams, and memory records. This ownership does not isolate a shared workspace; separate working files at the deployment layer.

Startup fails when the initial signing-key fetch fails. Afterward, cached keys remain valid for --oidc-max-jwks-staleness (default 1h). A failed refresh after that bound returns 503; a rejected token returns 401. Setting the bound to 0 accepts cached keys regardless of age.

With --rate-limit, rejected tokens use a separate bucket keyed by the direct peer IP and return 429 when exhausted. Mecatl does not trust forwarded-IP headers for this check. Authentication logs omit credentials, tokens, claims, and caller identifiers. See Caller identity and OIDC for the full behavior.

Daemon config file (daemon.yaml)

Put listener addresses, TLS files, and rate limits in daemon.yaml when you do not want to repeat flags. This is separate from the policy and provider settings in settings.yaml. Mecatl loads it only when you pass --config:

mecated config daemon init # write the conventional skeleton
mecated config daemon init --print # print it to stdout, no file
mecated config daemon validate # validate the conventional path
mecated config daemon validate --file /etc/mecatl/daemon.yaml
mecated serve --config ~/.config/mecatl/daemon.yaml # start with it

The strict v1 schema rejects unknown keys. Explicit flags override file values, including empty values and zero. Keep the bearer token in MECATL_AUTH_TOKEN or --auth-token; daemon.yaml does not accept it. Validation prints neither secrets nor file contents.

Session state

FlagDefaultNotes
--store-dir"" (in-memory)Directory for the JSONL session store. Empty = in-memory, no persistence across restart
--session-lease-dir""Single-host flock lease backend; see multi-replica
--session-lease-k8s-namespace""k8s coordination.k8s.io Lease backend; see multi-replica
--session-lease-ttl30sLease lifetime; a crashed holder's lease becomes claimable after this long
--session-store-url""gRPC driver endpoint replacing the local JSONL store (mutually exclusive with --store-dir)

Scheduled tasks

FlagDefaultNotes
--no-schedulerfalseDisable firing due schedules. The management API remains available
--scheduler-tick-interval30sHow often the tick loop polls for due schedules
--scheduler-min-interval1mMinimum schedule frequency; 0 disables the floor
--scheduler-max-concurrent-fires4Max schedules fired in parallel per tick
--schedule-store-url""gRPC driver for the schedule registry, independent of the session store. Not supported with OIDC ownership
--learning-store-url""Trusted single-tenant gRPC driver for distributed learning. Not supported with OIDC ownership

See Scheduled tasks for the in-chat Schedule tool and the gRPC/REST management APIs.

LLM resilience

FlagDefaultNotes
--llm-per-attempt-timeout300sBounds establishing the stream only (connect + first chunk). Does not cut an actively-streaming turn
--llm-stream-idle-timeout180sMax idle gap between chunks after the first arrives. A stall exceeding this is terminal and not retried
--llm-max-attempts3Max stream-establish attempts (initial call + retries)
--llm-breaker-threshold5Consecutive LLM failures that open the circuit breaker; 0 disables
--llm-breaker-cooldown30sHow long the breaker stays open before half-opening

The per-attempt timeout stops after the first chunk. The stream-idle timeout then bounds gaps between chunks without limiting an active turn.

Provider and model

FlagDefaultNotes
--model""Model id sent to the provider; empty uses the provider-appropriate default
--default-provider""Deployment-wide default provider (openai, openrouter, anthropic, opencode); validated fail-fast
--default-model""Deployment-wide default model id for the default provider; validated fail-fast
--subagent-model""Global default model for child engines (Subagent, Parallel branches, team members) that do not pin their own
--no-prompt-cachefalseDisable provider-side prompt caching; see ADR 0100
--anthropic-cache-ttl"" (API default, 5m)TTL on every Anthropic ephemeral cache breakpoint: 5m or 1h
--mockfalseOffline canned provider with one text turn; smoke tests only
--mock-script""Path to a strict JSON mock script; implies the offline provider and replaces its canned turn with ordered text/tool-call turns

Provider credentials are read from OPENAI_API_KEY, OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or OPENCODE_API_KEY, never flag values. You can instead use an owner-only auth.yaml file. See Configure provider credentials.

The experimental openai-codex provider uses a manually supplied ChatGPT Codex token and has no login or refresh flow. See the same credential guide for its schema and lifecycle.

Offline mock providers (no credentials)

--mock provides one canned text response for offline smoke tests. --mock-script PATH instead loads ordered text and tool-call turns from a strict JSON file. Both start without provider credentials and fail before binding a listener when the script is invalid.

{
"turns": [
{
"tool_calls": [
{
"id": "write-1",
"name": "Write",
"args": { "path": "proof.txt", "content": "ok\n" }
}
]
},
{ "text": "continued after the tool" }
]
}

A ToolHive-managed LLM gateway (no API key needed)

If you have configured access to an LLM gateway through ToolHive, --toolhive-llm (on by default) detects that configuration and registers two provider IDs: toolhive uses OpenAI Responses, while toolhive-anthropic uses native Anthropic Messages. ToolHive manages the gateway credentials. Use /models to select either provider; the mecatui welcome screen reports when one is available but not selected. Disable detection with --toolhive-llm=false on shared hosts.

--toolhive-llm-mode selects how both providers reach the gateway:

  • auto uses direct mode when ToolHive has an HTTPS gateway URL, issuer, and client ID; otherwise it uses the local thv llm proxy.
  • direct requires that complete configuration and refreshes the OIDC token in process.
  • proxy requires a running local proxy and supports self-signed gateways.

Run thv llm setup before using direct mode; mecated does not open a browser when credentials are missing. Direct mode does not honor tls_skip_verify.

FlagDefaultPurpose
--toolhive-llmtrueDetect ToolHive's local LLM configuration. Set it to false on a shared host where this process must not use another operator's configuration.
--toolhive-llm-base-url""Use an explicit loopback proxy URL. This always selects proxy mode.
--toolhive-llm-modeautoUse direct when the HTTPS gateway URL and OIDC settings are complete; otherwise use the loopback proxy. Set proxy or direct to require one path.

For provider selection, protocol paths, independent catalog status, and model-routing troubleshooting, see Choose models and providers.

Posture

FlagNotes
--posture strictDefault. Every mutating call asks for approval
--posture trustedHonor a project's ALLOW rules (alias: --trust-project)
--posture autoAllow unattended calls while keeping child injection defense on
--posture yoloAlso disable child injection defense. Isolated single-tenant only. Refused as root without MECATL_SANDBOX=1

On a headless root (--headless), posture never raises TrustProject. Explicit --trust-project, trustedWorkspaces:, or undrifted remembered trust admits BOTH repo steering and the read-only child shell. Without a trust source, --posture auto keeps its approvals but gets neither because .git is not vouched. See Permissions and posture for the trust sources and headless behavior.

See Permissions & guardrails for the full rule engine. Posture is read from the operator-global settings.yaml (posture: key) and out-ranked by the CLI flag when both are set.

Guardrails

FlagDefaultNotes
--guardrails-model"" (off)Model id or alias for the content checker. Configuring a model enables guardrails
--guardrails""Kill-switch only: pass --guardrails=off to force off regardless of model config

The rule list and cost knobs live in the operator-global settings.yaml (guardrails: subtree). A project-tier guardrails: block is ignored with a WARN because a checked-in file cannot weaken an operator security check.

MCP

FlagDefaultNotes
--mcp-server name=URL(none)Remote MCP server, repeatable. Per-server bearer token from MCP_<NAME>_TOKEN; a token-bearing URL must be https (or http to loopback)
--mcp-server-insecure-http name(none)Allow the named server's bearer over off-host HTTP. Use network controls and short-lived tokens. Unknown, HTTPS, and loopback names fail startup
--mcp-resource-toolstrueRegister ListMcpResources/ReadMcpResource meta-tools when a server exposes resources
--toolhivetrueDiscover MCP servers from running ToolHive workloads (fails soft when no container runtime is reachable)
--toolhive-group"" (default group)ToolHive group to discover from

--mcp-server uses streaming HTTP. ToolHive can proxy stdio backends. For OAuth profiles, resources, debugging, and transport constraints, see MCP client.

Skills

FlagDefaultNotes
--skills-dir(none)Trusted local Agent Skills directory; repeatable
--skills-conventionalfalseAdd conventional project/user skill locations
--skill-source-url(none)Remote SkillSourceService; replaces local discovery and snapshots metadata at startup

Mecatl loads skill instructions and assets on demand. Assets do not become workspace files or executable scripts. See Skills, commands, and soul.

Observability

FlagDefaultNotes
--otlp-endpoint"" (off)OTLP trace collector, e.g. localhost:4317; empty disables tracing
--otlp-protocolgrpcOTLP transport: grpc or http
--flight-recordertrueArm the execution-trace ring buffer; snapshots at /debug/flightrecorder
--perf-mcpfalseMount a read-only perf MCP server at /mcp. Requires loopback --metrics-addr
--mutex-profile-fraction0 (off)runtime.SetMutexProfileFraction; adds overhead when > 0
--block-profile-rate0 (off)runtime.SetBlockProfileRate in ns; adds overhead when > 0

The loopback admin listener carries /metrics, /debug/pprof, /debug/vars, and /debug/flightrecorder. Diagnostic output can contain prompts, file paths, and goroutine stacks. Keep this listener on loopback.


The trust model

Mecatl exposes command and file execution. Both API listeners therefore bind to loopback by default. Before binding to another interface:

  • authenticate callers with --auth-token, OIDC, mTLS, or an operator-managed private edge;
  • encrypt traffic with --tls-cert and --tls-key, unless the edge terminates TLS; and
  • keep the unauthenticated admin listener on loopback.

Add --client-ca to require client certificates. OIDC records distinct caller identities; a shared bearer token does not. Authentication does not isolate a shared workspace. A non-loopback API with no authentication is allowed for service-mesh deployments but produces a startup warning.


Persistence

By default, sessions live in memory and disappear when the server restarts.

Enable JSONL persistence by pointing --store-dir at a directory:

mecated serve --store-dir /var/lib/mecatl/sessions

The store writes snapshots, tool-call audit, and events beneath sid-v1. In-flight sessions recover from their snapshot. Older histories migrate on the next write. Files are plaintext and owner-only; do not edit or share them. See Session store for the layout and durability guarantees.

Kubernetes and persistent volumes

If you run mecated in Kubernetes with --store-dir, you need a PersistentVolume backed by ReadWriteOnce (or ReadWriteMany for multi-replica with affinity routing). If a PVC is a hard constraint, use mecak8s instead. When configured, its Redis-backed store has no PVC requirement.

Configure retention in the operator settings.yaml. Main-session deletion is off by default and requires acknowledge_main_deletion: true when enabled. Follow Operate local session storage for the schema, service definitions, backups, migration, and restore.

--session-store-url replaces the local store with a gRPC driver and cannot be combined with --store-dir. Current remote drivers do not support OIDC caller ownership; use local JSONL or mecak8s for multi-user deployments.

--learning-store-url selects a trusted single-tenant driver for distributed learning. Startup rejects partial driver support and deployments with OIDC caller ownership.

Import from Codex or Claude Code

mecated import creates a resumable Mecatl session from a local Codex or Claude Code transcript. It can also copy project files and skills into a workspace.

Codex stores active session rollouts under ~/.codex/sessions/; Claude Code stores project transcripts under ~/.claude/projects/. Select the JSONL session you want and use the same --store-dir when starting the server:

mecated import \
--from codex \
--session ~/.codex/sessions/2026/08/05/rollout-....jsonl \
--store-dir ~/.local/share/mecatl/sessions \
--workspace ~/work/imported-project \
--copy-files \
--skills

mecated serve \
--store-dir ~/.local/share/mecatl/sessions \
--workspace ~/work/imported-project \
--skills-dir ~/work/imported-project/.mecatl/skills

For Claude Code, use --from claude-code with a transcript under ~/.claude/projects/. Imported history keeps user and assistant text but omits provider-private reasoning, instructions, and tool activity. The new session is idle and uses the provider and model selected when you resume it.

Only use --skills or --skills-dir with trusted sources because imported skills steer the model. --copy-files requires --workspace and skips .git, symlinks, and special files. Mecatl never overwrites existing sessions, files, or skill names. Imported data remains local in the plaintext JSONL store.

Use --source-workspace when the transcript's recorded working directory has moved. Use --id to choose a different session ID after a collision.


Multi-replica

By default, route every request for a session to the same replica.

For failover or routing without session affinity, configure a lease backend. Only one replica can hold a session lease; competing requests return gRPC FAILED_PRECONDITION or HTTP 409.

Three lease backends are available:

BackendFlagWhen to use
flock (single-host)--session-lease-dir <dir>Multiple mecated processes on one machine. flock auto-releases on crash
k8s Lease--session-lease-k8s-namespace <ns>Multi-replica in Kubernetes; uses coordination.k8s.io Leases
gRPC driver--session-lease-url <host:port>Custom or managed lease backend via the driver protocol

The ServiceAccount for the k8s backend needs get,create,update,delete on leases.coordination.k8s.io in the configured namespace. It does not need list or watch.

Remote shared stores still need an explicit lease

A local --store-dir automatically uses a flock lease beneath the store root, so multiple current mecated processes on one host participate without another flag. Remote stores and multi-host filesystems still require an explicit Kubernetes or gRPC lease backend (and local flock is not reliable over NFS/EFS). Without one, use session affinity; destructive maintenance fails closed.

The lease TTL defaults to 30 seconds. After a crash, another replica can claim the session when that TTL expires.


Operator subcommands

Two subcommands run a service:

mecated serve [flags]
mecated acp [flags]

The remaining commands perform offline operator tasks:

TaskCommand
Create operator settingsmecated config init
Validate operator settingsmecated config validate [--file PATH]
Create daemon settingsmecated config daemon init
Validate daemon settingsmecated config daemon validate [--file PATH]
Promote a legacy draft skillmecated skills promote [flags] NAME
Print perf MCP configurationmecated perf-mcp print-config

Add --print to either config init command to write the skeleton to standard output. Both validation commands are read-only and omit settings values. Use config validate --learning-patch PATH to test one learning: mapping without changing the base file.

skills promote is a deprecated compatibility path for model-authored files in --skills-draft-dir; it does not activate skill-lifecycle repository records. Manage schedules through the Schedule tool or the gRPC/REST API. See Scheduled tasks.


Hosting a spawned daemon

When an SDK, editor, or wrapper starts mecated, use a private socket, readiness file, and lifetime pipe:

mecated serve \
--grpc-unix-socket /run/user/1000/myapp/mecated.sock \
--http-addr "" \
--ready-file /run/user/1000/myapp/ready.json \
--lifetime-pipe-fd 3

--grpc-unix-socket serves gRPC without opening a TCP port. Dial the example as unix:///run/user/1000/myapp/mecated.sock. Mecatl creates the socket owner-only, rejects unsafe parent-directory permissions, removes stale sockets, and refuses to replace a live listener. Configuring both the socket and --grpc-addr is an error. An empty --http-addr disables both HTTP/SSE and the admin listener, so it cannot be combined with --perf-mcp. Keep the full socket path within 103 bytes on macOS or 107 bytes on Linux.

--ready-file is published atomically after every listener is bound. Its presence means the parent can dial the daemon:

{
"schema": "mecated-ready/1",
"pid": 5821,
"transport": "unix",
"grpc_address": "/run/user/1000/myapp/mecated.sock",
"socket_path": "/run/user/1000/myapp/mecated.sock",
"api_major": 1,
"features": ["mcp_servers_on_create", "server_info"],
"deployment": "eu-west-1 staging"
}

Use api_major and features to check compatibility before the first RPC. The owner-only file contains no credentials and remains after shutdown, so check pid before trusting an existing record.

--lifetime-pipe-fd accepts a pipe read end or a connected UNIX stream socket. Keep the other endpoint open. EOF triggers graceful shutdown and persistence. The socket, readiness-file, and lifetime-pipe flags are off by default.

--lifetime-stdin uses piped stdin for the same parent-liveness signal. A parent using Deno.Command sets stdin: "piped" and holds the writer open. Closing the writer or exiting closes the channel and triggers graceful shutdown. The daemon discards any bytes received. Regular files and terminals are rejected, and the flag cannot be combined with --lifetime-pipe-fd. The Deno SDK integration manages this channel for you.

Add MCP servers per session

Clients can add per-session MCP servers only when mecated uses a UNIX socket and HTTP is disabled:

Listener topologymcp_servers
--grpc-unix-socket and --http-addr ""accepted
Anything else, including loopback TCPrefused with UNIMPLEMENTED / HTTP 501 and client_mcp_unsupported

Clients can check for mcp_servers_on_create through GetCompatibilityInfo or GET /v1/compatibility. Per-session entries support streaming HTTP without redirects. Server names must use 1-64 characters from [A-Za-z0-9._-], cannot contain __, and must be unique. Put credentials in headers; Mecatl redacts them and rejects URLs containing user information.

If any requested server is unreachable, session creation fails atomically with client_mcp_unreachable and no session is created.


Graceful shutdown

On SIGINT or SIGTERM, mecated drains listeners for 10 seconds, flushes telemetry for up to five seconds, and persists configured session state. Interrupted runs recover from their snapshots after restart.

EOF on an inherited --lifetime-pipe-fd or piped --lifetime-stdin follows the same path. See Hosting a spawned daemon.

Browsers and CORS

For local browser development, allow each origin explicitly:

mecated serve --http-addr 127.0.0.1:8081 \
--cors-origins http://localhost:5173 \
--cors-origins https://app.internal.example.com

Origins require an exact scheme, host, and port. Wildcards, paths, queries, and fragments are rejected. Grant only origins you control because the HTTP API can start agent runs.

Local development only

In production, place a same-origin backend in front of mecated. Keep the bearer token on the server and enforce Origin and CSRF policy there.


Next steps