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.
| 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. |
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")
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.
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
1..65535 and must not use platform-reserved ports 22, 18789, or 18790.sync_status=ready proves only that declaration synchronization completed. Also confirm proxy_status=ready; only then should you deliver the current webui_urls.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>
| 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. |
The business process must listen on 0.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.
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 fixed start-webui.sh or a fixed health path.
The WebUI is mounted under a /webui/proxy/<id>/ subpath. fetch('/api/health') and src="/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.
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". |
token, description, or custom fields.oasn-webui. Do not use an ordinary json block or append parameters to the fence info.webui_urls value returned by the CLI into the block.127.0.0.1 or localhost. An external page must use safe public HTTPS; public HTTP and private-network addresses are not allowed.defaultOpen=true values, 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.# 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.
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.
Confirm that the process listens on 0.0.0.0, access the target page and API through loopback, and verify the business response content.
Run version webui-ports get and confirm ports, sync_status=ready, proxy_status=ready, and the current webui_urls together.
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.
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.
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.
input.user_prompt and the current input.attachments; it does not expose the complete file.sync_status and proxy_status are ready, and every page has been tested for real.views[].uri.