diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 68cb49478c88..d9db1417b903 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -1904,6 +1904,9 @@ public struct EnvironmentSummary: Codable, Sendable { public let type: String public let label: String? public let status: EnvironmentStatus + public let platform: String? + public let sessionhost: Bool? + public let trust: String? public let capabilities: [String]? public let worker: WorkerEnvironmentMetadata? @@ -1912,6 +1915,9 @@ public struct EnvironmentSummary: Codable, Sendable { type: String, label: String? = nil, status: EnvironmentStatus, + platform: String? = nil, + sessionhost: Bool? = nil, + trust: String? = nil, capabilities: [String]? = nil, worker: WorkerEnvironmentMetadata? = nil) { @@ -1919,6 +1925,9 @@ public struct EnvironmentSummary: Codable, Sendable { self.type = type self.label = label self.status = status + self.platform = platform + self.sessionhost = sessionhost + self.trust = trust self.capabilities = capabilities self.worker = worker } @@ -1928,6 +1937,9 @@ public struct EnvironmentSummary: Codable, Sendable { case type case label case status + case platform + case sessionhost = "sessionHost" + case trust case capabilities case worker } @@ -1956,6 +1968,9 @@ public struct EnvironmentsCreateResult: Codable, Sendable { public let type: String public let label: String? public let status: EnvironmentStatus + public let platform: String? + public let sessionhost: Bool? + public let trust: String? public let capabilities: [String]? public let worker: WorkerEnvironmentMetadata? @@ -1964,6 +1979,9 @@ public struct EnvironmentsCreateResult: Codable, Sendable { type: String, label: String? = nil, status: EnvironmentStatus, + platform: String? = nil, + sessionhost: Bool? = nil, + trust: String? = nil, capabilities: [String]? = nil, worker: WorkerEnvironmentMetadata? = nil) { @@ -1971,6 +1989,9 @@ public struct EnvironmentsCreateResult: Codable, Sendable { self.type = type self.label = label self.status = status + self.platform = platform + self.sessionhost = sessionhost + self.trust = trust self.capabilities = capabilities self.worker = worker } @@ -1980,6 +2001,9 @@ public struct EnvironmentsCreateResult: Codable, Sendable { case type case label case status + case platform + case sessionhost = "sessionHost" + case trust case capabilities case worker } @@ -2008,6 +2032,9 @@ public struct EnvironmentsDestroyResult: Codable, Sendable { public let type: String public let label: String? public let status: EnvironmentStatus + public let platform: String? + public let sessionhost: Bool? + public let trust: String? public let capabilities: [String]? public let worker: WorkerEnvironmentMetadata? @@ -2016,6 +2043,9 @@ public struct EnvironmentsDestroyResult: Codable, Sendable { type: String, label: String? = nil, status: EnvironmentStatus, + platform: String? = nil, + sessionhost: Bool? = nil, + trust: String? = nil, capabilities: [String]? = nil, worker: WorkerEnvironmentMetadata? = nil) { @@ -2023,6 +2053,9 @@ public struct EnvironmentsDestroyResult: Codable, Sendable { self.type = type self.label = label self.status = status + self.platform = platform + self.sessionhost = sessionhost + self.trust = trust self.capabilities = capabilities self.worker = worker } @@ -2032,6 +2065,9 @@ public struct EnvironmentsDestroyResult: Codable, Sendable { case type case label case status + case platform + case sessionhost = "sessionHost" + case trust case capabilities case worker } @@ -2076,6 +2112,9 @@ public struct EnvironmentsStatusResult: Codable, Sendable { public let type: String public let label: String? public let status: EnvironmentStatus + public let platform: String? + public let sessionhost: Bool? + public let trust: String? public let capabilities: [String]? public let worker: WorkerEnvironmentMetadata? @@ -2084,6 +2123,9 @@ public struct EnvironmentsStatusResult: Codable, Sendable { type: String, label: String? = nil, status: EnvironmentStatus, + platform: String? = nil, + sessionhost: Bool? = nil, + trust: String? = nil, capabilities: [String]? = nil, worker: WorkerEnvironmentMetadata? = nil) { @@ -2091,6 +2133,9 @@ public struct EnvironmentsStatusResult: Codable, Sendable { self.type = type self.label = label self.status = status + self.platform = platform + self.sessionhost = sessionhost + self.trust = trust self.capabilities = capabilities self.worker = worker } @@ -2100,6 +2145,9 @@ public struct EnvironmentsStatusResult: Codable, Sendable { case type case label case status + case platform + case sessionhost = "sessionHost" + case trust case capabilities case worker } diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index 2c86504e3d73..d99ac38ab48e 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"59fb241bbe47669d5d7f99a945a18d86f318b9af8c4c281bb11f01506c84ebbf","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"e4a7ffbb2daf39077b19f28d0dbf9267bb645be42ce18e24161966e34b16bdc7","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index d8bd1df6bd82..1833f5059110 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"b7195e12bbe582de01faacc9d3a22f5bc30e0210f7b0c03d3b6242b969f7a0be","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"517a4d6efc324c135d1761b9d8e77ed573162bcc8b662372cebdfd7c5e3083c1","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index c916c343013f..203834274d5b 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"04848d6e3e592c5edf260afa6ab27452770f99ef09e110809fab0813fd1671b0","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"007c23cfcc4af2cc62967750beabdb9825352b54afc3057c989c7e9179790ad0","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index 02b270894ae1..bf8bc40c34e2 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"656e29f955e218c9c35843a876eecd769b722e64fb8ea1849f266fa9a430ffd1","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"454a8b3a3ee15a1a691970f4b5cb15878b8f2f4e166d6600bf639f57292232f3","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json index 14dcaef32df9..9b88c0385485 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json @@ -1 +1 @@ -{"contentHash":"efed0dfd7d5ade67b9f4549b1505e8def917262e1940c9cbe10f761ff154811b","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} +{"contentHash":"aaadbf677c001d81a784acbe65ce9acb4d76794be31fecff9d2c9b8d984ec37c","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index b63227e93569..114a6844663d 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"f57838a5ab613e8e168c74cb309beef813587f5465ff80f18b8171320f579e7a","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"8bafa462130e5c1da30329c7face6a483957d4a0c3cc689b1632f1974420b69f","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index fa5b42274078..18108cd706e1 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"0b81e57bcb56659f98b52d55a1dea6f987bae23500a62d56daf4e86065929ac9","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"9e02fe7d5c9f80328c7f557420ee84687954f753dfd84b19e1469eb94b8dd168","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json index 8b48b54f231d..cf53792a6a90 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json @@ -1 +1 @@ -{"contentHash":"7375a14d1a2ead1dce9d13bc9a6c5c94dc7e809cde37860a7ace9fce480b6751","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} +{"contentHash":"982f42cd1ef5594aff26969ac256e5cf2f124f409b7e020fcad5117fc8322055","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index 5e031f0b5935..dd26b93016a8 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"57eaf2d515736c9a6880da9434dd6456e7744e75ac8a946709d9e5f30b166aed","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"a0b2dd06b690fe208c45c02e858934b527ed3c419f1a7ebd394423fc295cbff9","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index 872ff135b1d1..5e312e48516c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"bb239ef24687ac9b89335858dff0008f3b861546c242e3af3545fbcae1878309","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"402dc7fb02d64dc390510abbf1eb240ccb807b1bf3e26ddaa582c9393bdfb57a","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 87ac95ea1cca..05ecf007aa82 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"34a3ec8d182434d7682a6ae2258a4b1797ffc20f62a7a0449ca3055ccc8c69c5","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"4b0344b65821cc88ea69e7d6e8ca37ddfd21d258035e6ba7d613748df743c761","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json b/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json index 9c7c77875450..754a94714b19 100644 --- a/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/gateway-runtime.json @@ -1 +1 @@ -{"contentHash":"38dbab19f1c23c82c20443339807822f5dfc626d42171dbb71f3322992d01097","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"} +{"contentHash":"ffbc6c5c809ed0ceb135ea42926e321edf4265eb96c80e0f9df536d7eb472811","entrypoint":"gateway-runtime","importSpecifier":"openclaw/plugin-sdk/gateway-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index cf22bb77fc49..39d224aff695 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"080a0e85aaff22d1e860781d779254461b05e40e4af84b37197b9ae6281ca566","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"0ebc21dbb6435d83c96ddae60291abbf36a47292288533e1b921bf9501581895","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/infra-runtime.json b/docs/.generated/plugin-sdk-api-baseline/infra-runtime.json index 2c582c8f4fce..6190e3aba025 100644 --- a/docs/.generated/plugin-sdk-api-baseline/infra-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/infra-runtime.json @@ -1 +1 @@ -{"contentHash":"d83147f1ddbab6a62f7afc6ec7bbf5ec960b3abf428e6c9abe4a0f9518b0a8c7","entrypoint":"infra-runtime","importSpecifier":"openclaw/plugin-sdk/infra-runtime"} +{"contentHash":"463ab9088e5cbf0f008716d00676aaf2a4bdf0f13a6621cd42a76aac34f5b7a0","entrypoint":"infra-runtime","importSpecifier":"openclaw/plugin-sdk/infra-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/media-runtime.json b/docs/.generated/plugin-sdk-api-baseline/media-runtime.json index 55532189f010..3a43ce46cb20 100644 --- a/docs/.generated/plugin-sdk-api-baseline/media-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/media-runtime.json @@ -1 +1 @@ -{"contentHash":"04ca7bf038cd8ad69f3145ebd3c774b8b27b18ed96fa8227e289abcecdbd83b5","entrypoint":"media-runtime","importSpecifier":"openclaw/plugin-sdk/media-runtime"} +{"contentHash":"fe35d8eebcbfbdb20fc4a1e3b903e2dc7891dc2f12feb3e6130fec5dcd605376","entrypoint":"media-runtime","importSpecifier":"openclaw/plugin-sdk/media-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index c8116ff80bce..6dcd8cb9bea4 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"ecf1ca74a42f003b046e3bf2578bae03c609050c6cb16fa741f763d692937e67","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"458825733f53d1c06d468287d7059d2400202572082a8300d7806c71856498c7","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index 0cc601906bfe..71d11bee323e 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"a8e907e41641913ebb36b982bb320090197af1408a0196672c15d038883cf1e0","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"0cbd356b5509eb2bd0f744987a48e34726525e8932fe5a6bb9c7eb5884b55329","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index 4c888362e736..aa8b3e7e588c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"8d4c89bf2968217111f333c149b51bb6798dd961a62fbafeef1c19f2473e7ab6","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"f662a31a8c5db27d767489160e9d443df45190429e8f4fb9f6e184d12bac3f19","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index 27cdf069b69c..2c41811f6d01 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"5772bc63ec00a1de6e76827a9a8f79c1ed61493d07f6c2bb21f50c18a806e187","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"d0c70224a6b22aef9680ea81d32d071b11a2533c885dc8665080c6c00255ea4d","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json index 79cdc352ffaa..7b0ea384c738 100644 --- a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json +++ b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json @@ -1 +1 @@ -{"contentHash":"9a243e9ff6e6512ef4b635ed503159631e4e000a5dbc4d7743ffd091dfb1ef1d","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} +{"contentHash":"babd3de9e3fbbc52a328d73ae815f4df4dbc57377aa0a0ec5de08636b87ab542","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} diff --git a/docs/.generated/plugin-sdk-api-baseline/security-runtime.json b/docs/.generated/plugin-sdk-api-baseline/security-runtime.json index be8317cb10de..6eaafdeaf00a 100644 --- a/docs/.generated/plugin-sdk-api-baseline/security-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/security-runtime.json @@ -1 +1 @@ -{"contentHash":"ea0c884272571c2494865e709f61fd3a33e0da4e2096c301ab5d292783b923bb","entrypoint":"security-runtime","importSpecifier":"openclaw/plugin-sdk/security-runtime"} +{"contentHash":"d293aefd15df648be5808cdbe6194cb363cc3410e9e0ae01cf1d2162f8a475c5","entrypoint":"security-runtime","importSpecifier":"openclaw/plugin-sdk/security-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json index bb1b8d7247f9..0138ede67eee 100644 --- a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json +++ b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json @@ -1 +1 @@ -{"contentHash":"382193e8ef6f05d7bc0d872abefba471c374737bcc86db3810be780b893b72b4","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} +{"contentHash":"e843ae3223098e08b57d4af6efd78cc1693bb5ed3b7f589dee10c223d914b1bb","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} diff --git a/docs/.generated/plugin-sdk-api-baseline/ssrf-policy.json b/docs/.generated/plugin-sdk-api-baseline/ssrf-policy.json index d8b388e663d2..bf5d2f79c5fc 100644 --- a/docs/.generated/plugin-sdk-api-baseline/ssrf-policy.json +++ b/docs/.generated/plugin-sdk-api-baseline/ssrf-policy.json @@ -1 +1 @@ -{"contentHash":"c0619235a9636dc3fc22ab5ac74e7d14a01bf9c40594ced596a17143b6eb90c4","entrypoint":"ssrf-policy","importSpecifier":"openclaw/plugin-sdk/ssrf-policy"} +{"contentHash":"f15daa0e828f7f32901d14f7174b31883799c1bd6c1498d4295a85cbd045eeb6","entrypoint":"ssrf-policy","importSpecifier":"openclaw/plugin-sdk/ssrf-policy"} diff --git a/docs/.generated/plugin-sdk-api-baseline/ssrf-runtime.json b/docs/.generated/plugin-sdk-api-baseline/ssrf-runtime.json index 499a5a4c03b0..51c9c6382458 100644 --- a/docs/.generated/plugin-sdk-api-baseline/ssrf-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/ssrf-runtime.json @@ -1 +1 @@ -{"contentHash":"77efaea8a61aa4e0dccdffcf2d6e6bb4bcc758587691126c17905ffdf11ddd23","entrypoint":"ssrf-runtime","importSpecifier":"openclaw/plugin-sdk/ssrf-runtime"} +{"contentHash":"9df7566aad3ff6c1d41e693351d47e09f77f30a0b6d43a828afe7439b84fe36f","entrypoint":"ssrf-runtime","importSpecifier":"openclaw/plugin-sdk/ssrf-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index 5dc9f0ffad81..fc44f2286ac0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"993018e34f2e7951c2a0412a4df0cd4be50256da04f3d6a539942c3a8168da24","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"4962f66547dd18b573f4c3ee7ad08f35f220417559437ed2653ef1e3f07fb4be","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index 3d26f79a7fcb..6f05bba24054 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"079ce3652967694b58caa87ed04cdaba07b3766d82c491faa9a6f974d584e18b","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"326217df400252a236ce5f5e1f98adfcbbec79d3a5d068cf44fc3b7a70e0debe","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/docs/plan/runners.md b/docs/plan/runners.md index 88fab30f0dfc..b76e8e8aaf79 100644 --- a/docs/plan/runners.md +++ b/docs/plan/runners.md @@ -13,21 +13,21 @@ Proposal, revision 2. Supersedes revision 1 in place (2026-08-11, operator decision). Implementation in progress; update this table in every PR that advances a milestone. -| # | Milestone | Status | PRs | -| --- | ---------------------------------------------------------- | ----------- | ------- | -| 0 | This plan (revision 2) | landed | #122454 | -| 1a | Naming: session copy revert | landed | #120667 | -| 1b | Naming: devices consolidation | landed | #120689 | -| 1c | Cleanup: node-pairing → device-pairing merge | not started | — | -| 2 | `openclaw resume` + web Continue in terminal | in progress | #120664 | -| 3 | `openclaw connect` one-paste onboarding + `/j/` join route | not started | — | -| 4 | Picker: grouping, placement, liveness, enrichment | in progress | #120804 | -| 5 | Public worker ingress path | not started | — | -| 6 | Node worker provider (device runners) | not started | — | -| 7 | Bundle push consent + runner updates | not started | — | -| 8 | Stop-and-continue moves | not started | — | -| 9 | Deletions (ssh sandbox, openshell, exec-host clones, …) | not started | — | -| 10 | Cloud convergence (provisioners run `openclaw connect`) | not started | — | +| # | Milestone | Status | PRs | +| --- | ---------------------------------------------------------- | ----------- | ---------------- | +| 0 | This plan (revision 2) | landed | #122454 | +| 1a | Naming: session copy revert | landed | #120667 | +| 1b | Naming: devices consolidation | landed | #120689 | +| 1c | Cleanup: node-pairing → device-pairing merge | not started | — | +| 2 | `openclaw resume` + web Continue in terminal | in progress | #120664 | +| 3 | `openclaw connect` one-paste onboarding + `/j/` join route | not started | — | +| 4 | Picker: grouping, placement, liveness, enrichment | in progress | #120804, #122531 | +| 5 | Public worker ingress path | not started | — | +| 6 | Node worker provider (device runners) | not started | — | +| 7 | Bundle push consent + runner updates | not started | — | +| 8 | Stop-and-continue moves | not started | — | +| 9 | Deletions (ssh sandbox, openshell, exec-host clones, …) | not started | — | +| 10 | Cloud convergence (provisioners run `openclaw connect`) | not started | — | Revision history: revision 1 (2026-08-08) established the session/runner vocabulary, the naming rulings, and the milestone skeleton after a diff --git a/extensions/feishu/src/app-registration.test.ts b/extensions/feishu/src/app-registration.test.ts index ff191b9b44ba..b46e6937a4ac 100644 --- a/extensions/feishu/src/app-registration.test.ts +++ b/extensions/feishu/src/app-registration.test.ts @@ -35,10 +35,9 @@ type RegistrationFetchOptions = { const HERMETIC_PUBLIC_LOOKUP_ADDRESS = "93.184.216.34"; -const hermeticPublicLookup: LookupFn = (async (_hostname: string, _options?: unknown) => ({ - address: HERMETIC_PUBLIC_LOOKUP_ADDRESS, - family: 4, -})) as LookupFn; +const hermeticPublicLookup: LookupFn = async () => [ + { address: HERMETIC_PUBLIC_LOOKUP_ADDRESS, family: 4 }, +]; async function startLocalServer( handler: (req: IncomingMessage, res: ServerResponse) => void, diff --git a/extensions/feishu/src/streaming-card.test.ts b/extensions/feishu/src/streaming-card.test.ts index cb9a5985521b..1efafe66b2c0 100644 --- a/extensions/feishu/src/streaming-card.test.ts +++ b/extensions/feishu/src/streaming-card.test.ts @@ -43,10 +43,9 @@ type StreamingRequest = { const serverStops: Array<() => Promise> = []; const HERMETIC_PUBLIC_LOOKUP_ADDRESS = "93.184.216.34"; -const hermeticPublicLookup: LookupFn = (async (_hostname: string, _options?: unknown) => ({ - address: HERMETIC_PUBLIC_LOOKUP_ADDRESS, - family: 4, -})) as LookupFn; +const hermeticPublicLookup: LookupFn = async () => [ + { address: HERMETIC_PUBLIC_LOOKUP_ADDRESS, family: 4 }, +]; async function readRequestBody(req: IncomingMessage): Promise { let body = ""; diff --git a/extensions/msteams/src/attachments/shared.ts b/extensions/msteams/src/attachments/shared.ts index a49ce3d491d2..914425391eaa 100644 --- a/extensions/msteams/src/attachments/shared.ts +++ b/extensions/msteams/src/attachments/shared.ts @@ -677,6 +677,10 @@ async function safeFetch(params: { } if (!hasDispatcher) { + const lookupFn: LookupFn = async (hostname) => { + const resolved = await resolveFn(hostname); + return [{ ...resolved, family: resolved.address.includes(":") ? 6 : 4 }]; + }; const guarded = await fetchWithSsrFGuard({ url: currentUrl, fetchImpl: resolveGuardedFetchImpl({ @@ -690,7 +694,7 @@ async function safeFetch(params: { maxRedirects: MAX_SAFE_REDIRECTS, requireHttps: true, policy: resolveMediaSsrfPolicy(params.allowHosts), - lookupFn: resolveFn as LookupFn, + lookupFn, retainAuthorizationRedirectHostnameAllowlist: resolveRetainedAuthorizationRedirectHostnameAllowlist(params.authorizationAllowHosts), auditContext: "msteams.attachment", diff --git a/packages/gateway-protocol/src/schema/environments.test.ts b/packages/gateway-protocol/src/schema/environments.test.ts index 895122a3f36c..23abddbc29fd 100644 --- a/packages/gateway-protocol/src/schema/environments.test.ts +++ b/packages/gateway-protocol/src/schema/environments.test.ts @@ -82,7 +82,12 @@ describe("worker environment protocol schemas", () => { }); it("accepts worker metadata additively across summary and mutation results", () => { - const requested = workerSummary("requested"); + const requested = { + ...workerSummary("requested"), + platform: "linux", + sessionHost: false, + trust: "disposable", + }; const destroyedBase = workerSummary("destroyed", "unavailable"); const destroyed = { ...destroyedBase, @@ -140,7 +145,7 @@ describe("worker environment protocol schemas", () => { expect( Value.Check(EnvironmentsListResultSchema, { environments: [], - profiles: [{ id: "aws", providerId: "crabbox" }], + profiles: [{ id: "aws", providerId: "crabbox", trust: "disposable" }], }), ).toBe(true); expect( @@ -149,6 +154,12 @@ describe("worker environment protocol schemas", () => { profiles: [{ id: "aws", providerId: "crabbox", settings: { token: "hidden" } }], }), ).toBe(false); + expect( + Value.Check(EnvironmentsListResultSchema, { + environments: [], + profiles: [{ id: "aws", providerId: "crabbox", trust: "temporary" }], + }), + ).toBe(false); }); it("preserves summaries without worker metadata and rejects malformed worker metadata", () => { @@ -180,5 +191,11 @@ describe("worker environment protocol schemas", () => { worker: { ...workerSummary("failed").worker, error: "" }, }), ).toBe(false); + expect( + Value.Check(EnvironmentSummarySchema, { + ...workerSummary("ready", "available"), + trust: "temporary", + }), + ).toBe(false); }); }); diff --git a/packages/gateway-protocol/src/schema/environments.ts b/packages/gateway-protocol/src/schema/environments.ts index 22b621b8e69f..c1b0d5ce3a44 100644 --- a/packages/gateway-protocol/src/schema/environments.ts +++ b/packages/gateway-protocol/src/schema/environments.ts @@ -14,6 +14,10 @@ export const EnvironmentStatusSchema = Type.String({ enum: ["available", "unavailable", "starting", "stopping", "error"], }); +const EnvironmentTrustSchema = Type.String({ + enum: ["persistent", "disposable"], +}); + /** Durable lifecycle states for plugin-provisioned worker environments. */ export const WorkerEnvironmentStateSchema = Type.Union([ Type.Literal("requested"), @@ -65,6 +69,9 @@ function createEnvironmentSummarySchema() { type: NonEmptyString, label: Type.Optional(NonEmptyString), status: EnvironmentStatusSchema, + platform: Type.Optional(NonEmptyString), + sessionHost: Type.Optional(Type.Boolean()), + trust: Type.Optional(EnvironmentTrustSchema), capabilities: Type.Optional(Type.Array(NonEmptyString)), worker: Type.Optional(WorkerEnvironmentMetadataSchema), }); @@ -80,6 +87,7 @@ export const EnvironmentsListParamsSchema = closedObject({}); const WorkerEnvironmentProfileSummarySchema = closedObject({ id: NonEmptyString, providerId: NonEmptyString, + trust: Type.Optional(EnvironmentTrustSchema), }); /** List response containing all gateway-visible environment summaries. */ diff --git a/packages/sdk/src/app-sdk-composed-resources.e2e.test.ts b/packages/sdk/src/app-sdk-composed-resources.e2e.test.ts index 16ece49b2bed..7cd426180c65 100644 --- a/packages/sdk/src/app-sdk-composed-resources.e2e.test.ts +++ b/packages/sdk/src/app-sdk-composed-resources.e2e.test.ts @@ -756,6 +756,9 @@ async function proveRealGatewayContracts(): Promise { type: "local", label: "Gateway local", status: "available", + platform: process.platform, + sessionHost: true, + trust: "persistent", capabilities: ["agent.run", "sessions", "tools", "workspace"], }); const gatewayEnvironment = await oc.environments.status("gateway"); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 95850555d827..d0c2ec7443b6 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -79,6 +79,9 @@ export type EnvironmentSummary = { type: "local" | "gateway" | "node" | "managed" | "ephemeral" | (string & {}); label?: string; status: "available" | "unavailable" | "starting" | "stopping" | "error"; + platform?: string; + sessionHost?: boolean; + trust?: "persistent" | "disposable"; capabilities?: string[]; worker?: WorkerEnvironmentMetadata; }; @@ -91,6 +94,7 @@ export type EnvironmentCreateParams = { export type WorkerEnvironmentProfileSummary = { id: string; providerId: string; + trust?: "persistent" | "disposable"; }; export type EnvironmentsListResult = { diff --git a/src/gateway/server-lifecycle.ts b/src/gateway/server-lifecycle.ts index 8ff49fbac1e7..b2a2fb68033f 100644 --- a/src/gateway/server-lifecycle.ts +++ b/src/gateway/server-lifecycle.ts @@ -202,7 +202,7 @@ export async function prepareGatewayLifecycle(params: { instanceId: session.nodeId, reason: "connect", }); - incrementPresenceVersion(); + broadcastPresenceSnapshot({ broadcast, incrementPresenceVersion, getHealthVersion }); recordRemoteNodeInfo({ nodeId: session.nodeId, connId: session.connId, diff --git a/src/gateway/server-methods/environments.test.ts b/src/gateway/server-methods/environments.test.ts index 8283489e461f..938612464594 100644 --- a/src/gateway/server-methods/environments.test.ts +++ b/src/gateway/server-methods/environments.test.ts @@ -99,6 +99,7 @@ function workerRecord(overrides: Partial = {}): TestWorkerReco profileSnapshot: { settings: {} }, provisionOperationId: "provision:worker-1", leaseId: "lease-1", + sharedHost: false, desktop: null, sshEndpoint: { host: "worker.example.test", @@ -208,6 +209,9 @@ describe("environment gateway methods", () => { type: "local", label: "Gateway local", status: "available", + platform: process.platform, + sessionHost: true, + trust: "persistent", capabilities: ["agent.run", "sessions", "tools", "workspace"], }, { @@ -215,6 +219,9 @@ describe("environment gateway methods", () => { type: "node", label: "Live Node", status: "available", + platform: "ios", + sessionHost: false, + trust: "persistent", capabilities: ["camera", "system.run"], }, { @@ -222,6 +229,8 @@ describe("environment gateway methods", () => { type: "node", label: "Offline Node", status: "unavailable", + sessionHost: false, + trust: "persistent", capabilities: ["camera.snap", "screen"], }, ], @@ -254,6 +263,7 @@ describe("environment gateway methods", () => { id: "worker-1", type: "worker", status: "available", + trust: "disposable", worker: { providerId: "static-ssh", leaseId: "lease-1", @@ -285,6 +295,18 @@ describe("environment gateway methods", () => { expect(summarizeWorkerEnvironment(workerRecord({ state }), NOW).status).toBe(status); }); + it("projects trust from recorded worker isolation without guessing unknown leases", () => { + expect(summarizeWorkerEnvironment(workerRecord({ sharedHost: true }), NOW).trust).toBe( + "persistent", + ); + expect(summarizeWorkerEnvironment(workerRecord({ sharedHost: false }), NOW).trust).toBe( + "disposable", + ); + expect(summarizeWorkerEnvironment(workerRecord({ sharedHost: null }), NOW)).not.toHaveProperty( + "trust", + ); + }); + it("projects recorded errors only for terminal error states", () => { expect( summarizeWorkerEnvironment( @@ -324,6 +346,9 @@ describe("environment gateway methods", () => { type: "node", label: "Live Node", status: "available", + platform: "ios", + sessionHost: false, + trust: "persistent", capabilities: ["camera", "system.run"], }); }); @@ -341,6 +366,7 @@ describe("environment gateway methods", () => { expect(payload).toMatchObject({ id: "worker-1", status: "available", + trust: "disposable", worker: { state: "attached", ageMs: 9_000 }, }); expect(get).toHaveBeenCalledWith("worker-1"); diff --git a/src/gateway/server-methods/environments.ts b/src/gateway/server-methods/environments.ts index 100123cebdcd..abd31671bf17 100644 --- a/src/gateway/server-methods/environments.ts +++ b/src/gateway/server-methods/environments.ts @@ -25,6 +25,9 @@ const GATEWAY_ENVIRONMENT: EnvironmentSummary = { type: "local", label: "Gateway local", status: "available", + platform: process.platform, + sessionHost: true, + trust: "persistent", capabilities: ["agent.run", "sessions", "tools", "workspace"], }; const WORKER_STATUS: Record = { @@ -54,11 +57,15 @@ function summarizeNodeEnvironment(node: NodeListNode): EnvironmentSummary { // Expose both declared capabilities and command names so older node // runtimes still advertise useful execution surfaces in one stable list. const capabilities = uniqueSortedStrings(node.caps, node.commands); + const platform = node.platform?.trim(); return { id: `node:${node.nodeId}`, type: "node", label: node.displayName ?? node.nodeId, status: node.connected ? "available" : "unavailable", + ...(platform ? { platform } : {}), + sessionHost: false, + trust: "persistent", ...(capabilities.length > 0 ? { capabilities } : {}), }; } @@ -71,6 +78,9 @@ export function summarizeWorkerEnvironment( id: record.environmentId, type: "worker", status: WORKER_STATUS[record.state], + ...(record.sharedHost === null + ? {} + : { trust: record.sharedHost ? "persistent" : "disposable" }), worker: { providerId: record.providerId, ...(record.leaseId ? { leaseId: record.leaseId } : {}), diff --git a/src/gateway/worker-environments/service-contract.ts b/src/gateway/worker-environments/service-contract.ts index fedfff02d9e1..c7a4db4c17f0 100644 --- a/src/gateway/worker-environments/service-contract.ts +++ b/src/gateway/worker-environments/service-contract.ts @@ -24,6 +24,7 @@ export type WorkerEnvironmentServiceRecord = { environmentId: string; providerId: string; leaseId: string | null; + sharedHost: boolean | null; state: WorkerEnvironmentState; ownerEpoch: number; createdAtMs: number; diff --git a/src/infra/net/ssrf.ts b/src/infra/net/ssrf.ts index cd8f03d2f997..2fe00508a78b 100644 --- a/src/infra/net/ssrf.ts +++ b/src/infra/net/ssrf.ts @@ -43,7 +43,7 @@ export class SsrFBlockedError extends Error { } } -export type LookupFn = typeof dnsLookup; +export type LookupFn = (hostname: string, options: { all: true }) => Promise; export type SsrFPolicy = { allowPrivateNetwork?: boolean; @@ -607,9 +607,7 @@ export async function resolvePinnedHostnameWithPolicy( ); const lookupFn = params.lookupFn ?? dnsLookup; - const results = normalizeLookupResults( - (await lookupFn(normalized, { all: true })) as LookupResult, - ); + const results = normalizeLookupResults(await lookupFn(normalized, { all: true })); if (results.length === 0) { throw new Error(`Unable to resolve hostname: ${hostname}`); } diff --git a/ui/src/e2e/new-session-page.places-live.e2e.test.ts b/ui/src/e2e/new-session-page.places-live.e2e.test.ts new file mode 100644 index 000000000000..9c3c1628a35d --- /dev/null +++ b/ui/src/e2e/new-session-page.places-live.e2e.test.ts @@ -0,0 +1,135 @@ +import { expect, it } from "vitest"; +import { + WORKSPACE, + captureUiProof, + captureUiProofEnabled, + createNewSessionPageE2eSuite, + installMockGateway, + pollLocatorText, +} from "./new-session-page.test-support.ts"; + +const suite = createNewSessionPageE2eSuite(); + +suite.define(() => { + it("hides the destination axis when the Gateway is the only place", async () => { + const context = await suite.browser.newContext({ locale: "en-US", serviceWorkers: "block" }); + const page = await context.newPage(); + const gateway = await installMockGateway(page, { + workspace: WORKSPACE, + workspaceGit: true, + methodResponses: { + "node.list": { nodes: [] }, + "environments.list": { environments: [], profiles: [] }, + }, + }); + + try { + await page.goto(`${suite.server.baseUrl}new`); + await gateway.waitForRequest("node.list"); + const trigger = page.locator("#new-session-place-trigger"); + await pollLocatorText(trigger.locator(".new-session-page__trigger-label")).toBe("openclaw"); + await trigger.click(); + const place = page.locator("wa-popover.new-session-page__place-popover"); + expect(await place.getByText("This gateway", { exact: true }).count()).toBe(0); + await place.getByText("Runs on Gateway · local", { exact: true }).waitFor(); + } finally { + await context.close(); + } + }); + + it("refreshes destinations from gateway events while the picker stays open", async () => { + const context = await suite.browser.newContext({ + locale: "en-US", + serviceWorkers: "block", + ...(captureUiProofEnabled + ? { + recordVideo: { + dir: ".artifacts/control-ui-e2e/picker-liveness", + size: { height: 900, width: 1280 }, + }, + viewport: { height: 900, width: 1280 }, + } + : {}), + }); + const page = await context.newPage(); + const gateway = await installMockGateway(page, { + workspace: WORKSPACE, + workspaceGit: true, + methodResponses: { + "node.list": { + nodes: [ + { + nodeId: "existing-mac", + displayName: "Existing Mac", + connected: true, + commands: ["system.run"], + }, + ], + }, + "environments.list": { environments: [], profiles: [] }, + }, + }); + + try { + await page.goto(`${suite.server.baseUrl}new`); + await gateway.waitForRequest("node.list"); + await gateway.waitForRequest("environments.list"); + const trigger = page.locator("#new-session-place-trigger"); + const place = page.locator("wa-popover.new-session-page__place-popover"); + await trigger.click(); + await place.getByRole("button", { name: "Existing Mac" }).waitFor(); + const nodeRequests = (await gateway.getRequests("node.list")).length; + const environmentRequests = (await gateway.getRequests("environments.list")).length; + await gateway.setMethodResponse("node.list", { + nodes: [ + { + nodeId: "existing-mac", + displayName: "Existing Mac", + connected: true, + commands: ["system.run"], + }, + { + nodeId: "new-mac", + displayName: "New Mac", + connected: true, + commands: ["system.run"], + }, + ], + }); + await gateway.emitGatewayEvent("presence", { + presence: [ + { deviceId: "existing-mac", mode: "node", reason: "connect", ts: 1 }, + { deviceId: "new-mac", mode: "node", reason: "connect", ts: 2 }, + ], + }); + + await expect + .poll(async () => (await gateway.getRequests("node.list")).length) + .toBeGreaterThan(nodeRequests); + await place.getByRole("button", { name: "New Mac" }).waitFor(); + await place.getByText("This gateway", { exact: true }).waitFor(); + await place.getByText("Your devices", { exact: true }).waitFor(); + expect(await place.getAttribute("open")).not.toBeNull(); + expect(await gateway.getRequests("environments.list")).toHaveLength(environmentRequests); + + await gateway.setMethodResponse("environments.list", { + environments: [], + profiles: [{ id: "aws", providerId: "crabbox", trust: "disposable" }], + }); + await gateway.emitGatewayEvent("config.changed", { + path: "/tmp/openclaw.json", + hash: "picker-cloud-refresh", + ts: 3, + }); + await expect + .poll(async () => (await gateway.getRequests("environments.list")).length) + .toBeGreaterThan(environmentRequests); + await place.getByText("Cloud", { exact: true }).waitFor(); + await place.getByRole("button", { name: "Cloud · aws" }).waitFor(); + expect(await place.getAttribute("open")).not.toBeNull(); + await captureUiProof(page, "picker-after-live-regroup.png"); + } finally { + await context.close(); + } + }); +}); diff --git a/ui/src/e2e/new-session-page.places.e2e.test.ts b/ui/src/e2e/new-session-page.places.e2e.test.ts index eee3326b3b36..b162a31e6ff0 100644 --- a/ui/src/e2e/new-session-page.places.e2e.test.ts +++ b/ui/src/e2e/new-session-page.places.e2e.test.ts @@ -522,32 +522,6 @@ suite.define(() => { } }); - it("hides the destination axis when the Gateway is the only place", async () => { - const context = await suite.browser.newContext({ locale: "en-US", serviceWorkers: "block" }); - const page = await context.newPage(); - const gateway = await installMockGateway(page, { - workspace: WORKSPACE, - workspaceGit: true, - methodResponses: { - "node.list": { nodes: [] }, - "environments.list": { environments: [], profiles: [] }, - }, - }); - - try { - await page.goto(`${suite.server.baseUrl}new`); - await gateway.waitForRequest("node.list"); - const trigger = page.locator("#new-session-place-trigger"); - await pollLocatorText(trigger.locator(".new-session-page__trigger-label")).toBe("openclaw"); - await trigger.click(); - const place = page.locator("wa-popover.new-session-page__place-popover"); - expect(await place.getByText("Places", { exact: true }).count()).toBe(0); - await place.getByText("Runs on Gateway · local", { exact: true }).waitFor(); - } finally { - await context.close(); - } - }); - it("uses advertised system info for Gateway place labels", async () => { const context = await suite.browser.newContext({ locale: "en-US", serviceWorkers: "block" }); const page = await context.newPage(); @@ -925,7 +899,7 @@ suite.define(() => { const placeLabel = placeTrigger.locator(".new-session-page__trigger-label"); const browserEntries = page.locator(".new-session-page__browser-list"); - // Pick the node from Places. + // Pick the node from Your devices. await placeTrigger.click(); await placeSelect.getByRole("button", { name: "MacBook" }).click(); await pollLocatorText(placeLabel).toBe("Agent workspace · MacBook"); @@ -950,9 +924,9 @@ suite.define(() => { placeSelect.evaluate((element) => (element as HTMLElement & { open: boolean }).open), ) .toBe(true); - await placeSelect.getByText("Places", { exact: true }).waitFor(); + await placeSelect.getByText("Your devices", { exact: true }).waitFor(); - // Destination selection stays in Places; browsing is fixed to the current target. + // Destination selection stays in the destination sections; browsing is fixed to the target. await placeSelect.getByRole("button", { name: "Gateway · local" }).click(); await pollLocatorText(placeLabel).toBe("openclaw · Gateway · local"); await placeTrigger.click(); diff --git a/ui/src/e2e/new-session-page.projects-places.e2e.test.ts b/ui/src/e2e/new-session-page.projects-places.e2e.test.ts index f98b5d2c5759..aa189ff9b0e2 100644 --- a/ui/src/e2e/new-session-page.projects-places.e2e.test.ts +++ b/ui/src/e2e/new-session-page.projects-places.e2e.test.ts @@ -249,7 +249,7 @@ suite.define(() => { await gateway.waitForRequest("environments.list"); await page.locator("#new-session-place-trigger").click(); const place = page.locator("wa-popover.new-session-page__place-popover"); - await place.getByText("Devices", { exact: true }).waitFor(); + await place.getByText("Your devices", { exact: true }).waitFor(); await place.getByRole("button", { name: "Fallback device" }).waitFor(); await captureUiProof(page, "04-catalog-unavailable-device-fallback.png"); } finally { @@ -368,7 +368,7 @@ suite.define(() => { .locator(".new-session-page__menu-title") .getByText("This gateway", { exact: true }) .waitFor(); - await place.getByText("Devices", { exact: true }).waitFor(); + await place.getByText("Your devices", { exact: true }).waitFor(); const first = page.locator('[data-value="node:11111111aaaaaaaa"]'); const second = page.locator('[data-value="node:22222222bbbbbbbb"]'); const phone = page.locator('[data-value="node:33333333cccccccc"]'); @@ -713,7 +713,7 @@ suite.define(() => { .locator(".new-session-page__menu-title") .getByText("This gateway", { exact: true }) .waitFor(); - await placeSelect.getByText("Devices", { exact: true }).waitFor(); + await placeSelect.getByText("Your devices", { exact: true }).waitFor(); await placeSelect.getByRole("button", { name: "MacBook" }).click(); await pollLocatorText(placeLabel).toBe("Agent workspace · MacBook"); // Node sessions cannot use managed worktrees, so the menu drops the item. diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index afd1971312fc..0f0d39acff90 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -717,7 +717,7 @@ export const en: TranslationMap = { cloudSyncsFolder: "Syncs {folder} to the cloud worker", folder: "Folder", folderPlaceholder: "Agent workspace", - places: "Places", + yourDevices: "Your devices", projects: "Projects", projectsAdminHint: "Admins can register projects from Browse folders", projectSearchPlaceholder: "Search projects or paste a Git URL", diff --git a/ui/src/pages/new-session/discovery.test.ts b/ui/src/pages/new-session/discovery.test.ts index 91a642c43a1e..83843388da03 100644 --- a/ui/src/pages/new-session/discovery.test.ts +++ b/ui/src/pages/new-session/discovery.test.ts @@ -38,17 +38,20 @@ describe("readDraftCloudProfiles", () => { { id: " zeta ", providerId: " static-ssh ", + trust: "disposable", settings: { token: "hidden" }, }, - { id: "aws", providerId: "crabbox" }, + { id: "aws", providerId: "crabbox", trust: "persistent" }, { id: "legacy", providerId: "static-ssh" }, + { id: "invalid-trust", providerId: "crabbox", trust: "temporary" }, { id: "", providerId: "crabbox" }, { id: "missing-provider" }, ]), ).toEqual([ - { id: "aws", providerId: "crabbox" }, - { id: "legacy", providerId: "static-ssh" }, - { id: "zeta", providerId: "static-ssh" }, + { id: "aws", providerId: "crabbox", trust: "persistent" }, + { id: "invalid-trust", providerId: "crabbox", trust: undefined }, + { id: "legacy", providerId: "static-ssh", trust: undefined }, + { id: "zeta", providerId: "static-ssh", trust: "disposable" }, ]); }); }); diff --git a/ui/src/pages/new-session/discovery.ts b/ui/src/pages/new-session/discovery.ts index 153a74549d3b..d2cd5255044a 100644 --- a/ui/src/pages/new-session/discovery.ts +++ b/ui/src/pages/new-session/discovery.ts @@ -30,6 +30,7 @@ export type DraftNode = { export type DraftCloudProfile = { id: string; providerId: string; + trust?: "persistent" | "disposable"; }; export type DraftEnvironment = { @@ -92,16 +93,17 @@ export function readDraftCloudProfiles(value: unknown): DraftCloudProfile[] { if (!raw || typeof raw !== "object") { return []; } - const profile = raw as { - id?: unknown; - providerId?: unknown; - }; + const profile = raw as { id?: unknown; providerId?: unknown; trust?: unknown }; const id = normalizeOptionalString(profile.id); const providerId = normalizeOptionalString(profile.providerId); if (!id || !providerId) { return []; } - return [{ id, providerId }]; + const trust: DraftCloudProfile["trust"] = + profile.trust === "persistent" || profile.trust === "disposable" + ? profile.trust + : undefined; + return [{ id, providerId, trust }]; }) .toSorted((left, right) => left.id.localeCompare(right.id)); } diff --git a/ui/src/pages/new-session/draft-gateway-state.ts b/ui/src/pages/new-session/draft-gateway-state.ts index f10b124d9e51..8672f019e43c 100644 --- a/ui/src/pages/new-session/draft-gateway-state.ts +++ b/ui/src/pages/new-session/draft-gateway-state.ts @@ -188,6 +188,10 @@ export class DraftGatewayState { return this.preferenceModeValue === "loading"; } + refreshCloudProfiles() { + return this.cloudProfileTask.run(); + } + synchronize(gateway: ApplicationContext["gateway"]) { const snapshot = gateway.snapshot; const connected = snapshot.phase === "connected"; diff --git a/ui/src/pages/new-session/draft-place-state.ts b/ui/src/pages/new-session/draft-place-state.ts index d198189e1a97..28b0ea48379d 100644 --- a/ui/src/pages/new-session/draft-place-state.ts +++ b/ui/src/pages/new-session/draft-place-state.ts @@ -151,6 +151,10 @@ export class DraftPlaceState { ); } + refreshNodes() { + return this.loadNodes({ quiet: true }); + } + isAdmin(): boolean { return hasOperatorAdminAccess(this.read().context?.gateway.snapshot.hello?.auth ?? null); } @@ -592,9 +596,11 @@ export class DraftPlaceState { }); } - private async loadNodes() { + private async loadNodes(options: { quiet?: boolean } = {}) { const requestId = ++this.nodesRequestToken; - this.nodesHydrated = false; + if (!options.quiet) { + this.nodesHydrated = false; + } const snapshot = this.read().context?.gateway.snapshot; const client = snapshot?.client; if (snapshot?.phase !== "connected" || !client || !this.isAdmin()) { @@ -626,7 +632,7 @@ export class DraftPlaceState { } this.callbacks.requestUpdate(); } catch { - if (requestId === this.nodesRequestToken) { + if (requestId === this.nodesRequestToken && !options.quiet) { this.nodesValue = []; this.nodesHydrated = true; this.callbacks.requestUpdate(); diff --git a/ui/src/pages/new-session/new-session-page.ts b/ui/src/pages/new-session/new-session-page.ts index 7cd4848659f8..93bbe0638769 100644 --- a/ui/src/pages/new-session/new-session-page.ts +++ b/ui/src/pages/new-session/new-session-page.ts @@ -1,6 +1,7 @@ import { consume } from "@lit/context"; import { html, nothing, type ReactiveController, type ReactiveControllerHost } from "lit"; import { property } from "lit/decorators.js"; +import type { PresenceEntry } from "../../api/types.ts"; import { selectApplicationSession } from "../../app/agent-selection.ts"; import { applicationContext, type ApplicationContext } from "../../app/context.ts"; import { beginNativeWindowDragFromTopInset } from "../../app/native-window-drag.ts"; @@ -29,6 +30,24 @@ import type { NewSessionRouteData } from "./location.ts"; import { renderPlaceSelect } from "./place-picker.ts"; import { renderAgentSelect } from "./target-controls.ts"; +function readPresence(value: unknown): PresenceEntry[] | null { + const presence = + value && typeof value === "object" ? (value as { presence?: unknown }).presence : null; + return Array.isArray(presence) ? (presence as PresenceEntry[]) : null; +} + +function presenceConnectivitySignature(entries: PresenceEntry[]): string { + const states = new Map(); + for (const entry of entries) { + const id = (entry.deviceId ?? entry.instanceId)?.trim().toLowerCase(); + if (!id || entry.mode?.trim().toLowerCase() === "gateway") { + continue; + } + states.set(id, entry.reason?.trim().toLowerCase() === "disconnect" ? "offline" : "connected"); + } + return JSON.stringify([...states].toSorted(([left], [right]) => left.localeCompare(right))); +} + function controllerHost(element: OpenClawLightDomElement): ReactiveControllerHost { return { addController: (controller: ReactiveController) => element.addController(controller), @@ -49,6 +68,7 @@ class NewSessionPage extends OpenClawLightDomElement { private openedFor: string | null = null; private openedAgentId = ""; private messageOwnerKey = ""; + private presenceSignature = ""; private readonly gateway: DraftGatewayState; private readonly browser: DraftPlaceBrowser; private readonly place: DraftPlaceState; @@ -147,6 +167,41 @@ class NewSessionPage extends OpenClawLightDomElement { (gateway, notify) => gateway.subscribe(notify), (gateway) => this.gateway.synchronize(gateway), ) + .effect( + () => this.context?.gateway, + (gateway) => { + this.presenceSignature = presenceConnectivitySignature( + readPresence(gateway.snapshot.hello?.snapshot) ?? [], + ); + return gateway.subscribeEvents((event) => { + if (this.context?.gateway !== gateway) { + return; + } + if (event.event === "config.changed") { + void this.gateway.refreshCloudProfiles(); + return; + } + if ( + event.event === "node.pair.requested" || + event.event === "node.pair.resolved" || + event.event === "device.pair.requested" || + event.event === "device.pair.resolved" + ) { + void this.place.refreshNodes(); + return; + } + const presence = event.event === "presence" ? readPresence(event.payload) : null; + if (!presence) { + return; + } + const signature = presenceConnectivitySignature(presence); + if (signature !== this.presenceSignature) { + this.presenceSignature = signature; + void this.place.refreshNodes(); + } + }); + }, + ) .watch( () => this.context?.agents, (agents, notify) => agents.subscribe(notify), diff --git a/ui/src/pages/new-session/place-picker.test.ts b/ui/src/pages/new-session/place-picker.test.ts index c09f2546fd12..a4e10cfd11fc 100644 --- a/ui/src/pages/new-session/place-picker.test.ts +++ b/ui/src/pages/new-session/place-picker.test.ts @@ -88,6 +88,41 @@ describe("project picker", () => { expect(projectCloneInput(value) !== null).toBe(expected); }); + it("groups gateway, device, and cloud destinations without status copy", () => { + const container = document.createElement("div"); + render( + renderPlaceSelect( + placeParams({ + showDestinations: true, + worktreeAvailable: true, + execNodes: [ + { + nodeId: "macbook", + displayName: "MacBook", + connected: true, + canExec: true, + canBrowse: true, + }, + ], + cloudProfiles: [{ id: "aws", providerId: "crabbox", trust: "disposable" }], + }), + ), + container, + ); + + const destinationHeadings = [ + ...container.querySelectorAll(".new-session-page__menu-title"), + ] + .map((element) => element.textContent?.trim()) + .filter((label) => ["This gateway", "Your devices", "Cloud", "Places"].includes(label ?? "")); + expect(destinationHeadings).toEqual(["This gateway", "Your devices", "Cloud"]); + expect(container.querySelector('[data-value="gateway"]')).not.toBeNull(); + expect(container.querySelector('[data-value="node:macbook"]')).not.toBeNull(); + expect(container.querySelector('[data-value="cloud:aws"]')).not.toBeNull(); + expect(container.textContent).not.toContain("persistent"); + expect(container.textContent).not.toContain("disposable"); + }); + it("renders local matches before remote clone results and explains missing credentials", () => { const onCloneProject = vi.fn(); const container = document.createElement("div"); @@ -258,7 +293,7 @@ describe("Where picker", () => { const titles = [...container.querySelectorAll(".new-session-page__menu-title")].map((element) => element.textContent?.trim(), ); - expect(titles).toEqual(["Folder", "Projects", "Places", "This gateway", "Devices", "Cloud"]); + expect(titles).toEqual(["Folder", "Projects", "This gateway", "Your devices", "Cloud"]); expect(container.querySelector('[data-value="node:macbook"]')).not.toBeNull(); for (const nodeId of [ "worker", diff --git a/ui/src/pages/new-session/place-picker.ts b/ui/src/pages/new-session/place-picker.ts index 488048ec65b4..631d719c89e1 100644 --- a/ui/src/pages/new-session/place-picker.ts +++ b/ui/src/pages/new-session/place-picker.ts @@ -533,7 +533,6 @@ export function renderPlaceSelect(params: { ${params.showDestinations ? html` -
${t("newSession.places")}
${t("newSession.thisGateway")}
${renderSessionMenuItem( { @@ -547,7 +546,9 @@ export function renderPlaceSelect(params: { )} ${deviceNodes.length > 0 ? html` -
${t("tabs.devices")}
+
+ ${t("newSession.yourDevices")} +
${deviceNodes.map((node, index) => renderSessionMenuItem( { @@ -567,41 +568,42 @@ export function renderPlaceSelect(params: { ` : nothing} ${cloudProfiles.length > 0 || (params.cloudProfileId && !activeProfile) - ? html`
- ${t("newSession.cloud")} -
` - : nothing} - ${renderCloudProfileMenuItems({ - profiles: cloudProfiles, - selectedId: params.cloudProfileId, - submitting: params.submitting, - icon: icons.server, - disabled: !params.worktreeAvailable || Boolean(params.cloudDisabledReason), - disabledReason: params.cloudDisabledReason, - onSelect: params.onSelectCloudProfile, - })} - ${params.cloudProfileId && !activeProfile - ? renderSessionMenuItem( - { - value: `cloud:${params.cloudProfileId}`, - label: t("newSession.cloudWorker", { - profile: params.cloudProfileId, - }), + ? html` +
${t("newSession.cloud")}
+ ${renderCloudProfileMenuItems({ + profiles: cloudProfiles, + selectedId: params.cloudProfileId, + submitting: params.submitting, icon: icons.server, - checked: true, - disabled: true, - title: t("newSession.catalogUnavailable"), - onSelect: () => undefined, - }, - params.submitting, - ) - : nothing} - ${params.cloudProfileId && params.syncFolder - ? html`
- ${t("newSession.cloudSyncsFolder", { - folder: folderDisplayName(params.syncFolder), + disabled: + !params.worktreeAvailable || Boolean(params.cloudDisabledReason), + disabledReason: params.cloudDisabledReason, + onSelect: params.onSelectCloudProfile, })} -
` + ${params.cloudProfileId && !activeProfile + ? renderSessionMenuItem( + { + value: `cloud:${params.cloudProfileId}`, + label: t("newSession.cloudWorker", { + profile: params.cloudProfileId, + }), + icon: icons.server, + checked: true, + disabled: true, + title: t("newSession.catalogUnavailable"), + onSelect: () => undefined, + }, + params.submitting, + ) + : nothing} + ${params.cloudProfileId && params.syncFolder + ? html`
+ ${t("newSession.cloudSyncsFolder", { + folder: folderDisplayName(params.syncFolder), + })} +
` + : nothing} + ` : nothing} ` : nothing}