Implement your Agent
Inputs, Files, and WebUI
Yes: production user input has structured JSON, but it is not the WebUI output JSON.
These two contracts flow in opposite directions: the platform writes the current invocation to /oasn/in/invocation.json for the Program to read; the Program writes a strict oasn-webui JSON block in its final reply for the platform to parse into a business page. Do not combine them, and do not let the browser read the former.
Distinguish the two JSON documents first
| Direction | Carrier | Writer and reader | Purpose |
|---|---|---|---|
| User → Program | /oasn/in/invocation.json | The platform writes it; the Program reads it only on the server | Obtain the current user text, arguments, and attachment paths. |
| Program → user | The oasn-webui fenced block in the final Assistant reply | Agent main writes it; the platform parses it | Project a running page as a WebUI view/canvas. |
1. Read structured user input
The following example shows business fields that are safe to display. It illustrates the file structure; it is not a request body for users to write. The real file is generated by the platform and also contains platform fields for the runtime contract.
{
"schema_version": "oasn.invocation.v1",
"invocation_id": "inv-example",
"run_id": "run-example",
"input": {
"user_prompt": "Read the attachment and generate an interactive sales dashboard",
"args": [],
"attachments": [
"/oasn/in/files/file-sales-csv"
]
}
}
| Field | Type | How the Program uses it |
|---|---|---|
input.user_prompt | string | The current user's text. If it is blank, ask for the missing information instead of inventing a task. |
input.args | array | The current production projection is normally an empty array. Do not depend on it unless the business contract explicitly requires it. |
input.attachments | string[] | Read-only server paths for the current Run. Read only the listed entries. |
Read the file only when the business logic truly needs the raw structure; ordinary OpenClaw Skills and Plugins normally receive the message directly. When parsing is required, keep the server-side read minimal:
from json import loads
from pathlib import Path
payload = loads(Path("/oasn/in/invocation.json").read_text(encoding="utf-8"))
business_input = payload.get("input", {})
user_prompt = business_input.get("user_prompt", "")
attachments = business_input.get("attachments", [])
if not isinstance(user_prompt, str) or not isinstance(attachments, list):
raise ValueError("INVALID_INVOCATION_INPUT")
Do not output, log, or screenshot the complete invocation.json
In addition to the business fields, the real file may contain short-lived runtime material. Do not use cat to write the full file to logs, return it through a WebUI API, or scan /oasn/in, an attachment's parent directory, or historical directories. Do not expose attachment paths to the browser or turn them directly into attachments for another OpenAgent.
2. Declare business ports with CLI 0.7.3
The website's Version form does not currently have WebUI-port controls. Configure ports through the Skill-verified oasn-sa-dev CLI. First confirm version 0.7.3 or later, then query the complete current list:
oasn-sa-dev --version
oasn-sa-dev version webui-ports get --version-id <agent_version_id>
After the developer explicitly confirms the complete list of ports to expose, run set. It replaces the entire list; it does not append:
# Replace the complete list with 7860 and 8765
oasn-sa-dev version webui-ports set --version-id <agent_version_id> --ports 7860,8765
# Clear the list only when all WebUIs are explicitly being disabled
oasn-sa-dev version webui-ports set --version-id <agent_version_id> --clear
- Each port must be a unique integer in
1..65535and must not use platform-reserved ports22,18789, or18790. - When there is no active Workspace, the Version declaration is only saved. When there is an active Workspace, the mapping is updated in place without rebuilding the Sandbox.
sync_status=readyproves only that declaration synchronization completed. Also confirmproxy_status=ready; only then should you deliver the currentwebui_urls.- A ready proxy is still not proof of business health. Visit the page and API for real; if WebSocket is used, also verify message round trips and reconnection.
- Setting the same ports again is an idempotent no-op and does not refresh links that have already become invalid.
To keep all ports and create a new generation of business links, first run get, obtain explicit authorization to refresh, and then run:
oasn-sa-dev version webui-ports refresh --version-id <agent_version_id>
How to interpret port status
| Status | Meaning | Action |
|---|---|---|
pending | The new mapping is converging | Continue only with get; do not repeat set/refresh with different parameters. |
ready | Synchronization or proxy metadata checks passed | Check both sync_status and proxy_status, then perform real HTTP acceptance testing. |
expired | The Workspace connection or business proxy session expired | First distinguish the two expiration times. If necessary, run workspace resume with authorization; use refresh only when the business links need a new generation. |
revoked | The previous business session was revoked | Setting the same ports cannot revive it; refresh with authorization. |
unavailable | Proxy validity cannot currently be confirmed | Report the service or Version problem; do not clear or refresh by default. |
not_configured | The Version has no declared ports | Confirm the complete list, then set it. |
3. Implement a service that works under the proxy subpath
Listen on all container interfaces
The business process must listen on0.0.0.0:<declared-port>, not only on localhost. The CLI only establishes the mapping; it does not start or stop the process or prove that it is healthy.Provide a business page and a minimal health endpoint
At minimum, make the target page and business API return verifiable content. The Program determines how it starts the process; the current contract does not require a fixedstart-webui.shor a fixed health path.Preserve the proxy prefix
The WebUI is mounted under a/webui/proxy/<id>/subpath.fetch('/api/health')andsrc="/vendor/app.js"discard that prefix. The page, SPA routes, static assets, and WebSocket should all resolve their base from the current URL.
const prefix = window.location.pathname.match(/^.*\/webui\/proxy\/[^/]+\//)?.[0]
const base = prefix
? new URL(prefix, window.location.origin)
: new URL('./', window.location.href)
fetch(new URL('api/health', base))
const scriptUrl = new URL('vendor/app.js', base)
Do not hard-code a Sandbox IP, 127.0.0.1, a development Workspace's public URL, or an external domain in page code. Note that the internal WebUI declaration in the next section must use a loopback URL; that does not conflict with the relative-address rule inside browser pages.

4. The final reply must contain strict oasn-webui JSON
After the page is running and reachable, make the final Assistant reply from main include ordinary explanatory text and one lowercase fenced block per page:
The dashboard is ready. Open it to view the results.
```oasn-webui
{"type":"webui","title":"Sales dashboard","url":"http://127.0.0.1:7860/results?range=q3#summary","defaultOpen":true}
```
| Field | Strict rule |
|---|---|
type | Must be the string "webui". |
title | Must be a non-empty string. Use a page name that users can understand. |
url | For a page in the same Sandbox, use http://127.0.0.1:<declared-port>/<business-path> or localhost. The field is named url, not uri. |
defaultOpen | Must be a real JSON boolean, true or false, not the string "true". |
- The JSON must be an object containing exactly these four fields. Do not add
token,description, or custom fields. - The fence info must be exactly lowercase
oasn-webui. Do not use an ordinaryjsonblock or append parameters to the fence info. - The port must be declared on the Version. Do not copy a public
webui_urlsvalue returned by the CLI into the block. - Internal HTTP allows only
127.0.0.1or localhost. An external page must use safe public HTTPS; public HTTP and private-network addresses are not allowed. - The URL must not contain a username or password, and its query or fragment must not carry sensitive state such as token, secret, signature, credential, password, authorization, or api_key.
- You may provide multiple blocks for multiple pages. The current parser preserves multiple
defaultOpen=truevalues, but the interaction contract allows at most one page to be true so that clients do not open several pages in succession. This is a developer rule; do not mistake it for server-side rejection.
Three common mistakes
# Wrong: defaultOpen is a string
{"type":"webui","title":"Dashboard","url":"http://127.0.0.1:7860/","defaultOpen":"true"}
# Wrong: the URL is relative and there is an extra custom field
{"type":"webui","title":"Dashboard","url":"/results","defaultOpen":true,"token":"..."}
# Wrong: the platform output field uri is used as a developer input field
{"type":"webui","title":"Dashboard","uri":"https://example.invalid/","defaultOpen":true}
A block with invalid JSON syntax, missing fields, or incorrect types does not become a WebUI view and normally remains verbatim in the text. If the structure is valid but the URL uses an undeclared port, a private-network address, or sensitive state, production projection closes with OPENCLAW_FINAL_REPLY_INVALID. If later result-layer validation fails again, it returns RUN_RESULT_INVALID or MCP_INVOCATION_RESULT_INVALID; it does not hide the error behind partial success.
5. url becomes uri in platform output
The developer is responsible only for the final reply in the previous section and does not construct the following object directly. After the platform validates and proxies the page, the client reads trusted structuredContent.views[], where the field is named uri:
{
"content": [
{"type": "text", "text": "The dashboard is ready. Open it to view the results."}
],
"structuredContent": {
"view": "invocation_result",
"status": "completed",
"invocation_id": "inv-example",
"views": [
{
"type": "webui",
"uri": "https://platform-controlled-host/webui/proxy/.../results?range=q3#summary",
"title": "Sales dashboard",
"defaultOpen": true
}
]
},
"isError": false
}
Integration clients should trust structuredContent.views[] instead of reparsing the fence in the text. A view with defaultOpen=false should remain a visible entry point; it simply does not open automatically. Failure to open a browser page also must not rewrite the Agent's completed terminal state or automatically invoke the Agent again.
6. Accept at four layers, not just by seeing HTTP 200
Local Sandbox service
Confirm that the process listens on0.0.0.0, access the target page and API through loopback, and verify the business response content.Port and proxy status
Runversion webui-ports getand confirmports,sync_status=ready,proxy_status=ready, and the currentwebui_urlstogether.Development proxy page
Open every new URL and verify the page, nested routes, static assets, and API. If WebSocket is used, verify round trips and reconnection. A 410 means that the business proxy session is invalid; it does not mean Owner OAuth is invalid. For a 502, check the business listener first.Final result and production invocation
Have the Agent return a real block, confirm that it is projected as a view/canvas, and verify that the path, query, and fragment are preserved without sensitive state. After publishing, run one more production invocation; development WebChat is not a substitute.
Reopening after completion: implemented in code, still awaiting formal product acceptance
The current implementation is designed to recover a production WebUI from the committed process snapshot for the same session. The number of ports does not determine recovery eligibility, and developers are not required to provide an additional fixed restart script. Do not promise that a page can always be reopened until real single-port and multi-port tasks have completed and their original result links have been reopened successfully in acceptance testing. A development Workspace link cannot substitute for this production acceptance.
Pre-publish I/O gates
- A normal request returns non-empty, useful final text consistent with the input.
- Structured input reads only
input.user_promptand the currentinput.attachments; it does not expose the complete file. - A file output can be downloaded and its contents verified; a local path alone is not sufficient.
- The port declaration is frozen, both
sync_statusandproxy_statusare ready, and every page has been tested for real. - Every WebUI block has exactly four fields, uses the matching port, and contains no sensitive URL state; the client receives a trusted
views[].uri. - Missing input, unsupported formats, an invalid proxy, or capability failure returns a stable, actionable error.