Documentation
¶
Overview ¶
Package cloud holds the client-side state for the "sx cloud" relay feature: credential storage, WebSocket dialing, and the MCP dispatcher loop.
A "relay" is an sx process (this one) serving a public MCP endpoint hosted by skills.new. The credential persisted here is the machine token minted during “sx cloud connect“ — it authenticates this sx instance's long-lived WebSocket connection back to pulse.
Storage layout:
- Non-secret metadata (relay base URL, GID) lives in a TOML file at “<config-dir>/cloud.toml“ with 0600 permissions.
- The machine token itself is stored in the OS keyring (macOS Keychain, Windows Credential Manager, freedesktop Secret Service on Linux) so a full-disk backup or a misconfigured backup agent can't leak it. If the keyring is unavailable (headless Linux without a Secret Service, containerized envs, etc.) we fall back to storing the token in the same TOML file with a visible warning — better than refusing to work on CI runners.
Index ¶
- Constants
- Variables
- func Delete() error
- func ParseRelayURL(raw string) (baseURL string, relayGID string, err error)
- func Path() (string, error)
- func Probe(ctx context.Context, cred *Credential, httpClient *http.Client) error
- func RevokeKeyringEntry(relayGID string) error
- func Save(cred *Credential) error
- func Serve(ctx context.Context, opts ServeOptions) error
- func SetTokenStore(ts TokenStore) (restore func())
- type Credential
- type ServeOptions
- type TokenStore
Constants ¶
const CredentialFileName = "cloud.toml"
CredentialFileName is the TOML file that holds the active relay's metadata. Stored under the user's sx config dir with 0600 permissions so nothing else on the machine can read even the fallback-mode token.
Variables ¶
var ErrNoCredential = errors.New("no sx cloud credential; run `sx cloud connect`")
ErrNoCredential is returned by Load when no cloud credential has been persisted yet. Callers check with errors.Is so "not attached" can be distinguished from genuine I/O failures.
ErrProbeUnauthorized is returned by Probe when the relay's auth handshake rejected our bearer token. Callers surface this as "token rejected; re-run `sx cloud connect`" to the user without needing to inspect the wrapped error.
var ErrTokenNotFound = errors.New("token not found in keyring")
ErrTokenNotFound is returned by TokenStore.Get when no token is stored for the account. Separate from ErrNoCredential so Load can tell "TOML exists but keyring forgot the token" (bad state) from "user never attached" (clean state).
Functions ¶
func Delete ¶
func Delete() error
Delete removes the persisted credential: TOML metadata file AND the keyring entry for this relay's GID. Returns nil if nothing was stored.
func ParseRelayURL ¶
ParseRelayURL takes a base URL like “https://app.skills.new/relay/SR.../“ and returns the cleaned URL and the relay GID segment. Used by “sx cloud attach“ to validate what the user pasted.
func Path ¶
Path returns the absolute path to the credential metadata file, creating the config dir if it doesn't already exist.
func Probe ¶
Probe performs a single-handshake verification of a credential against its relay. Used by “sx cloud connect“ / “sx cloud attach“ to fail fast with a readable error when the pasted token is wrong, rather than letting the failure surface only inside the long-running “sx cloud serve“ loop.
The probe dials the WebSocket endpoint with the stored Bearer token and closes cleanly. A successful handshake means the relay accepted the token and subscribed us — we don't need to stay connected. A 401/403 on handshake maps to “ErrProbeUnauthorized“.
“httpClient“ is optional; tests pass a stub, production leaves it nil. “ctx“ bounds the whole dance — a reasonable default is 5s.
func RevokeKeyringEntry ¶
RevokeKeyringEntry removes the keyring entry for a specific relay GID without touching the on-disk metadata. Used by the “--force“ swap path to clean up the old relay's token before saving the new one, so an orphan doesn't accumulate in the user's OS keychain.
No-op if the keyring doesn't know this GID. Errors are returned so callers can log them, but the caller should continue regardless — a stale keyring entry is a secret-hygiene regression, not a correctness blocker.
func Save ¶
func Save(cred *Credential) error
Save writes the credential atomically: the token goes to the OS keyring (with a TOML-file fallback on keyring failure), and the URL + GID land in the TOML file with 0600 permissions. If the TOML rename fails after the keyring write succeeded, we roll the keyring back so the caller is never left with a keyring entry that points at a relay the on-disk metadata doesn't know about.
func Serve ¶
func Serve(ctx context.Context, opts ServeOptions) error
Serve runs the WebSocket dispatcher loop, returning only when “ctx“ is cancelled or a non-recoverable error occurs. Reconnect attempts are made with exponential backoff; “ctx“ cancellation is observed at each backoff step.
func SetTokenStore ¶
func SetTokenStore(ts TokenStore) (restore func())
SetTokenStore replaces the active keyring backend and returns a function that restores the previous one. Intended for test helpers in “internal/cloud/cloudtest“ — production code has no reason to call this.
Types ¶
type Credential ¶
type Credential struct {
// RelayBaseURL is the base URL that includes the relay GID, e.g.
// “https://app.skills.new/relay/SR.../“. The trailing slash is
// preserved when written but not required on read.
RelayBaseURL string `toml:"relay_base_url"`
// RelayGID is the URL-facing opaque id ("SR..."). Derived from
// RelayBaseURL but stored for convenience.
RelayGID string `toml:"relay_gid"`
// MachineToken is the plaintext bearer token. NEVER serialized to
// TOML in the normal path — it lives in the OS keyring. Only
// written to the TOML file when the keyring is unavailable, and
// even then the file is 0600.
MachineToken string `toml:"machine_token,omitempty"`
}
Credential is the full persisted state for an attached relay. The RelayGID is derived from RelayBaseURL (“.../relay/<gid>/“) and stored explicitly so we don't have to re-parse the URL every time.
func Load ¶
func Load() (*Credential, error)
Load reads the stored credential, looking up the machine token in the OS keyring (or taking it from the TOML file when the keyring is unavailable). Returns ErrNoCredential if nothing has been persisted.
func (*Credential) Validate ¶
func (c *Credential) Validate() error
Validate returns an error if required fields are missing.
func (*Credential) WebSocketURL ¶
func (c *Credential) WebSocketURL() (string, error)
WebSocketURL returns the URL sx dials to subscribe for inbound MCP requests. Derived from “RelayBaseURL“ by swapping “http(s)“ for “ws(s)“ and appending “ws/“.
type ServeOptions ¶
type ServeOptions struct {
// Credential is the persisted relay credential (URL + machine
// token). Required.
Credential *Credential
// MCPServerFactory builds a freshly-initialized MCP server that
// exposes the local vault's tools. Each reconnect creates a new
// server + in-memory transport pair so stale state from a prior
// session can't leak across reconnects. Returning an error aborts
// the reconnect and surfaces it to the operator; a silent empty
// server would be indistinguishable from a healthy vault with no
// tools.
MCPServerFactory func() (*mcp.Server, error)
// HTTPClient, if set, overrides the HTTP client used for the
// WebSocket handshake. Tests inject a stub; production passes nil
// to use the default client.
HTTPClient *http.Client
}
ServeOptions collects the knobs for “sx cloud serve“. The zero value is NOT usable — Credential + MCPServerFactory must be set.
type TokenStore ¶
type TokenStore interface {
Set(account, token string) error
Get(account string) (string, error)
Delete(account string) error
}
TokenStore abstracts the OS keyring so tests can swap in an in-memory implementation. Exported rather than unexported because the sibling test-helper package (“internal/cloud/cloudtest“) needs to implement it for tests in neighbouring packages (e.g. “internal/commands“). Production code has no reason to install a custom store; “SetTokenStore“ is the only hook.