LAN agent
The LAN agent tag backs local gateway / CLI provisioning: discover supported protocol versions (LanAgentMetaResponse), mint short-lived bootstrap keys (LanAgentBootstrapTokenRequest / LanAgentBootstrapTokenResponse), fetch a bash installer (text/x-shellscript), swap a browser/session credential for an agent JWT (LanAgentCliTokenResponse), and drive integration-scoped queues (post_integration_lan_agent_task_spec, get_integration_lan_agent_tasks).
Related: URLs under /integrations/{integration_id}/lan-agent/... are also summarized in Integrations.
Operations vs wire routes
Section titled “Operations vs wire routes”| Concern | HTTP | operationId | Notes |
|---|---|---|---|
| Agent metadata | GET /lan-agent/meta | get_lan_agent_meta | LanAgentMetaResponse. Published bundle omits security (readable before auth in many deployments). |
| Bootstrap key | POST /lan-agent/cli/bootstrap-token | post_lan_agent_cli_bootstrap_token | JSON body; bearer_auth. 500 / 503 for signing or infra. |
| Bootstrap script | GET /lan-agent/cli/bootstrap.sh?key=… | get_lan_agent_cli_bootstrap_sh | Query key (ULID) required. 200 → text/x-shellscript (not JSON). |
| Agent JWT | POST /lan-agent/cli/token | post_lan_agent_cli_token | bearer_auth. LanAgentCliTokenResponse. Empty body on the wire unless your deployment extends it. |
| Task queue list | GET .../integrations/{integration_id}/lan-agent/tasks | get_integration_lan_agent_tasks | X-Org header required; 200 body described as a JSON object with task_ids. |
| Task spec push | POST .../integrations/{integration_id}/lan-agent/task-spec | post_integration_lan_agent_task_spec | X-Org + LanAgentTaskSpecRequest (task_id, lan_protocol_version, payload). LanAgentTaskSpecResponse. 426 if the agent/protocol is too old. |
Non-JSON response
Section titled “Non-JSON response”Treat GET /lan-agent/cli/bootstrap.sh as a shell script download. Go’s LANAgentAPIService returns *http.Response — read Body as bytes/text. Python and Rust openapp_sdk JSON decoders do not treat text/x-shellscript as a success body; avoid LanAgentClient.bootstrap_script() for real downloads until a bytes/stream helper exists.
SDK coverage
Section titled “SDK coverage”| Capability | Python | Rust (openapp_sdk) | Go | TypeScript (AsyncClient) |
|---|---|---|---|---|
/lan-agent/meta | client.lan_agent.meta() | client.lan_agent().meta() | LANAgentAPI.GetLanAgentMeta | Not on façade |
| Bootstrap token | lan_agent.bootstrap_token(...) | lan_agent().bootstrap_token(...) | PostLanAgentCliBootstrapToken | Not on façade |
Bootstrap script (?key=) | Prefer raw HTTP — JSON bridge cannot parse shell output | Prefer Transport::request_* streaming / raw bytes | GetLanAgentCliBootstrapSh.Key | Not on façade |
/lan-agent/cli/token | lan_agent.token(**body) — often {} | lan_agent().token(...) | PostLanAgentCliToken | Not on façade |
task-spec / tasks | submit_task_spec / list_tasks | Thin helpers (no explicit X-Org) — use transport + RequestSpec (extra_headers) for strict parity | PostIntegrationLanAgentTaskSpec, GetIntegrationLanAgentTasks + .XOrg(...) | Not on façade |
Typical errors
Section titled “Typical errors”401 on protected routes.post_integration_lan_agent_task_spec may return 426 (upgrade agent/protocol). post_lan_agent_cli_bootstrap_token may return 503 when JWKS / Redis / base URL is misconfigured.get_lan_agent_cli_bootstrap_sh returns 401 for bad/expired keys.
Examples
Section titled “Examples”Agent metadata (GET /lan-agent/meta)
Section titled “Agent metadata (GET /lan-agent/meta)”meta = await client.lan_agent.meta()import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
meta, httpResp, err := client.LANAgentAPI.GetLanAgentMeta(context.Background()).Execute()if err != nil { return err}defer httpResp.Body.Close()_ = metause openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let meta = client.lan_agent().meta().await?;const meta = await fetch("https://api.openapp.house/api/v1/lan-agent/meta").then( (r) => r.json() as Promise<{ supported_protocol_versions: number[] }>,);The bundle omits a security block — most deployments allow this read anonymously, so the API key is optional. Replace with AsyncClient.getLanAgentMeta once the Node façade exposes it.
Mint bootstrap key (POST /lan-agent/cli/bootstrap-token)
Section titled “Mint bootstrap key (POST /lan-agent/cli/bootstrap-token)”Replace task_id with the queued task ULID returned by get_integration_lan_agent_tasks (shape is deployment-specific).
out = await client.lan_agent.bootstrap_token( api_base_url="https://app.example.com/api/v1", task_id=task_ulid, integration_id=integration_ulid,)NewLanAgentBootstrapTokenRequest(apiBaseUrl, taskId) — set optional IntegrationId fields via setters when needed.
import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
body := *openapiclient.NewLanAgentBootstrapTokenRequest("https://app.example.com/api/v1", taskUlid)// body.SetIntegrationId(...)token, httpResp, err := client.LANAgentAPI.PostLanAgentCliBootstrapToken(context.Background()). LanAgentBootstrapTokenRequest(body). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = tokenuse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let body = json!({ "api_base_url": "https://app.example.com/api/v1", "task_id": task_ulid, "integration_id": integration_ulid,});let out = client.lan_agent().bootstrap_token(&body).await?;const out = await fetch( "https://api.openapp.house/api/v1/lan-agent/cli/bootstrap-token", { method: "POST", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", }, body: JSON.stringify({ api_base_url: "https://app.example.com/api/v1", task_id: taskUlid, integration_id: integrationUlid, }), },).then((r) => r.json() as Promise<{ key: string; expires_at: string }>);
const installerUrl = new URL( "https://api.openapp.house/api/v1/lan-agent/cli/bootstrap.sh",);installerUrl.searchParams.set("key", out.key);const installer = await fetch(installerUrl).then((r) => r.text());POST .../bootstrap-token mints a short-lived ULID; GET .../bootstrap.sh?key=… returns text/x-shellscript (use r.text(), not r.json()). Treat 500 / 503 on the token mint as transient infra errors. Replace with AsyncClient LAN agent helpers once the Node façade exposes them.
List queued tasks (X-Org required)
Section titled “List queued tasks (X-Org required)”ids = await client.lan_agent.list_tasks(integration_id)import ( "context" "io"
openapiclient "github.com/tomers/openapp-sdk/go")
resp, err := client.LANAgentAPI.GetIntegrationLanAgentTasks(context.Background(), integrationId). XOrg(orgId). Execute()if err != nil { return err}defer resp.Body.Close()body, err := io.ReadAll(resp.Body)if err != nil { return err}Decode body as JSON manually — OpenAPI describes a task_ids-style object.
use openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;use serde_json::Value;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let org_id = "01HORG00000000000000000000".to_string();let path = format!("/integrations/{integration_id}/lan-agent/tasks");let queued: Value = client .transport() .request_json(RequestSpec { method: Method::GET, path: &path, extra_headers: &[("X-Org", org_id)], ..Default::default() }) .await?;const queued = await fetch( `https://api.openapp.house/api/v1/integrations/${integrationId}/lan-agent/tasks`, { headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId, }, },).then((r) => r.json() as Promise<{ task_ids: string[] }>);Body shape is { task_ids: string[] }-style — drive a poll loop with the existing task_ids before re-pushing specs. Replace with AsyncClient.getIntegrationLanAgentTasks once the Node façade exposes it.
Exchange session for agent JWT (POST /lan-agent/cli/token)
Section titled “Exchange session for agent JWT (POST /lan-agent/cli/token)”Call with the same Bearer or session cookie you use for the dashboard API. The wire body is usually an empty JSON object ({}).
jwt_payload = await client.lan_agent.token()Add keyword arguments to token(...) only if your deployment extends the request body.
import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
out, httpResp, err := client.LANAgentAPI.PostLanAgentCliToken(context.Background()).Execute()if err != nil { return err}defer httpResp.Body.Close()_ = outuse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let out = client.lan_agent().token(&json!({})).await?;const jwt = await fetch("https://api.openapp.house/api/v1/lan-agent/cli/token", { method: "POST", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", }, body: JSON.stringify({}),}).then((r) => r.json() as Promise<{ token: string }>);Wire body is an empty JSON object unless the deployment extends it. Persist jwt.token for the agent process — it is not a long-lived API key. Replace with AsyncClient.postLanAgentCliToken once the Node façade exposes it.
Push task spec (POST /integrations/{integration_id}/lan-agent/task-spec)
Section titled “Push task spec (POST /integrations/{integration_id}/lan-agent/task-spec)”LanAgentTaskSpecRequest requires task_id, lan_protocol_version, and payload. X-Org is required on the wire (Go .XOrg(...); Rust uses transport + RequestSpec with extra_headers, same pattern as List queued tasks above).
spec = await client.lan_agent.submit_task_spec( integration_id, lan_protocol_version=1, task_id=task_ulid, payload={"example": "opaque-to-server"},)import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
body := *openapiclient.NewLanAgentTaskSpecRequest( int32(1), map[string]any{"example": "opaque-to-server"}, taskUlid,)out, httpResp, err := client.LANAgentAPI.PostIntegrationLanAgentTaskSpec(context.Background(), integrationId). XOrg(orgId). LanAgentTaskSpecRequest(body). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = outuse openapp_sdk::{Client, transport::RequestSpec};use reqwest::Method;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let org_id = "01HORG00000000000000000000".to_string();let path = format!("/integrations/{integration_id}/lan-agent/task-spec");let body = json!({ "task_id": task_ulid, "lan_protocol_version": 1, "payload": { "example": "opaque-to-server" },});let spec = client .transport() .request_json(RequestSpec { method: Method::POST, path: &path, body: Some(&body), extra_headers: &[("X-Org", org_id)], ..Default::default() }) .await?;const res = await fetch( `https://api.openapp.house/api/v1/integrations/${integrationId}/lan-agent/task-spec`, { method: "POST", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({ task_id: taskUlid, lan_protocol_version: 1, payload: { example: "opaque-to-server" }, }), },);
if (res.status === 426) { // Agent is on an older lan_protocol_version — surface an upgrade prompt. return;}const spec = await res.json();Body matches LanAgentTaskSpecRequest (task_id, lan_protocol_version, payload). Branch on 426 so callers prompt the LAN agent to upgrade instead of treating it as a generic transport error. Replace with AsyncClient.postIntegrationLanAgentTaskSpec once the Node façade exposes it.
HTTP contract: API reference · OpenAPI JSON