feat: portals — expose agent-run dev servers to the operator (#122536)

* feat(protocol): add portal methods and event

Bump the reviewed protocol owner-module count from 55 to 56.

* feat(gateway): add portal service and reverse proxy

* feat(agents): add portal tool

* fix(gateway): refine portal URL and proxy auth

* refactor(gateway): keep portal helper types private

* fix(gateway): declare portal transport service

* test(gateway): satisfy portal proxy lint

* test(gateway): narrow websocket payload types

* refactor(protocol): compact portal schema exports

* fix(gateway): export portal protocol types

* feat(ui): add portals page

* docs(gateway): add portals guide

* fix(gateway): dial portal targets via localhost dual-stack

Vite and other Node >=17 dev servers bind ::1 only for localhost, so a
fixed 127.0.0.1 dial 502s on the default path. Use hostname localhost
with family autoselection and rewrite Host to match.

* fix(gateway): type portal dual-stack connection

* fix: satisfy portal integration gates

* fix(gateway): isolate portal cookie jars per target

Cookies are hostname-scoped, not port-scoped, so the per-port origin
split alone let Gateway plugin-auth cookies reach agent-run targets.
Forward only cookies carrying this portal's own name prefix (stripped),
rewrite target Set-Cookie names to the prefixed form incl. the WS 101
handshake, and drop Domain attributes.

* fix(ui): detect unreachable portals behind proxied gateways

Probe the portal origin from the browser (no-cors, 4s timeout) and show
a recovery notice with the gateway-host URL instead of a dead iframe
when only the gateway port is exposed (Serve/Funnel/reverse proxy).
Docs: cookie isolation + reachability; zh-CN glossary entry.

* test(ui): satisfy portal reachability lint

* test(gateway): provide control UI request hosts

* chore(protocol): regenerate after rebase

* fix(gateway): namespace portal auth cookies by listener

* fix(gateway): scope portal token URLs to write-capable clients

The portal bearer token rides in the summary url/tokenQuery; portal.list
is operator.read and portal.changed fans out to read subscribers, so a
read-only client could harvest an openable URL. Make those fields
optional, redact them from read-scope list responses, and drop them from
every portal.changed broadcast; write/admin clients still receive them
and the UI refetches the list on change.

* docs(web): list the portals route

* fix(gateway): type portal open credentials

* docs(gateway): clarify portals PORT/PUBLIC_URL are agent-set

Opening a portal creates only the proxy listener; the agent sets PORT
and PUBLIC_URL in its own exec command, matching the portal tool
contract. Removes the implication of an automatic env handoff.

* chore(protocol): regenerate portal models

* style(gateway): format portal method-order assertions

Rebase union-merge left the portal.list assertion wrapped; oxfmt fits it
on one line.

* chore(plugin-sdk): refresh API baseline after rebase

* chore(plugin-sdk): refresh API baseline after rebase

* chore(protocol): refresh portal event order after rebase

* chore(plugin-sdk): refresh API baseline after rebase

* fix(gateway): pin portal referrer policy to no-referrer

The portal URL carries its bearer token in the query, and upstream
response headers are copied verbatim, so a target answering with
Referrer-Policy: unsafe-url could leak that URL to every third-party
origin it references. Force no-referrer after the copy and drop any
inbound Referer that still carries the token before forwarding.
This commit is contained in:
Peter Steinberger
2026-08-13 00:46:11 -07:00
committed by GitHub
parent 4afccbdaa4
commit cc2fc55f9b
87 changed files with 3376 additions and 38 deletions
@@ -1 +1 @@
{"contentHash":"812e818c8d7c013b2287c4502426123222037a4235e130a69481272912062866","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
{"contentHash":"29e2f1aadc64c83fccf94d7fb4747b10e31bbe0922f4806ed3f23d8c1698e6c3","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"f639fea8b8ee53626452bdbce156724b09a3a29bb05a134eb8e8f8fb8062d4da","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
{"contentHash":"8407ad2fc13c783155999acdbef231efe60ba69538448a955ff5f10888e51d31","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"02db4abe2f1f4578d438f7afacd68d44114e46b48b9582711e7e10191353e6c5","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
{"contentHash":"f44495c27167838fbcdb0ba6afaafd770689e33bb57f725b45b8f9ac20af1565","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
@@ -1 +1 @@
{"contentHash":"6826237a93cc52b6039fddbd4b4e0e00e82b5c83cbadd36a055a50ab0480879c","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
{"contentHash":"29b2feadc45ec3f6c9a7281839142334bf1f8d4ab5fddbed200e99c5e2c51407","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
@@ -1 +1 @@
{"contentHash":"bc8881a906f40f0a3ede29eb83efc1e4d3e59b62c00154b2480d85d69cbe4010","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
{"contentHash":"90a7e6988de562edab9ee28696207bba9fba6d83488dc62d9e0b893ca9603dc6","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
@@ -1 +1 @@
{"contentHash":"f4b35d03ac9df9788462f3e50b64819ff85aba0245b1a51208a288bd994edb8e","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
{"contentHash":"43cedd1b0efa245682de25aa7922045223c58a004bd6a424f1c98968f50396bf","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
@@ -1 +1 @@
{"contentHash":"80b5ab5fedd16c952c83747f4093db46e49640b3cfa1fdaa5a9727ff31edf0a7","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
{"contentHash":"f478b7aa4001f84bb97202d38a75807a67afc2f2c07e074f9121f30298e7493f","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"edfa6b7a219aef521935ac17a20c5cd74e91bf4988eb3fad5ac75fc2e5015596","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
{"contentHash":"41d0227914b1eaf4f05dcbc6a92cbe1c824ae9415df8472a5edc0492f5b764ca","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"6490014377554ea6b62c657ab22c53fcec385b4ad24cdfccd95ebd9a79717e59","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
{"contentHash":"2e71c5a50c2efb3af7410ffb1cf7b6a6321b4f279c78ab5377fc984321251697","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
@@ -1 +1 @@
{"contentHash":"b691fcfb34a5f228938d06c50f9f8a26bb63ce06644e9df25f44876601e4cebb","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
{"contentHash":"067122d86f2c4c36fdebd428eb0183210a4cf3d5ac1ba5f082ae1120b57d14a3","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"}
@@ -1 +1 @@
{"contentHash":"bb5122c6ac5f4dfe381493b9d128a303108434edca9434ad5528781d005271b8","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
{"contentHash":"afddb2d3e7f79b386ed6074f45f85419a0bcae5a6ce49c0157524851c7261d5e","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
@@ -1 +1 @@
{"contentHash":"9060d4011e1249ca8aa0b01f2e2825d71b29eff14dc84f7f702440dc01e73460","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
{"contentHash":"a3a31f78a73ea9159d1779d354b84cff1036741ee3980367c5effc4dd29b1b67","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"a36c13699a318b3fb1b6701ab3280c45e5a4f5ca982cef29778d10af8a5ae97a","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
{"contentHash":"64593f3e4ff693f2041d34fd5f3ee6e96795b9b2d49bbd60173d89da7d203494","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"bdf0b57a425cac872d156021006cce6813893bee30e77c5ef4c055343697dba0","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
{"contentHash":"9e2037874e2efb4faa53078d6c71089927097ccf08ecb4ba5ca9bd3f4b64414a","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
@@ -1 +1 @@
{"contentHash":"62f6dc31a2b1789b3667fe16c2d465681aa4c01e2cd9b2a6da062fd995235963","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
{"contentHash":"bd3c786f5753a95f03474833f1f5a8872c55ebdf3d541b96c558843e06d3aa44","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"3662752cd7db434787d728355a4fb46e8f4be5b88dece9c29d96225bc02da6db","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"abf5f77043e2e1b8210541ed48d6ab5e22ec475bf05484d5f6103f82cfdd9f38","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
@@ -1 +1 @@
{"contentHash":"66ab292503af6befc63d5962f3312a8ebab4ab3bf3a56f47fe4f6a465d7c40b9","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
{"contentHash":"9c8a8d0ccdb9961abc35ca210b183a307df34e4ca2b1cf645d5db652cd7f8677","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}