Agent Instance Protocol Invocation Channels#
Description#
After an agent instance is created (Create Agent Instance) and stays running, an in-sandbox server listens on a port (e.g. sshd / WS server / HTTP server). External callers reach that port via one of frontend’s three L4 passthrough channels. All three are transparent to the upper-layer protocol, route by instance + port, and reuse the function_proxy tcp.tunnel:
Channel |
Endpoint |
Use case |
Route carrier |
|---|---|---|---|
SSH |
frontend |
Holds an SSH client; needs interactive shell / port forwarding |
SSH username field |
WebSocket |
|
Holds a WS client (browser, |
URL query |
HTTP |
|
Can only issue plain HTTP ( |
URL query |
All three ultimately go through resolveInstance → dialTunnel (function_proxy tcp.tunnel) to reach the in-container service. frontend and function_proxy only move bytes end-to-end and never parse upper-layer protocol frames.
Prerequisites#
The openYuanrong cluster is deployed and healthy; frontend is up (
yr start --master, frontend included by default).An instance has been created via Create Agent Instance and its
instance_idobtained; the instance isRUNNING.The in-sandbox server is listening on
?port(WS/HTTP) or the instance sshd port (SSH). A server not started / port not listening yields502(WS/HTTP) or connection failure (SSH).The function_proxy
tcp.tunnelis published (log lineTCP tunnel listening on 127.0.1.1:22775). The SSH channel additionally requiresssh_enable=trueat create time (injects the platform public key into the rootfs as a read-only mount, see the switch below).
Constraints#
Route key: WS/HTTP use URL query (
?instance=<id>&port=<n>); SSH carries the route in the username field (yr:instance:<id>:port=<n>) since the SSH handshake has no query string. All three resolve toinstance + port.L4 passthrough: frontend does not terminate the upper-layer protocol or parse frames. HTTP requests/responses (including streaming chunked / SSE), WS frames, and SSH channel bytes are relayed verbatim through the tunnel. Streaming response bytes flow back continuously; frontend does not buffer, assemble, or demux.
Auth: WS/HTTP share
IamConfig.EnableFuncTokenAuth(default off → takes?tenant_idordefault; on → validates JWT); SSH usesYR_FRONTEND_SSH_AUTH_ENABLEseparately (default on → validatesauthorized_keys). Cross-tenant check: the system tenant may reach any instance; otherwise the instance owner must match the caller.Failure paths (WS/HTTP): all pre-work (auth / route resolution / tunnel dial) happens in the HTTP layer before Hijack, returning a clean
4xx/502on failure; after Hijack there is no HTTP layer left to write an error code, connection issues surface as RST/FIN from the peer.Port range:
1–65535. Whenportis omitted in WS it defaults to18092(aligned with the AgentServer default WS port; only a “guess when omitted”, it does not constrain the server to listen on this port).
Switches and Configuration#
Deployment Switches#
All three channels depend on the function_proxy tcp.tunnel underneath. Whether this listener and each channel are started is controlled by the following top-level switches at cluster deploy time:
Switch |
Default |
Effect |
|---|---|---|
|
|
Enables the SSH channel. When on, it also turns on |
|
|
Enables |
Relationship in short:
To use the SSH channel → set only
ssh_enable=true(tcp.tunnelcomes along; no need to also setenable_tcp_tunnel).To use only the WS/HTTP channels, no SSH → set
enable_tcp_tunnel=true(no SSH keys configured).tcp.tunnellistens on127.0.1.1:22775(tcp_tunnel_portis tunable, default 22775) with a concurrent connection limit of 1024 (tcp_tunnel_max_connectionsis tunable). Once ready the log printsTCP tunnel listening on 127.0.1.1:22775.
SSH Channel Environment Variables#
The SSH bastion is enabled by YR_FRONTEND_SSH_ENABLE; once on, frontend starts an independent listener on :2222 and process-level behavior is tuned by the following environment variables:
Environment variable |
Default |
Description |
|---|---|---|
|
|
Whether to enable the SSH bastion. Empty / unset is treated as off. |
|
|
SSH bastion listen address. |
|
|
Whether to validate client public keys. Off → |
|
— |
Host private key file path frontend presents to clients (required when SSH is enabled). |
|
— |
Backend private key file path frontend uses to dial the instance sshd (required when SSH is enabled). |
|
— |
Client public key allowlist file path (required when |
|
|
Seconds to wait for the instance route to be ready (RUNNING + functionProxyID + tunnel address). |
|
|
Retry attempts after a failed dial to the instance sshd. |
|
|
Retry interval in milliseconds. |
|
|
Concurrent connection limit. |
SSH Key Configuration#
The SSH channel involves three key sets with distinct roles:
Key |
Role |
Environment variable |
Generation example |
|---|---|---|---|
host key |
frontend → client: the bastion’s identity to the outside |
|
|
backend key |
frontend → instance sshd: used for the second SSH hop |
|
|
authorized_keys |
client → frontend: the client public key allowlist |
|
each client runs |
The public key of the backend key must be injected into the instance rootfs authorized_keys (done automatically by ssh_enable=true at create time as a read-only mount), otherwise the instance sshd rejects the frontend’s backend connection. Host-key verification is intentionally disabled for the backend hop (InsecureIgnoreHostKey, because instance host keys are dynamic and frontend cannot know them in advance).
WS / HTTP Channel Switch#
WS and HTTP channels start with frontend and have no independent switch (routes r.GET("/serverless/v1/ws") / r.Any("/serverless/v1/http") are registered in InitRoute). Auth is governed uniformly by IamConfig.EnableFuncTokenAuth:
Config |
Default |
WS/HTTP behavior |
|---|---|---|
|
|
Off: takes |
|
|
On: validates JWT; token taken from |
Security warning:
enable_func_token_authis off by default. When off,?tenant_idis self-reported by the caller and fully trusted, and the system tenant (ID0) may reach any instance. Thus under the default configuration any unauthenticated caller can impersonate the system tenant by carryingtenant_id=0in the query and access any instance in the cluster — cross-tenant isolation does not hold. Production deployments must enableenable_func_token_auth=trueso the caller’s real tenant is decided by the JWTsub.
URI#
WebSocket#
GET /serverless/v1/ws?instance=<instance_id>&port=<port>&tenant_id=<tenant_id>(&token=<jwt>)
HTTP#
<METHOD> /serverless/v1/http?instance=<instance_id>&port=<port>&tenant_id=<tenant_id>(&token=<jwt>)
METHOD is arbitrary (GET / POST / PUT / DELETE …); the passthrough channel does not constrain the upper-layer method. port defaults to 18092 when omitted.
SSH#
ssh -p 2222 'yr:instance:<instance_id>:port=<port>'@<frontend_host>
The username uses the fixed prefix yr:instance:, followed by <instance_id> and optional port=<n> key-value pairs, segments separated by :. Each segment is url.PathUnescape-decoded by the server — percent-encode only when the instanceID or an option value itself contains URL-reserved characters like : (e.g. : → %3A); a plain UUID (e.g. e836df1a-...) must be passed verbatim, not encoded, otherwise it is treated as a different string and routing fails. When port is omitted it is determined by the instance rootfs.ports (the first sshd port).
Request Parameters#
WS / HTTP Query Parameters#
Parameter |
Required |
Type |
Description |
|---|---|---|---|
instance |
Yes |
string |
Target instance ID (UUID, from the create response). |
port |
No |
int |
In-sandbox server listen port, |
tenant_id |
Recommended when auth off |
string |
Caller tenant. Taken when auth is off (defaults to |
token |
Required when auth on |
string |
JWT. Must be provided when auth is on (also via |
SSH Username Parameters#
Segment |
Required |
Description |
|---|---|---|
|
Yes |
Fixed route prefix. |
|
Yes |
Fixed, indicates instance-based routing. |
|
Yes |
Target instance UUID. |
|
No |
In-container sshd port. When omitted, determined by the instance |
Response Parameters#
WS / HTTP#
The response body is produced by the in-sandbox server (not frontend). frontend relays the status line + headers + body verbatim, including streaming chunked / SSE — no assembly, no buffering. Failure paths (before Hijack) return:
HTTP status |
Trigger |
|---|---|
400 |
Missing |
401 |
Auth failure (no token / invalid JWT). |
403 |
Cross-tenant (caller is not system and does not own the instance). |
502 |
Instance does not exist / not |
SSH#
After the SSH handshake succeeds, an interactive session or port forwarding is entered; bytes are relayed bidirectionally. On handshake / route / backend dial failure the connection is refused and the client sees an SSH disconnect (not an HTTP status). Common failures: public key ... is not authorized (client public key not in authorized_keys), wait for instance ... route (instance not RUNNING or route not ready), backend sshd not started (port wrong / ssh_enable off so the public key was not injected).
Examples#
Prerequisite: create an instance with a server port#
# SSH: rootfs declares the sshd port, cluster enables ssh_enable (key auto-injected)
curl -X POST http://{frontend}:8888/api/agent -H "Content-Type: application/json" -d '{
"name": "agent-ssh", "namespace": "dev",
"runtime_spec": {
"runtime": "python3.11", "sandbox_type": "docker",
"rootfs": {"imageurl": "yr-docker-runtime:v0", "user": "agentos", "ports": ["tcp:22"]}
}
}'
# WS/HTTP: cmds starts the in-sandbox WS/HTTP server, rootfs.ports declares the listen port
curl -X POST http://{frontend}:8888/api/agent -H "Content-Type: application/json" -d '{
"name": "agent-http", "namespace": "dev",
"runtime_spec": {
"runtime": "python3.11", "sandbox_type": "supervisor",
"rootfs": {"imageurl": "ws-agent-runtime:latest", "user": "root", "ports": ["tcp:18092"]},
"cmds": [["python3.11", "/home/root/http_server.py"]],
"cpu": 2000, "memory": 4096
}
}'
Response (take the instance_id):
{"code":200,"instance_id":"e836df1a-b800-4000-8000-00004f815568"}
SSH Invocation#
# Interactive login (instance sshd on 22, rootfs.ports declares tcp:22, ssh_enable on)
ssh -p 2222 'yr:instance:e836df1a-b800-4000-8000-00004f815568'@<frontend_host> -i ~/.ssh/client_key
# Specify the port (instance sshd listens on 2222)
ssh -p 2222 'yr:instance:e836df1a-b800-4000-8000-00004f815568:port=2222'@<frontend_host> -i ~/.ssh/client_key
# Port forwarding: forward local 8080 to in-instance 18092
ssh -p 2222 -L 8080:127.0.0.1:18092 \
'yr:instance:e836df1a-b800-4000-8000-00004f815568'@<frontend_host> -i ~/.ssh/client_key
The username contains
:; in a shell it must be wrapped in single quotes.-p 2222is the frontend bastion port, not the instance sshd port. The client public key must be inYR_FRONTEND_SSH_AUTHORIZED_KEYS.
WebSocket Invocation#
# Auth off (default)
curl -sS -i -N \
"http://{frontend}:8888/serverless/v1/ws?instance=e836df1a-b800-4000-8000-00004f815568&port=18092&tenant_id=default" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=="
# Auth on (browsers cannot set custom headers; the JWT is carried via subprotocol)
const ws = new WebSocket(
"ws://{frontend}:8888/serverless/v1/ws?instance=<id>&port=18092",
["<jwt>"]
);
HTTP Invocation#
# Request-response round-trip
curl -sS -i -X POST \
"http://{frontend}:8888/serverless/v1/http?instance=e836df1a-b800-4000-8000-00004f815568&port=18092&tenant_id=default" \
-H "Content-Type: application/json" -d '{"any":"body"}'
# Streaming (SSE)
curl -sS -N \
"http://{frontend}:8888/serverless/v1/http?instance=e836df1a-b800-4000-8000-00004f815568&port=18092&tenant_id=default&mode=sse"
Observed response (produced by the in-sandbox server, relayed verbatim):
HTTP/1.1 200 OK
Content-Length: 59
method=POST path=/serverless/v1/http body=hello-from-client
Failure Paths#
curl -sS -o /dev/null -w "%{http_code}" \
"http://{frontend}:8888/serverless/v1/http?instance=<id>&port=18094&tenant_id=default" # 502 port not listening
curl -sS -o /dev/null -w "%{http_code}" \
"http://{frontend}:8888/serverless/v1/http?tenant_id=default&port=18092" # 400 missing instance
Error Codes#
The code (HTTP status) of the WS/HTTP channels is in the “Response Parameters” table above. The SSH channel has no HTTP status; failures surface as an SSH connection refusal.
HTTP status |
Description |
|---|---|
400 |
Bad request: missing |
401 |
Unauthorized: auth on but no token / invalid JWT. |
403 |
Forbidden: cross-tenant access and not the system tenant. |
502 |
Bad gateway: instance does not exist / not |