Skip to content

Commit 64c8281

Browse files
authored
feat(dashboard): pinned MCP apps — board rendering, lease re-mint, durable tool grants (#111524)
* feat(boards): mint pinned MCP app views * feat(ui): render pinned MCP apps on dashboards * fix(dashboard): reconcile MCP app rebase * fix(dashboard): align generated CI contracts * test(gateway): record board.widget.appView release train * fix(dashboard): revalidate pinned MCP app views
1 parent ddf57c0 commit 64c8281

49 files changed

Lines changed: 2498 additions & 83 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212
- **Control UI user profiles:** let trusted-proxy users manage their own display name and avatar, resolve attributed chat and presence identities through uploaded avatars or a private cached Gravatar proxy, and keep other users' profiles admin-only.
1313
- **Trusted-proxy browser pairing:** optionally auto-approve new Control UI and WebChat devices from allowlisted proxy identities with non-admin scope caps, while keeping existing-device upgrades manual.
1414
- **Channel plugin ingress monitors:** add a shared plugin SDK monitor for durable admission, polling, pruning, claim identity validation, adoption handoff, and shutdown, and migrate IRC, Synology Chat, and Google Chat to the shared lifecycle.
15+
- **Dashboard MCP apps:** pin originating-session MCP app views as living dashboard widgets, renew their sandboxed view leases, and keep tool interactivity behind revision-bound grants with graceful stale-state recovery.
1516
- **External gateway supervision:** add `OPENCLAW_SUPERVISOR_MODE=external` for lifecycle owners such as OCM, preserving verified restart and deferral behavior without exposing native service authority, blocking native service mutation and self-update, and providing a versioned atomic restart-handoff consume contract. Thanks @shakkernerd.
1617
- **ClickClack guided setup:** configure ClickClack from `openclaw onboard` or `openclaw channels add clickclack` with URL, token, and workspace prompts, default-account env fallback, nonfatal live connection validation, and gateway-aware next steps that connect automatically when OpenClaw is already running. Thanks @shakkernerd.
1718
- **ClickClack command menus:** publish each bot's native OpenClaw commands to ClickClack composer autocomplete at gateway startup, with per-account opt-out and nonfatal compatibility handling for older tokens and servers. Thanks @shakkernerd.

apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ enum class GatewayMethod(
235235
BoardUpdate("board.update"),
236236
BoardWidgetPut("board.widget.put"),
237237
BoardWidgetGrant("board.widget.grant"),
238+
BoardWidgetAppView("board.widget.appView"),
238239
BoardEvent("board.event"),
239240
AuditList("audit.list"),
240241
AuditActivityList("audit.activity.list"),

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 180 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ public struct BoardWidget: Codable, Sendable {
244244
public let position: Int
245245
public let grantstate: AnyCodable
246246
public let revision: Int
247+
public let instanceid: String?
247248
public let declaredsummary: [String]?
248249
public let frameurl: String?
249250

@@ -257,6 +258,7 @@ public struct BoardWidget: Codable, Sendable {
257258
position: Int,
258259
grantstate: AnyCodable,
259260
revision: Int,
261+
instanceid: String? = nil,
260262
declaredsummary: [String]? = nil,
261263
frameurl: String? = nil)
262264
{
@@ -269,6 +271,7 @@ public struct BoardWidget: Codable, Sendable {
269271
self.position = position
270272
self.grantstate = grantstate
271273
self.revision = revision
274+
self.instanceid = instanceid
272275
self.declaredsummary = declaredsummary
273276
self.frameurl = frameurl
274277
}
@@ -283,6 +286,7 @@ public struct BoardWidget: Codable, Sendable {
283286
case position
284287
case grantstate = "grantState"
285288
case revision
289+
case instanceid = "instanceId"
286290
case declaredsummary = "declaredSummary"
287291
case frameurl = "frameUrl"
288292
}
@@ -510,6 +514,40 @@ public struct BoardMcpAppDescriptor: Codable, Sendable {
510514
}
511515
}
512516

517+
public struct BoardMcpAppPinDescriptor: Codable, Sendable {
518+
public let viewid: String
519+
public let servername: String
520+
public let toolname: String
521+
public let uiresourceuri: String
522+
public let originsessionkey: String
523+
public let toolcallid: String
524+
525+
public init(
526+
viewid: String,
527+
servername: String,
528+
toolname: String,
529+
uiresourceuri: String,
530+
originsessionkey: String,
531+
toolcallid: String)
532+
{
533+
self.viewid = viewid
534+
self.servername = servername
535+
self.toolname = toolname
536+
self.uiresourceuri = uiresourceuri
537+
self.originsessionkey = originsessionkey
538+
self.toolcallid = toolcallid
539+
}
540+
541+
private enum CodingKeys: String, CodingKey {
542+
case viewid = "viewId"
543+
case servername = "serverName"
544+
case toolname = "toolName"
545+
case uiresourceuri = "uiResourceUri"
546+
case originsessionkey = "originSessionKey"
547+
case toolcallid = "toolCallId"
548+
}
549+
}
550+
513551
public struct BoardWidgetHtmlContent: Codable, Sendable {
514552
public let kind: String
515553
public let html: String
@@ -546,6 +584,24 @@ public struct BoardWidgetMcpAppContent: Codable, Sendable {
546584
}
547585
}
548586

587+
public struct BoardWidgetMcpAppPutContent: Codable, Sendable {
588+
public let kind: String
589+
public let descriptor: BoardMcpAppPinDescriptor
590+
591+
public init(
592+
kind: String,
593+
descriptor: BoardMcpAppPinDescriptor)
594+
{
595+
self.kind = kind
596+
self.descriptor = descriptor
597+
}
598+
599+
private enum CodingKeys: String, CodingKey {
600+
case kind
601+
case descriptor
602+
}
603+
}
604+
549605
public struct BoardCanvasDocumentSource: Codable, Sendable {
550606
public let kind: String
551607
public let docid: String
@@ -635,24 +691,72 @@ public struct BoardWidgetGrantParams: Codable, Sendable {
635691
public let name: String
636692
public let decision: AnyCodable
637693
public let revision: Int
694+
public let instanceid: String?
638695

639696
public init(
640697
sessionkey: String,
641698
name: String,
642699
decision: AnyCodable,
643-
revision: Int)
700+
revision: Int,
701+
instanceid: String? = nil)
644702
{
645703
self.sessionkey = sessionkey
646704
self.name = name
647705
self.decision = decision
648706
self.revision = revision
707+
self.instanceid = instanceid
649708
}
650709

651710
private enum CodingKeys: String, CodingKey {
652711
case sessionkey = "sessionKey"
653712
case name
654713
case decision
655714
case revision
715+
case instanceid = "instanceId"
716+
}
717+
}
718+
719+
public struct BoardWidgetAppViewParams: Codable, Sendable {
720+
public let sessionkey: String
721+
public let name: String
722+
public let revision: Int
723+
public let instanceid: String?
724+
725+
public init(
726+
sessionkey: String,
727+
name: String,
728+
revision: Int,
729+
instanceid: String? = nil)
730+
{
731+
self.sessionkey = sessionkey
732+
self.name = name
733+
self.revision = revision
734+
self.instanceid = instanceid
735+
}
736+
737+
private enum CodingKeys: String, CodingKey {
738+
case sessionkey = "sessionKey"
739+
case name
740+
case revision
741+
case instanceid = "instanceId"
742+
}
743+
}
744+
745+
public struct BoardWidgetAppViewResult: Codable, Sendable {
746+
public let viewid: String
747+
public let expiresatms: Int
748+
749+
public init(
750+
viewid: String,
751+
expiresatms: Int)
752+
{
753+
self.viewid = viewid
754+
self.expiresatms = expiresatms
755+
}
756+
757+
private enum CodingKeys: String, CodingKey {
758+
case viewid = "viewId"
759+
case expiresatms = "expiresAtMs"
656760
}
657761
}
658762

@@ -1150,7 +1254,7 @@ public struct ErrorShape: Codable, Sendable {
11501254
}
11511255
}
11521256

1153-
public struct GatewayErrorDetails: Codable, Sendable {
1257+
public struct MissingScopeErrorDetails: Codable, Sendable {
11541258
public let code: String
11551259
public let missingscope: String
11561260
public let requiredscopes: [String]
@@ -1172,6 +1276,20 @@ public struct GatewayErrorDetails: Codable, Sendable {
11721276
}
11731277
}
11741278

1279+
public struct McpAppViewExpiredErrorDetails: Codable, Sendable {
1280+
public let code: String
1281+
1282+
public init(
1283+
code: String)
1284+
{
1285+
self.code = code
1286+
}
1287+
1288+
private enum CodingKeys: String, CodingKey {
1289+
case code
1290+
}
1291+
}
1292+
11751293
public struct GatewaySuspendTaskBlocker: Codable, Sendable {
11761294
public let taskid: String
11771295
public let status: String
@@ -15578,7 +15696,7 @@ public enum BoardWidgetContent: Codable, Sendable {
1557815696

1557915697
public enum BoardWidgetPutContent: Codable, Sendable {
1558015698
case html(BoardWidgetHtmlContent)
15581-
case mcpApp(BoardWidgetMcpAppContent)
15699+
case mcpApp(BoardWidgetMcpAppPutContent)
1558215700
case canvasDoc(BoardCanvasDocumentSource)
1558315701

1558415702
private enum CodingKeys: String, CodingKey {
@@ -15590,7 +15708,7 @@ public enum BoardWidgetPutContent: Codable, Sendable {
1559015708
let discriminator = try container.decode(String.self, forKey: .discriminator)
1559115709
switch discriminator {
1559215710
case "html": self = try .html(BoardWidgetHtmlContent(from: decoder))
15593-
case "mcp-app": self = try .mcpApp(BoardWidgetMcpAppContent(from: decoder))
15711+
case "mcp-app": self = try .mcpApp(BoardWidgetMcpAppPutContent(from: decoder))
1559415712
case "canvas-doc": self = try .canvasDoc(BoardCanvasDocumentSource(from: decoder))
1559515713
default:
1559615714
throw DecodingError.dataCorruptedError(
@@ -15641,6 +15759,64 @@ public enum BoardCommand: Codable, Sendable {
1564115759
}
1564215760
}
1564315761

15762+
public enum GatewayErrorDetails: Codable, Sendable {
15763+
case missingScope(MissingScopeErrorDetails)
15764+
case mcpAppViewExpired(McpAppViewExpiredErrorDetails)
15765+
15766+
public init(code: String, missingscope: String, requiredscopes: [String]) {
15767+
self = .missingScope(
15768+
MissingScopeErrorDetails(
15769+
code: code,
15770+
missingscope: missingscope,
15771+
requiredscopes: requiredscopes
15772+
)
15773+
)
15774+
}
15775+
15776+
public var code: String {
15777+
switch self {
15778+
case .missingScope(let value): value.code
15779+
case .mcpAppViewExpired(let value): value.code
15780+
}
15781+
}
15782+
15783+
public var missingscope: String {
15784+
if case .missingScope(let value) = self { return value.missingscope }
15785+
return ""
15786+
}
15787+
15788+
public var requiredscopes: [String] {
15789+
if case .missingScope(let value) = self { return value.requiredscopes }
15790+
return []
15791+
}
15792+
15793+
private enum CodingKeys: String, CodingKey {
15794+
case discriminator = "code"
15795+
}
15796+
15797+
public init(from decoder: Decoder) throws {
15798+
let container = try decoder.container(keyedBy: CodingKeys.self)
15799+
let discriminator = try container.decode(String.self, forKey: .discriminator)
15800+
switch discriminator {
15801+
case "MISSING_SCOPE": self = try .missingScope(MissingScopeErrorDetails(from: decoder))
15802+
case "MCP_APP_VIEW_EXPIRED": self = try .mcpAppViewExpired(McpAppViewExpiredErrorDetails(from: decoder))
15803+
default:
15804+
throw DecodingError.dataCorruptedError(
15805+
forKey: .discriminator,
15806+
in: container,
15807+
debugDescription: "Unknown GatewayErrorDetails discriminator value"
15808+
)
15809+
}
15810+
}
15811+
15812+
public func encode(to encoder: Encoder) throws {
15813+
switch self {
15814+
case .missingScope(let value): try value.encode(to: encoder)
15815+
case .mcpAppViewExpired(let value): try value.encode(to: encoder)
15816+
}
15817+
}
15818+
}
15819+
1564415820
public enum GatewaySuspendPrepareResult: Codable, Sendable {
1564515821
case busy(GatewaySuspendPrepareBusyResult)
1564615822
case ready(GatewaySuspendPrepareReadyResult)

packages/gateway-protocol/src/gateway-error-details.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
2222
/** Stable discriminants for structured method-level authorization failures. */
2323
export const GatewayErrorDetailCodes = {
2424
MISSING_SCOPE: "MISSING_SCOPE",
25+
MCP_APP_VIEW_EXPIRED: "MCP_APP_VIEW_EXPIRED",
2526
} as const;
2627

2728
/** Missing operator-scope details shared by WebSocket and HTTP responses. */
@@ -31,8 +32,12 @@ export type MissingScopeErrorDetails = {
3132
requiredScopes: string[];
3233
};
3334

35+
export type McpAppViewExpiredErrorDetails = {
36+
code: typeof GatewayErrorDetailCodes.MCP_APP_VIEW_EXPIRED;
37+
};
38+
3439
/** Structured details emitted by method-level authorization failures. */
35-
export type GatewayErrorDetails = MissingScopeErrorDetails;
40+
export type GatewayErrorDetails = MissingScopeErrorDetails | McpAppViewExpiredErrorDetails;
3641

3742
type GatewayErrorLike = {
3843
code?: unknown;
@@ -69,6 +74,11 @@ export function readMissingScopeErrorDetails(details: unknown): MissingScopeErro
6974
};
7075
}
7176

77+
export function isMcpAppViewExpiredError(error: unknown): boolean {
78+
const record = asRecord(error);
79+
return asRecord(record?.details)?.code === GatewayErrorDetailCodes.MCP_APP_VIEW_EXPIRED;
80+
}
81+
7282
/**
7383
* Reads a method-level missing-scope failure, preferring structured details.
7484
* The message fallback keeps clients compatible with gateways predating structured details.

packages/gateway-protocol/src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export * from "./clawhub-trust-error-details.js";
22
export * from "./system-agent-error-details.js";
3-
export { readMissingScopeError, readMissingScopeErrorDetails } from "./gateway-error-details.js";
3+
export {
4+
isMcpAppViewExpiredError,
5+
readMissingScopeError,
6+
readMissingScopeErrorDetails,
7+
} from "./gateway-error-details.js";
48
export * from "./session-icon.js";
59
export * from "./terminal-validators.js";
610
export {
@@ -15,7 +19,11 @@ export type { ProtocolValidator } from "./protocol-validator.js";
1519
export * from "./schema/worker-inference.js";
1620
export * from "./schema/skill-history.js";
1721
export * from "./schema/ui-command.js";
18-
export type { GatewayErrorDetails, MissingScopeErrorDetails } from "./schema/error-codes.js";
22+
export type {
23+
GatewayErrorDetails,
24+
McpAppViewExpiredErrorDetails,
25+
MissingScopeErrorDetails,
26+
} from "./schema/error-codes.js";
1927
export * from "./schema/board.js";
2028
export * from "./migration-api.js";
2129
export type * from "./public-session-catalog.js";
@@ -24,6 +32,7 @@ import {
2432
BoardGetParamsSchema,
2533
BoardUpdateParamsSchema,
2634
BoardWidgetContentSchema,
35+
BoardWidgetAppViewParamsSchema,
2736
BoardWidgetGrantParamsSchema,
2837
BoardWidgetPutParamsSchema,
2938
AgentEventSchema,
@@ -671,6 +680,7 @@ export const validateWorktreesListParams = lazyCompile(WorktreesListParamsSchema
671680
export const validateBoardGetParams = lazyCompile(BoardGetParamsSchema);
672681
export const validateBoardUpdateParams = lazyCompile(BoardUpdateParamsSchema);
673682
export const validateBoardWidgetContent = lazyCompile(BoardWidgetContentSchema);
683+
export const validateBoardWidgetAppViewParams = lazyCompile(BoardWidgetAppViewParamsSchema);
674684
export const validateBoardWidgetPutParams = lazyCompile(BoardWidgetPutParamsSchema);
675685
export const validateBoardWidgetGrantParams = lazyCompile(BoardWidgetGrantParamsSchema);
676686
export const validateBoardEventParams = lazyCompile(BoardEventParamsSchema);

0 commit comments

Comments
 (0)