mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(protocol): add portal methods and event
Bump the reviewed protocol owner-module count from 55 to 56.
This commit is contained in:
@@ -19224,6 +19224,182 @@ public struct ShutdownEvent: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct PortalSummary: Codable, Sendable {
|
||||
public let id: String
|
||||
public let title: String
|
||||
public let port: Int
|
||||
public let listenport: Int
|
||||
public let tokenquery: String
|
||||
public let url: String
|
||||
public let path: String?
|
||||
public let description: String?
|
||||
public let createdatms: Int
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
title: String,
|
||||
port: Int,
|
||||
listenport: Int,
|
||||
tokenquery: String,
|
||||
url: String,
|
||||
path: String? = nil,
|
||||
description: String? = nil,
|
||||
createdatms: Int)
|
||||
{
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.port = port
|
||||
self.listenport = listenport
|
||||
self.tokenquery = tokenquery
|
||||
self.url = url
|
||||
self.path = path
|
||||
self.description = description
|
||||
self.createdatms = createdatms
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case title
|
||||
case port
|
||||
case listenport = "listenPort"
|
||||
case tokenquery = "tokenQuery"
|
||||
case url
|
||||
case path
|
||||
case description
|
||||
case createdatms = "createdAtMs"
|
||||
}
|
||||
}
|
||||
|
||||
public struct PortalListParams: Codable, Sendable {}
|
||||
|
||||
public struct PortalListResult: Codable, Sendable {
|
||||
public let portals: [PortalOpenResult]
|
||||
|
||||
public init(
|
||||
portals: [PortalOpenResult])
|
||||
{
|
||||
self.portals = portals
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case portals
|
||||
}
|
||||
}
|
||||
|
||||
public struct PortalOpenParams: Codable, Sendable {
|
||||
public let port: Int
|
||||
public let title: String?
|
||||
public let description: String?
|
||||
public let path: String?
|
||||
|
||||
public init(
|
||||
port: Int,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
path: String? = nil)
|
||||
{
|
||||
self.port = port
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.path = path
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case port
|
||||
case title
|
||||
case description
|
||||
case path
|
||||
}
|
||||
}
|
||||
|
||||
public struct PortalOpenResult: Codable, Sendable {
|
||||
public let id: String
|
||||
public let title: String
|
||||
public let port: Int
|
||||
public let listenport: Int
|
||||
public let tokenquery: String
|
||||
public let url: String
|
||||
public let path: String?
|
||||
public let description: String?
|
||||
public let createdatms: Int
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
title: String,
|
||||
port: Int,
|
||||
listenport: Int,
|
||||
tokenquery: String,
|
||||
url: String,
|
||||
path: String? = nil,
|
||||
description: String? = nil,
|
||||
createdatms: Int)
|
||||
{
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.port = port
|
||||
self.listenport = listenport
|
||||
self.tokenquery = tokenquery
|
||||
self.url = url
|
||||
self.path = path
|
||||
self.description = description
|
||||
self.createdatms = createdatms
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case title
|
||||
case port
|
||||
case listenport = "listenPort"
|
||||
case tokenquery = "tokenQuery"
|
||||
case url
|
||||
case path
|
||||
case description
|
||||
case createdatms = "createdAtMs"
|
||||
}
|
||||
}
|
||||
|
||||
public struct PortalCloseParams: Codable, Sendable {
|
||||
public let id: String
|
||||
|
||||
public init(
|
||||
id: String)
|
||||
{
|
||||
self.id = id
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
}
|
||||
}
|
||||
|
||||
public struct PortalCloseResult: Codable, Sendable {
|
||||
public let closed: Bool
|
||||
|
||||
public init(
|
||||
closed: Bool)
|
||||
{
|
||||
self.closed = closed
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case closed
|
||||
}
|
||||
}
|
||||
|
||||
public struct PortalChangedEvent: Codable, Sendable {
|
||||
public let portals: [PortalOpenResult]
|
||||
|
||||
public init(
|
||||
portals: [PortalOpenResult])
|
||||
{
|
||||
self.portals = portals
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case portals
|
||||
}
|
||||
}
|
||||
|
||||
public enum BoardOp: Codable, Sendable {
|
||||
case tabCreate(BoardTabCreateOp)
|
||||
case tabUpdate(BoardTabUpdateOp)
|
||||
|
||||
@@ -139,6 +139,14 @@ export {
|
||||
EnvironmentsListResultSchema,
|
||||
EnvironmentsStatusParamsSchema,
|
||||
EnvironmentsStatusResultSchema,
|
||||
PortalSummarySchema,
|
||||
PortalListParamsSchema,
|
||||
PortalListResultSchema,
|
||||
PortalOpenParamsSchema,
|
||||
PortalOpenResultSchema,
|
||||
PortalCloseParamsSchema,
|
||||
PortalCloseResultSchema,
|
||||
PortalChangedEventSchema,
|
||||
WorkerDesktopObserveParamsSchema,
|
||||
WorkerDesktopObserveResultSchema,
|
||||
WorkerDesktopLaunchParamsSchema,
|
||||
|
||||
@@ -50,6 +50,7 @@ export * from "./schema/terminal.js";
|
||||
export * from "./schema/ui-command.js";
|
||||
export * from "./schema/plugin-approvals.js";
|
||||
export * from "./schema/plugins.js";
|
||||
export * from "./schema/portals.js";
|
||||
export * from "./schema/projects.js";
|
||||
export * from "./schema/wizard.js";
|
||||
export * from "./schema/worker-admission.js";
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Value } from "typebox/value";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
PortalChangedEventSchema,
|
||||
PortalCloseResultSchema,
|
||||
PortalListResultSchema,
|
||||
PortalOpenResultSchema,
|
||||
PortalSummarySchema,
|
||||
validatePortalCloseParams,
|
||||
validatePortalListParams,
|
||||
validatePortalOpenParams,
|
||||
} from "../index.js";
|
||||
|
||||
const portal = {
|
||||
id: "p3000",
|
||||
title: "Development app",
|
||||
port: 3000,
|
||||
listenPort: 43123,
|
||||
tokenQuery: `openclaw_portal=${"a".repeat(64)}`,
|
||||
url: "http://127.0.0.1:43123/app",
|
||||
path: "/app",
|
||||
description: "Live preview",
|
||||
createdAtMs: 123,
|
||||
};
|
||||
|
||||
describe("portal protocol schemas", () => {
|
||||
it("accepts closed list, open, and close requests", () => {
|
||||
expect(validatePortalListParams({})).toBe(true);
|
||||
expect(validatePortalOpenParams({ port: 3000, title: "Development app", path: "/app" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(validatePortalCloseParams({ id: "p3000" })).toBe(true);
|
||||
expect(validatePortalListParams({ extra: true })).toBe(false);
|
||||
expect(validatePortalOpenParams({ port: 0 })).toBe(false);
|
||||
expect(validatePortalOpenParams({ port: 65_536 })).toBe(false);
|
||||
expect(validatePortalOpenParams({ port: 3000, path: "app" })).toBe(false);
|
||||
expect(validatePortalOpenParams({ port: 3000, host: "example.test" })).toBe(false);
|
||||
expect(validatePortalCloseParams({ id: "" })).toBe(false);
|
||||
});
|
||||
|
||||
it("validates summaries, results, and full replace-set events", () => {
|
||||
expect(Value.Check(PortalSummarySchema, portal)).toBe(true);
|
||||
expect(Value.Check(PortalOpenResultSchema, portal)).toBe(true);
|
||||
expect(Value.Check(PortalListResultSchema, { portals: [portal] })).toBe(true);
|
||||
expect(Value.Check(PortalCloseResultSchema, { closed: true })).toBe(true);
|
||||
expect(Value.Check(PortalChangedEventSchema, { portals: [portal] })).toBe(true);
|
||||
expect(Value.Check(PortalSummarySchema, { ...portal, targetPort: 3000 })).toBe(false);
|
||||
expect(Value.Check(PortalChangedEventSchema, { portal })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Type, type Static } from "typebox";
|
||||
import { closedObject } from "./closed-object.js";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
export const PortalSummarySchema = closedObject({
|
||||
id: NonEmptyString,
|
||||
title: NonEmptyString,
|
||||
port: Type.Integer({ minimum: 1, maximum: 65_535 }),
|
||||
listenPort: Type.Integer({ minimum: 1, maximum: 65_535 }),
|
||||
tokenQuery: NonEmptyString,
|
||||
url: NonEmptyString,
|
||||
path: Type.Optional(Type.String({ pattern: "^/" })),
|
||||
description: Type.Optional(Type.String()),
|
||||
createdAtMs: Type.Integer({ minimum: 0 }),
|
||||
});
|
||||
|
||||
export const PortalListParamsSchema = closedObject({});
|
||||
export const PortalListResultSchema = closedObject({
|
||||
portals: Type.Array(PortalSummarySchema),
|
||||
});
|
||||
|
||||
export const PortalOpenParamsSchema = closedObject({
|
||||
port: Type.Integer({ minimum: 1, maximum: 65_535 }),
|
||||
title: Type.Optional(NonEmptyString),
|
||||
description: Type.Optional(Type.String()),
|
||||
path: Type.Optional(Type.String({ pattern: "^/" })),
|
||||
});
|
||||
export const PortalOpenResultSchema = PortalSummarySchema;
|
||||
|
||||
export const PortalCloseParamsSchema = closedObject({ id: NonEmptyString });
|
||||
export const PortalCloseResultSchema = closedObject({ closed: Type.Boolean() });
|
||||
|
||||
export const PortalChangedEventSchema = closedObject({
|
||||
portals: Type.Array(PortalSummarySchema),
|
||||
});
|
||||
|
||||
export type PortalSummary = Static<typeof PortalSummarySchema>;
|
||||
export type PortalListParams = Static<typeof PortalListParamsSchema>;
|
||||
export type PortalListResult = Static<typeof PortalListResultSchema>;
|
||||
export type PortalOpenParams = Static<typeof PortalOpenParamsSchema>;
|
||||
export type PortalOpenResult = Static<typeof PortalOpenResultSchema>;
|
||||
export type PortalCloseParams = Static<typeof PortalCloseParamsSchema>;
|
||||
export type PortalCloseResult = Static<typeof PortalCloseResultSchema>;
|
||||
export type PortalChangedEvent = Static<typeof PortalChangedEventSchema>;
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as portals from "./portals.js";
|
||||
|
||||
export const PortalProtocolSchemas = {
|
||||
PortalSummary: portals.PortalSummarySchema,
|
||||
PortalListParams: portals.PortalListParamsSchema,
|
||||
PortalListResult: portals.PortalListResultSchema,
|
||||
PortalOpenParams: portals.PortalOpenParamsSchema,
|
||||
PortalOpenResult: portals.PortalOpenResultSchema,
|
||||
PortalCloseParams: portals.PortalCloseParamsSchema,
|
||||
PortalCloseResult: portals.PortalCloseResultSchema,
|
||||
PortalChangedEvent: portals.PortalChangedEventSchema,
|
||||
} as const;
|
||||
@@ -8,6 +8,7 @@ import { IntegrationProtocolSchemas } from "./protocol-schema-fragment-integrati
|
||||
import { NodeProtocolSchemas } from "./protocol-schema-fragment-nodes.js";
|
||||
import { OperationsProtocolSchemas } from "./protocol-schema-fragment-operations.js";
|
||||
import { PluginLifecycleProtocolSchemas } from "./protocol-schema-fragment-plugins-lifecycle.js";
|
||||
import { PortalProtocolSchemas } from "./protocol-schema-fragment-portals.js";
|
||||
import { SchedulerProtocolSchemas } from "./protocol-schema-fragment-scheduler.js";
|
||||
import { SessionCollaborationProtocolSchemas } from "./protocol-schema-fragment-sessions-collaboration.js";
|
||||
import { SessionCoreProtocolSchemas } from "./protocol-schema-fragment-sessions-core.js";
|
||||
@@ -30,6 +31,7 @@ export const ProtocolSchemas = composeProtocolSchemaFragments([
|
||||
SchedulerProtocolSchemas,
|
||||
ApprovalProtocolSchemas,
|
||||
PluginLifecycleProtocolSchemas,
|
||||
PortalProtocolSchemas,
|
||||
] as const);
|
||||
|
||||
export {
|
||||
|
||||
@@ -155,6 +155,9 @@ export const validateEnvironmentsCreateParams = compile(S.EnvironmentsCreatePara
|
||||
export const validateEnvironmentsDestroyParams = compile(S.EnvironmentsDestroyParamsSchema);
|
||||
export const validateEnvironmentsListParams = compile(S.EnvironmentsListParamsSchema);
|
||||
export const validateEnvironmentsStatusParams = compile(S.EnvironmentsStatusParamsSchema);
|
||||
export const validatePortalListParams = compile(S.PortalListParamsSchema);
|
||||
export const validatePortalOpenParams = compile(S.PortalOpenParamsSchema);
|
||||
export const validatePortalCloseParams = compile(S.PortalCloseParamsSchema);
|
||||
export const validateWorkerDesktopObserveParams = compile(S.WorkerDesktopObserveParamsSchema);
|
||||
export const validateWorkerDesktopObserveResult = compile(S.WorkerDesktopObserveResultSchema);
|
||||
export const validateWorkerDesktopLaunchParams = compile(S.WorkerDesktopLaunchParamsSchema);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"openclaw.approval.resolved": "OpenClaw system-agent config approvals are a web/desktop operator surface; iOS has no operator-approval prompt.",
|
||||
"plugin.approval.requested": "Plugin approval prompts are not implemented on iOS.",
|
||||
"plugin.approval.resolved": "Plugin approval prompts are not implemented on iOS.",
|
||||
"portal.changed": "Control-UI-only surface; native apps have no portal viewer yet.",
|
||||
"presence": "Presence roster is a control-UI (web/desktop) surface; iOS does not render it.",
|
||||
"session.approval": "Native approval review uses exec.approval push/nudge delivery; the session-scoped approval stream is a Control UI chat surface.",
|
||||
"session.operation": "Chat UI derives run state from chat/agent events; no session.operation consumer yet.",
|
||||
@@ -44,6 +45,7 @@
|
||||
"openclaw.approval.resolved": "OpenClaw system-agent config approvals are a web/desktop operator surface; Android has no operator-approval prompt.",
|
||||
"plugin.approval.requested": "Plugin approval prompts are not implemented on Android.",
|
||||
"plugin.approval.resolved": "Plugin approval prompts are not implemented on Android.",
|
||||
"portal.changed": "Control-UI-only surface; native apps have no portal viewer yet.",
|
||||
"presence": "Presence roster is a control-UI (web/desktop) surface; Android does not render it.",
|
||||
"session.approval": "Native approval review uses exec.approval push/nudge delivery; the session-scoped approval stream is a Control UI chat surface.",
|
||||
"session.operation": "Chat UI derives run state from chat/agent events; no session.operation consumer yet.",
|
||||
|
||||
@@ -101,6 +101,9 @@ const CURRENT_TRAIN_METHODS = [
|
||||
"desktop.launch",
|
||||
"device.scopes.requestUpgrade",
|
||||
"device.scopes.waitUpgrade",
|
||||
"portal.list",
|
||||
"portal.open",
|
||||
"portal.close",
|
||||
] as const;
|
||||
|
||||
describe("core gateway method release trains", () => {
|
||||
|
||||
@@ -520,6 +520,9 @@ const CORE_GATEWAY_METHOD_SPECS = [
|
||||
// Live device scope upgrades are additive so every older advertised index stays stable.
|
||||
["device.scopes.requestUpgrade", "devices", "operator.read", "2026.8"],
|
||||
["device.scopes.waitUpgrade", "devices", "operator.read", "2026.8"],
|
||||
["portal.list", "portals", "operator.read", "2026.8"],
|
||||
["portal.open", "portals", "operator.write", "2026.8", { controlPlaneWrite: true }],
|
||||
["portal.close", "portals", "operator.write", "2026.8", { controlPlaneWrite: true }],
|
||||
] as const satisfies readonly CoreGatewayMethodSpecRow[];
|
||||
|
||||
export type CoreGatewayHandlerFamily = Exclude<(typeof CORE_GATEWAY_METHOD_SPECS)[number][1], null>;
|
||||
|
||||
@@ -37,6 +37,7 @@ const EVENT_SCOPE_GUARDS: Record<string, string[]> = {
|
||||
chat: [READ_SCOPE],
|
||||
"board.changed": [READ_SCOPE],
|
||||
"board.command": [READ_SCOPE],
|
||||
"portal.changed": [READ_SCOPE],
|
||||
"ui.command": [READ_SCOPE],
|
||||
"chat.send_timing": [READ_SCOPE],
|
||||
"chat.side_result": [READ_SCOPE],
|
||||
|
||||
@@ -26,6 +26,10 @@ describe("GATEWAY_EVENTS", () => {
|
||||
expect(GATEWAY_EVENTS).toContain("skills.changed");
|
||||
});
|
||||
|
||||
it("advertises portal replace-set updates", () => {
|
||||
expect(GATEWAY_EVENTS).toContain("portal.changed");
|
||||
});
|
||||
|
||||
it("advertises session observer digests", () => {
|
||||
expect(GATEWAY_EVENTS).toContain("session.observer");
|
||||
});
|
||||
@@ -66,7 +70,7 @@ describe("listGatewayMethods", () => {
|
||||
});
|
||||
|
||||
it("appends new methods after model probing without shifting older method indices", () => {
|
||||
expect(listGatewayMethods().slice(-50)).toEqual([
|
||||
expect(listGatewayMethods().slice(-53)).toEqual([
|
||||
"models.probe",
|
||||
"migrations.memory.plan",
|
||||
"migrations.memory.apply",
|
||||
@@ -117,6 +121,9 @@ describe("listGatewayMethods", () => {
|
||||
"desktop.launch",
|
||||
"device.scopes.requestUpgrade",
|
||||
"device.scopes.waitUpgrade",
|
||||
"portal.list",
|
||||
"portal.open",
|
||||
"portal.close",
|
||||
]);
|
||||
const methods = listGatewayMethods();
|
||||
expect(methods.indexOf("node.pluginSurface.refresh")).toBe(
|
||||
@@ -208,7 +215,7 @@ describe("listGatewayMethods", () => {
|
||||
"exec.approval.get",
|
||||
]);
|
||||
expect(methods).toContain("tts.speak");
|
||||
expect(coreMethods.slice(-57)).toEqual([
|
||||
expect(coreMethods.slice(-60)).toEqual([
|
||||
"sessions.catalog.continue",
|
||||
"sessions.catalog.archive",
|
||||
"approval.get",
|
||||
@@ -266,6 +273,9 @@ describe("listGatewayMethods", () => {
|
||||
"desktop.launch",
|
||||
"device.scopes.requestUpgrade",
|
||||
"device.scopes.waitUpgrade",
|
||||
"portal.list",
|
||||
"portal.open",
|
||||
"portal.close",
|
||||
]);
|
||||
expect(methods.indexOf("approval.get")).toBeGreaterThan(methods.indexOf("tts.speak"));
|
||||
expect(methods.indexOf("approval.resolve")).toBe(methods.indexOf("approval.get") + 1);
|
||||
@@ -299,6 +309,11 @@ describe("listGatewayMethods", () => {
|
||||
expect(methods.indexOf("device.scopes.waitUpgrade")).toBe(
|
||||
methods.indexOf("device.scopes.requestUpgrade") + 1,
|
||||
);
|
||||
expect(methods.indexOf("portal.list")).toBe(
|
||||
methods.indexOf("device.scopes.waitUpgrade") + 1,
|
||||
);
|
||||
expect(methods.indexOf("portal.open")).toBe(methods.indexOf("portal.list") + 1);
|
||||
expect(methods.indexOf("portal.close")).toBe(methods.indexOf("portal.open") + 1);
|
||||
});
|
||||
|
||||
it("advertises the versioned Talk session RPCs", () => {
|
||||
|
||||
@@ -82,5 +82,6 @@ export const GATEWAY_EVENTS = [
|
||||
"openclaw.approval.resolved",
|
||||
"terminal.data",
|
||||
"terminal.exit",
|
||||
"portal.changed",
|
||||
GATEWAY_EVENT_UPDATE_AVAILABLE,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user