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

DirectionCarrierWriter and readerPurpose
User → Program/oasn/in/invocation.jsonThe platform writes it; the Program reads it only on the serverObtain the current user text, arguments, and attachment paths.
Program → userThe oasn-webui fenced block in the final Assistant replyAgent main writes it; the platform parses itProject 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"
    ]
  }
}
FieldTypeHow the Program uses it
input.user_promptstringThe current user's text. If it is blank, ask for the missing information instead of inventing a task.
input.argsarrayThe current production projection is normally an empty array. Do not depend on it unless the business contract explicitly requires it.
input.attachmentsstring[]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

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

StatusMeaningAction
pendingThe new mapping is convergingContinue only with get; do not repeat set/refresh with different parameters.
readySynchronization or proxy metadata checks passedCheck both sync_status and proxy_status, then perform real HTTP acceptance testing.
expiredThe Workspace connection or business proxy session expiredFirst distinguish the two expiration times. If necessary, run workspace resume with authorization; use refresh only when the business links need a new generation.
revokedThe previous business session was revokedSetting the same ports cannot revive it; refresh with authorization.
unavailableProxy validity cannot currently be confirmedReport the service or Version problem; do not clear or refresh by default.
not_configuredThe Version has no declared portsConfirm the complete list, then set it.

3. Implement a service that works under the proxy subpath

  1. Listen on all container interfaces

    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.

  2. 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 fixed start-webui.sh or a fixed health path.

  3. Preserve the proxy prefix

    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.

Website Version workspace without a WebUI-port form; business ports are configured with CLI 0.7.3
Current website boundary. The Program area still provides model, Resource, and development entry controls. Business WebUI ports are not in the form; manage them with the CLI commands on this page.

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}
```
FieldStrict rule
typeMust be the string "webui".
titleMust be a non-empty string. Use a page name that users can understand.
urlFor 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.
defaultOpenMust be a real JSON boolean, true or false, not the string "true".

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

  1. Local Sandbox service

    Confirm that the process listens on 0.0.0.0, access the target page and API through loopback, and verify the business response content.

  2. Port and proxy status

    Run version webui-ports get and confirm ports, sync_status=ready, proxy_status=ready, and the current webui_urls together.

  3. 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.

  4. 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