100% free and open source · every tool, every host, no licence key · read the licence
Secronyx

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_x runs hybrid mode.
  • SECRONYX_API_KEY=sk_x secronyx runs 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_x also runs hybrid mode, and opens no listener, despite what --help shows 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:

  1. With no API key, platform credentials come first. On Windows that is the DPAPI-protected credentials\device.enc under the config directory, written by the MSI, decrypted with CryptUnprotectData and expected in the PascalCase JSON the installer writes (DeviceToken, InstanceId, SaasUrl, Hostname, CreatedAt). On macOS it is the com.secronyx.agent / device-credentials generic password in the Keychain, read by shelling out to security find-generic-password and 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.
  2. Then hybrid_credentials.json in the config directory ({"instance_id", "device_token", "hostname", "registered_at"}, written with mode 0600 in a directory created 0700).
  3. With an API key present, the agent always re-registers: POST <saas-url>/v1/auth/device with Authorization: Bearer <api key>, sending device_name, device_type, hostname and os_version. The response's device id and device token replace whatever was stored, and are saved to hybrid_credentials.json. The API key is not used again — the device token is what authenticates the wake-up poll and the WebSocket.
  4. Failing all of that, stored login credentials from credentials.enc (AES-256-GCM under a key derived from hostname, home directory, GOOS/GOARCH and a static salt, mode 0600) 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 start

Put 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:

  1. Do not pass --api-key, and make sure SECRONYX_API_KEY is not set in the environment, in a systemd unit's Environment= line, or in a container's env.
  2. Clear stored credentials: secronyx --logout, then remove hybrid_credentials.json, credentials.enc, agent.crt, agent.key and registration.json from the config directory (~/.secronyx by default). On Windows, delete credentials\device.enc under the install folder; on macOS, delete the com.secronyx.agent / device-credentials generic password from the Keychain. --logout only removes credentials.enc; the rest is a manual delete.
  3. Stop and remove any installed service: secronyx service uninstall, or msiexec /x secronyx.msi /qn on Windows.
  4. Confirm at the network layer. A disabled agent makes no outbound connection to api.secronyx.com, wakeup.secronyx.com or ws.secronyx.com on 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.