Deploy
Hybrid and SaaS mode
How the outbound agent works — wake-up long-polling and the WebSocket work channel, how an API key becomes a device token, exactly what leaves the host, the execution limits that bound it, and how to turn it off.
Hybrid mode inverts the network relationship. Instead of listening for calls, Secronyx connects out to the Secronyx service, waits for a wake-up, runs the requested tools and returns the results over that same outbound connection. Nothing listens on the host and no firewall rule is needed, which is what makes laptops, NAT'd branch servers and customer-managed machines reachable at all.
This page describes the agent as implemented in internal/agent and wired in cmd/secronyx/main.go. It is deliberately explicit about what leaves the host, because that is the question an operator has to answer before enabling it.
How it is selected
secronyx --mode hybrid --api-key sk_live_...There is one piece of implicit behaviour worth knowing before you set any environment variables. If --mode is empty and an API key is present — from --api-key or from SECRONYX_API_KEY — the binary sets the mode to hybrid itself, and the agent-mode switch runs before the transport switch. So:
secronyx --api-key sk_xruns hybrid mode.SECRONYX_API_KEY=sk_x secronyxruns hybrid mode, even though the command line looks like a plain stdio server.secronyx --transport http --listen 0.0.0.0:8443 --api-key sk_xalso runs hybrid mode, and opens no listener, despite what--helpshows for that combination.
The older inbound "SaaS agent mode" — the one that auto-generates a certificate, registers a callback URL and validates SaaS-issued JWTs against a JWKS — lives in the HTTP transport branch and is only reached when --mode is set to some value that is neither empty, polling nor hybrid. It is described at the end of this page. If you did not intend an outbound agent, do not put an API key in the environment.
The two states
The agent is a two-state machine.
IDLE. It long-polls the wake-up service: POST <wakeup-url>/v1/poll with {"instance_id": "...", "timeout_seconds": N}, Authorization: Bearer <device token> and User-Agent: secronyx-agent/1.0. The HTTP client for this path is tuned for long polls and for machines that suspend: keep-alives disabled so every poll uses a fresh connection, HTTP/1.1 forced, a 10-second dial and TLS handshake timeout, a 90-second response-header timeout, and an overall client timeout of the poll timeout plus 30 seconds. Poll errors back off exponentially from 1 second to 60 seconds; an authentication error waits the full 60 seconds rather than hammering the service. The poller is handed a fallback URL (<saas-url>/v1/agent/poll) and a 5-minute unavailability threshold, but the fallback is not implemented: past the threshold it notes that it would fall back and keeps polling the wake-up service.
ACTIVE. On a wake-up the agent connects a WebSocket to --ws-url with token and instance_id as query parameters, reads work messages, executes them and sends results back, with a heartbeat every 15 seconds. Connection attempts retry up to three times with backoff doubling from --ws-reconnect-delay (1s) to --ws-reconnect-max (60s); after three failures it waits 60 seconds and returns to IDLE. When no message arrives for --idle-timeout (30s) it closes the connection and returns to IDLE. A result message over 28 KB is split into result_chunk messages carrying about 27.5 KB of data each, to stay inside API Gateway's 32 KB frame limit.
The effect is that an idle host holds one long-poll and nothing else, and a busy host holds a WebSocket for as long as work keeps arriving.
Flags and defaults
| Flag | Default | Purpose |
|---|---|---|
--mode |
(empty) | hybrid, polling, or unset |
--api-key |
SECRONYX_API_KEY |
Registers the device; also auto-selects hybrid |
--saas-url |
https://api.secronyx.com |
Registration and polling fallback |
--wakeup-url |
https://wakeup.secronyx.com |
Wake-up long-poll endpoint |
--ws-url |
wss://ws.secronyx.com/v1 |
Work channel |
--instance-id |
(from credentials) | Device identity |
--device-token |
SECRONYX_DEVICE_TOKEN |
Wake-up and WebSocket credential |
--poll-timeout |
10s |
Long-poll wait |
--idle-timeout |
30s |
WebSocket idle before returning to IDLE |
--ws-reconnect-delay |
1s |
Initial reconnect backoff |
--ws-reconnect-max |
60s |
Maximum reconnect backoff |
--config-dir |
~/.secronyx |
Credentials and state (the exe directory for the Windows service) |
--notifications-queue-url |
— | Legacy SQS notification path |
--debug |
off | Verbose logging |
How credentials are resolved
An API key is an enrolment credential, not a runtime one. What the agent actually authenticates with is a device token, and the resolution order in runHybridMode is:
- With no API key, platform credentials come first. On Windows that is the DPAPI-protected
credentials\device.encunder the config directory, written by the MSI, decrypted withCryptUnprotectDataand expected in the PascalCase JSON the installer writes (DeviceToken,InstanceId,SaasUrl,Hostname,CreatedAt). On macOS it is thecom.secronyx.agent/device-credentialsgeneric password in the Keychain, read by shelling out tosecurity find-generic-passwordand parsed as{"device_token", "instance_id", "hostname", "saas_url", "registered_at"}. On every other platform there are no platform credentials and this step is skipped. - Then
hybrid_credentials.jsonin the config directory ({"instance_id", "device_token", "hostname", "registered_at"}, written with mode0600in a directory created0700). - With an API key present, the agent always re-registers:
POST <saas-url>/v1/auth/devicewithAuthorization: Bearer <api key>, sendingdevice_name,device_type,hostnameandos_version. The response's device id and device token replace whatever was stored, and are saved tohybrid_credentials.json. The API key is not used again — the device token is what authenticates the wake-up poll and the WebSocket. - Failing all of that, stored login credentials from
credentials.enc(AES-256-GCM under a key derived from hostname, home directory,GOOS/GOARCHand a static salt, mode0600) drive a token manager that refreshes an access token.
Two failures are worth recognising:
Error: No credentials found: --api-key, --device-token, --token, or stored credentials required for hybrid mode
Error: No instance ID: --instance-id is required for hybrid mode (or use --api-key for auto-registration)
For interactive enrolment there is an email/password flow:
secronyx --login --email you@example.com # POST /v1/auth/login, then /v1/auth/device
secronyx --logout # clears stored credentials--login prints the instance id and then tells you how to run or install the agent. Two things to know: the password prompt reads from stdin without disabling terminal echo, and the secronyx service install -- --mode hybrid line it prints hits the -- problem described under "Running it as a service" below.
What leaves the host
Only what a query returns, plus enrolment metadata and heartbeats. Concretely:
At registration: the hostname (sent as both device_name and hostname), the device type (runtime.GOOS) and an os_version of GOOS/GOARCH.
Per unit of work: in hybrid mode the WebSocket message (action: "result", or result_chunk when chunked) carries the work item id, request id, agent id, query name, a success flag, the collector's data, an error string if it failed and duration_ms. Polling mode POSTs the whole WorkResult, which additionally has the timed_out flag and the start and end times.
The data field is the collector's own JSON, and how it is produced matters: internal/agent/executor.go calls the collectors directly rather than going through the MCP server, so the server's scope policy is not in the path. --scopes and --enable-sensitive gate tool registration on the stdio and HTTP transports; they do not bound what a work item may ask for, and the executor's switch does accept sensitive-scope queries such as get_env_vars, get_process_environ, get_user_accounts, get_sudo_config, get_ssh_config, get_certificates and get_auth_logs. Redaction does still apply, because the collectors call the redact package themselves: it is enabled by default (passwords, tokens, connection strings with embedded credentials, JWTs and vendor token patterns) and only --no-redact or --redact=false turns it off. The real ceiling on an outbound agent is the executor's own query list: 202 names, against the 530 tools the MCP server registers; anything else comes back as unknown query: <name>. See Redaction and Scopes and authorization.
Nothing else is streamed. While ACTIVE the agent sends a heartbeat every 15 seconds carrying its agent id, a healthy status and a timestamp; there is no metric shipping, no log tailing and no file upload, and each result is the answer to one requested query.
Execution limits
The control plane cannot make the agent work harder than the binary allows. internal/agent/executor.go fixes three limits:
DefaultQueryTimeout— 30 seconds when a work item carries no timeout.MaxQueryTimeout— 120 seconds, a hard ceiling; a work item asking for more is clamped.DefaultMaxConcurrent— 4 queries at once, enforced by a semaphore.
The deadline semantics are honest about Go's limits: when a query overruns, Execute returns immediately with timed_out set and the result's error built from ErrQueryTimeout (query failures are reported in the WorkResult, never as a Go error to the caller), but the collector goroutine is not killed: Go cannot kill a goroutine, and most collectors do not take a context. Collectors that shell out through cmdexec.CommandContext are killed by their own timeouts; one blocked on a file read may keep running until it finishes, and its result is discarded. The concurrency cap is what stops orphans piling up: an orphan holds its slot, so a run of timeouts degrades into back-pressure rather than unbounded load.
Polling mode
--mode polling is the simpler, older outbound path: no WebSocket, just adaptive HTTP polling of <saas-url>/v1/agent/poll, results to /v1/agent/result and heartbeats to /v1/agent/heartbeat. The interval starts at 5 seconds, backs off by a factor of 1.5 towards 60 seconds when there is no work, and drops back to the minimum for 2 minutes after any work arrives; the HTTP timeout is 30 seconds. The agent id comes from SECRONYX_AGENT_ID, or is generated as agent-<hostname>-<pid>. It authenticates with an API key, a bearer token or stored credentials. Use it where a WebSocket cannot be established; otherwise hybrid mode costs less, because an idle agent holds one long poll instead of making a request every few seconds.
Inbound SaaS agent mode
For completeness: with an API key, --transport http and a --mode value that is neither empty, polling nor hybrid, the binary takes the inbound path instead. It creates ~/.secronyx (mode 0700), generates a self-signed ECDSA P-256 certificate valid for 365 days as agent.crt/agent.key (both mode 0600), regenerating when within 24 hours of expiry, and registers with POST <saas-url>/v1/agents/register carrying the callback URL and the public certificate. The response's agent_id and jwks_url are cached in registration.json, and incoming requests are then authenticated as JWTs validated against that JWKS. This requires the service to be able to reach the agent, which is exactly the constraint hybrid mode exists to remove.
Running it as a service
# Linux (systemd) and macOS (launchd), as root
secronyx service install --mode hybrid --api-key sk_live_...
secronyx service startPut the agent's flags directly after the action, with no -- separator. The -- that --help and --login show is itself captured as one of the service arguments, and the service's flag parsing stops at it, so service install -- --mode hybrid ... writes a unit that starts and then logs No valid mode specified, waiting for stop signal.... Note also that the service's own flag set does not auto-select hybrid mode from an API key the way the top-level binary does: --mode hybrid has to be there.
# Windows: the MSI always installs hybrid mode
msiexec /i secronyx.msi /qn API_KEY=sk_live_...Details of the generated systemd unit and of the MSI are in Remote access over HTTP and Windows service. Fleet-wide enrolment and rotation patterns are in Fleet deployment.
How to disable it
Hybrid mode is never entered by accident from stored credentials alone — it needs --mode hybrid or an API key. To be sure it is off:
- Do not pass
--api-key, and make sureSECRONYX_API_KEYis not set in the environment, in a systemd unit'sEnvironment=line, or in a container's env. - Clear stored credentials:
secronyx --logout, then removehybrid_credentials.json,credentials.enc,agent.crt,agent.keyandregistration.jsonfrom the config directory (~/.secronyxby default). On Windows, deletecredentials\device.encunder the install folder; on macOS, delete thecom.secronyx.agent/device-credentialsgeneric password from the Keychain.--logoutonly removescredentials.enc; the rest is a manual delete. - Stop and remove any installed service:
secronyx service uninstall, ormsiexec /x secronyx.msi /qnon Windows. - Confirm at the network layer. A disabled agent makes no outbound connection to
api.secronyx.com,wakeup.secronyx.comorws.secronyx.comon port 443; an egress rule denying those is a durable check that does not depend on how the binary was invoked.
The local stdio and HTTP transports are entirely independent of all of this and work with no account, no API key and no outbound connectivity. See Getting started.
Built 2026-09-19. Source: levantar-ai/secronyx. Found a mistake? Tell us.