Skip to content

Commit 93ba8c4

Browse files
committed
feat(gateway): propagate approvals to ancestor sessions with replay
Squash-rebased #103921 segment onto the native-clients tip on current main. Session-scoped approval events publish sanitized pending/terminal transitions to opted-in session audiences, with authoritative pending replay on stream subscribe and durable-expiry reconciliation through the owning manager. SessionApprovalEvent/Replay wire types export from the approvals owner module; Swift models regenerated. (cherry picked from commit 2d1dcf9747044710111d0c730fc46ba6013a165c) (cherry picked from commit ccf88efd56b513d07599ffcee997bf0cddaf9adc) (cherry picked from commit 8eb33f59f00ec9ee4625259b76bd7ae60d2cd480) (cherry picked from commit c297cbc93c8401e9c79c20f242245a194f5dd236) (cherry picked from commit 93d68f28601ff37d863da4db009504993a22533a) (cherry picked from commit d285ccd8b2f212bb57e814107e7709c896e96b7d) (cherry picked from commit 5c536982231402b1b58d1683794ea9948b32d077) (cherry picked from commit 1646e5b6dc4dd20de54e10f110a5909be18d7d50) (cherry picked from commit 35cf1b705b247fb282f4d56ffe74bd2fb238221f) (cherry picked from commit 4f8ef36) (cherry picked from commit 74fbdcc99d5fff062d67760a51dba4ee9e84479b) (cherry picked from commit 222b285502681d2be14b1819798e9ee5555f431e) (cherry picked from commit b9e744951f676b600e7c5322bfd04b0e99797c07) (cherry picked from commit 9e904ef) (cherry picked from commit 678f238) (cherry picked from commit 72842e5) (cherry picked from commit be74c25)
1 parent 175b049 commit 93ba8c4

32 files changed

Lines changed: 2420 additions & 114 deletions

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

Lines changed: 162 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,26 @@ public enum ApprovalAllowDecision: String, Codable, Sendable {
107107
case allowAlways = "allow-always"
108108
}
109109

110+
public enum ApprovalAllowedReason: String, Codable, Sendable {
111+
case user = "user"
112+
}
113+
114+
public enum ApprovalDeniedReason: String, Codable, Sendable {
115+
case user = "user"
116+
case malformedVerdict = "malformed-verdict"
117+
case noRoute = "no-route"
118+
case storageCorrupt = "storage-corrupt"
119+
}
120+
121+
public enum ApprovalExpiredReason: String, Codable, Sendable {
122+
case timeout = "timeout"
123+
}
124+
125+
public enum ApprovalCancelledReason: String, Codable, Sendable {
126+
case runAborted = "run-aborted"
127+
case gatewayRestart = "gateway-restart"
128+
}
129+
110130
public enum PluginApprovalSeverity: String, Codable, Sendable {
111131
case info = "info"
112132
case warning = "warning"
@@ -3892,18 +3912,22 @@ public struct SessionsSendParams: Codable, Sendable {
38923912
public struct SessionsMessagesSubscribeParams: Codable, Sendable {
38933913
public let key: String
38943914
public let agentid: String?
3915+
public let includeapprovals: Bool?
38953916

38963917
public init(
38973918
key: String,
3898-
agentid: String? = nil)
3919+
agentid: String? = nil,
3920+
includeapprovals: Bool? = nil)
38993921
{
39003922
self.key = key
39013923
self.agentid = agentid
3924+
self.includeapprovals = includeapprovals
39023925
}
39033926

39043927
private enum CodingKeys: String, CodingKey {
39053928
case key
39063929
case agentid = "agentId"
3930+
case includeapprovals = "includeApprovals"
39073931
}
39083932
}
39093933

@@ -9992,9 +10016,9 @@ public struct AllowedApprovalSnapshot: Codable, Sendable {
999210016
public let expiresatms: Int
999310017
public let presentation: ApprovalPresentation
999410018
public let resolvedatms: Int
9995-
public let reason: ApprovalTerminalReason
999610019
public let status: String
999710020
public let decision: ApprovalAllowDecision
10021+
public let reason: ApprovalAllowedReason
999810022

999910023
public init(
1000010024
id: String,
@@ -10003,19 +10027,19 @@ public struct AllowedApprovalSnapshot: Codable, Sendable {
1000310027
expiresatms: Int,
1000410028
presentation: ApprovalPresentation,
1000510029
resolvedatms: Int,
10006-
reason: ApprovalTerminalReason,
1000710030
status: String,
10008-
decision: ApprovalAllowDecision)
10031+
decision: ApprovalAllowDecision,
10032+
reason: ApprovalAllowedReason)
1000910033
{
1001010034
self.id = id
1001110035
self.urlpath = urlpath
1001210036
self.createdatms = createdatms
1001310037
self.expiresatms = expiresatms
1001410038
self.presentation = presentation
1001510039
self.resolvedatms = resolvedatms
10016-
self.reason = reason
1001710040
self.status = status
1001810041
self.decision = decision
10042+
self.reason = reason
1001910043
}
1002010044

1002110045
private enum CodingKeys: String, CodingKey {
@@ -10025,9 +10049,9 @@ public struct AllowedApprovalSnapshot: Codable, Sendable {
1002510049
case expiresatms = "expiresAtMs"
1002610050
case presentation
1002710051
case resolvedatms = "resolvedAtMs"
10028-
case reason
1002910052
case status
1003010053
case decision
10054+
case reason
1003110055
}
1003210056
}
1003310057

@@ -10038,9 +10062,9 @@ public struct DeniedApprovalSnapshot: Codable, Sendable {
1003810062
public let expiresatms: Int
1003910063
public let presentation: ApprovalPresentation
1004010064
public let resolvedatms: Int
10041-
public let reason: ApprovalTerminalReason
1004210065
public let status: String
1004310066
public let decision: String
10067+
public let reason: ApprovalDeniedReason
1004410068

1004510069
public init(
1004610070
id: String,
@@ -10049,19 +10073,19 @@ public struct DeniedApprovalSnapshot: Codable, Sendable {
1004910073
expiresatms: Int,
1005010074
presentation: ApprovalPresentation,
1005110075
resolvedatms: Int,
10052-
reason: ApprovalTerminalReason,
1005310076
status: String,
10054-
decision: String)
10077+
decision: String,
10078+
reason: ApprovalDeniedReason)
1005510079
{
1005610080
self.id = id
1005710081
self.urlpath = urlpath
1005810082
self.createdatms = createdatms
1005910083
self.expiresatms = expiresatms
1006010084
self.presentation = presentation
1006110085
self.resolvedatms = resolvedatms
10062-
self.reason = reason
1006310086
self.status = status
1006410087
self.decision = decision
10088+
self.reason = reason
1006510089
}
1006610090

1006710091
private enum CodingKeys: String, CodingKey {
@@ -10071,9 +10095,9 @@ public struct DeniedApprovalSnapshot: Codable, Sendable {
1007110095
case expiresatms = "expiresAtMs"
1007210096
case presentation
1007310097
case resolvedatms = "resolvedAtMs"
10074-
case reason
1007510098
case status
1007610099
case decision
10100+
case reason
1007710101
}
1007810102
}
1007910103

@@ -10084,8 +10108,8 @@ public struct ExpiredApprovalSnapshot: Codable, Sendable {
1008410108
public let expiresatms: Int
1008510109
public let presentation: ApprovalPresentation
1008610110
public let resolvedatms: Int
10087-
public let reason: ApprovalTerminalReason
1008810111
public let status: String
10112+
public let reason: ApprovalExpiredReason
1008910113

1009010114
public init(
1009110115
id: String,
@@ -10094,17 +10118,17 @@ public struct ExpiredApprovalSnapshot: Codable, Sendable {
1009410118
expiresatms: Int,
1009510119
presentation: ApprovalPresentation,
1009610120
resolvedatms: Int,
10097-
reason: ApprovalTerminalReason,
10098-
status: String)
10121+
status: String,
10122+
reason: ApprovalExpiredReason)
1009910123
{
1010010124
self.id = id
1010110125
self.urlpath = urlpath
1010210126
self.createdatms = createdatms
1010310127
self.expiresatms = expiresatms
1010410128
self.presentation = presentation
1010510129
self.resolvedatms = resolvedatms
10106-
self.reason = reason
1010710130
self.status = status
10131+
self.reason = reason
1010810132
}
1010910133

1011010134
private enum CodingKeys: String, CodingKey {
@@ -10114,8 +10138,8 @@ public struct ExpiredApprovalSnapshot: Codable, Sendable {
1011410138
case expiresatms = "expiresAtMs"
1011510139
case presentation
1011610140
case resolvedatms = "resolvedAtMs"
10117-
case reason
1011810141
case status
10142+
case reason
1011910143
}
1012010144
}
1012110145

@@ -10126,8 +10150,8 @@ public struct CancelledApprovalSnapshot: Codable, Sendable {
1012610150
public let expiresatms: Int
1012710151
public let presentation: ApprovalPresentation
1012810152
public let resolvedatms: Int
10129-
public let reason: ApprovalTerminalReason
1013010153
public let status: String
10154+
public let reason: ApprovalCancelledReason
1013110155

1013210156
public init(
1013310157
id: String,
@@ -10136,17 +10160,17 @@ public struct CancelledApprovalSnapshot: Codable, Sendable {
1013610160
expiresatms: Int,
1013710161
presentation: ApprovalPresentation,
1013810162
resolvedatms: Int,
10139-
reason: ApprovalTerminalReason,
10140-
status: String)
10163+
status: String,
10164+
reason: ApprovalCancelledReason)
1014110165
{
1014210166
self.id = id
1014310167
self.urlpath = urlpath
1014410168
self.createdatms = createdatms
1014510169
self.expiresatms = expiresatms
1014610170
self.presentation = presentation
1014710171
self.resolvedatms = resolvedatms
10148-
self.reason = reason
1014910172
self.status = status
10173+
self.reason = reason
1015010174
}
1015110175

1015210176
private enum CodingKeys: String, CodingKey {
@@ -10156,8 +10180,8 @@ public struct CancelledApprovalSnapshot: Codable, Sendable {
1015610180
case expiresatms = "expiresAtMs"
1015710181
case presentation
1015810182
case resolvedatms = "resolvedAtMs"
10159-
case reason
1016010183
case status
10184+
case reason
1016110185
}
1016210186
}
1016310187

@@ -10229,6 +10253,92 @@ public struct ApprovalResolveResult: Codable, Sendable {
1022910253
}
1023010254
}
1023110255

10256+
public struct PendingSessionApprovalEvent: Codable, Sendable {
10257+
public let sessionkey: String
10258+
public let sourcesessionkey: String?
10259+
public let updatedatms: Int
10260+
public let phase: String
10261+
public let approval: PendingApprovalSnapshot
10262+
10263+
public init(
10264+
sessionkey: String,
10265+
sourcesessionkey: String? = nil,
10266+
updatedatms: Int,
10267+
phase: String,
10268+
approval: PendingApprovalSnapshot)
10269+
{
10270+
self.sessionkey = sessionkey
10271+
self.sourcesessionkey = sourcesessionkey
10272+
self.updatedatms = updatedatms
10273+
self.phase = phase
10274+
self.approval = approval
10275+
}
10276+
10277+
private enum CodingKeys: String, CodingKey {
10278+
case sessionkey = "sessionKey"
10279+
case sourcesessionkey = "sourceSessionKey"
10280+
case updatedatms = "updatedAtMs"
10281+
case phase
10282+
case approval
10283+
}
10284+
}
10285+
10286+
public struct TerminalSessionApprovalEvent: Codable, Sendable {
10287+
public let sessionkey: String
10288+
public let sourcesessionkey: String?
10289+
public let updatedatms: Int
10290+
public let phase: String
10291+
public let approval: TerminalApprovalSnapshot
10292+
10293+
public init(
10294+
sessionkey: String,
10295+
sourcesessionkey: String? = nil,
10296+
updatedatms: Int,
10297+
phase: String,
10298+
approval: TerminalApprovalSnapshot)
10299+
{
10300+
self.sessionkey = sessionkey
10301+
self.sourcesessionkey = sourcesessionkey
10302+
self.updatedatms = updatedatms
10303+
self.phase = phase
10304+
self.approval = approval
10305+
}
10306+
10307+
private enum CodingKeys: String, CodingKey {
10308+
case sessionkey = "sessionKey"
10309+
case sourcesessionkey = "sourceSessionKey"
10310+
case updatedatms = "updatedAtMs"
10311+
case phase
10312+
case approval
10313+
}
10314+
}
10315+
10316+
public struct SessionApprovalReplay: Codable, Sendable {
10317+
public let sessionkey: String
10318+
public let updatedatms: Int
10319+
public let approvals: [PendingApprovalSnapshot]
10320+
public let truncated: Bool
10321+
10322+
public init(
10323+
sessionkey: String,
10324+
updatedatms: Int,
10325+
approvals: [PendingApprovalSnapshot],
10326+
truncated: Bool)
10327+
{
10328+
self.sessionkey = sessionkey
10329+
self.updatedatms = updatedatms
10330+
self.approvals = approvals
10331+
self.truncated = truncated
10332+
}
10333+
10334+
private enum CodingKeys: String, CodingKey {
10335+
case sessionkey = "sessionKey"
10336+
case updatedatms = "updatedAtMs"
10337+
case approvals
10338+
case truncated
10339+
}
10340+
}
10341+
1023210342
public struct ExecApprovalsGetParams: Codable, Sendable {}
1023310343

1023410344
public struct ExecApprovalsSetParams: Codable, Sendable {
@@ -12209,6 +12319,37 @@ public enum TerminalApprovalSnapshot: Codable, Sendable {
1220912319
}
1221012320
}
1221112321

12322+
public enum SessionApprovalEvent: Codable, Sendable {
12323+
case pending(PendingSessionApprovalEvent)
12324+
case terminal(TerminalSessionApprovalEvent)
12325+
12326+
private enum CodingKeys: String, CodingKey {
12327+
case discriminator = "phase"
12328+
}
12329+
12330+
public init(from decoder: Decoder) throws {
12331+
let container = try decoder.container(keyedBy: CodingKeys.self)
12332+
let discriminator = try container.decode(String.self, forKey: .discriminator)
12333+
switch discriminator {
12334+
case "pending": self = try .pending(PendingSessionApprovalEvent(from: decoder))
12335+
case "terminal": self = try .terminal(TerminalSessionApprovalEvent(from: decoder))
12336+
default:
12337+
throw DecodingError.dataCorruptedError(
12338+
forKey: .discriminator,
12339+
in: container,
12340+
debugDescription: "Unknown SessionApprovalEvent discriminator value"
12341+
)
12342+
}
12343+
}
12344+
12345+
public func encode(to encoder: Encoder) throws {
12346+
switch self {
12347+
case .pending(let value): try value.encode(to: encoder)
12348+
case .terminal(let value): try value.encode(to: encoder)
12349+
}
12350+
}
12351+
}
12352+
1221212353
public enum PluginCatalogInstallAction: Codable, Sendable {
1221312354
case clawhub(PluginCatalogClawHubInstall)
1221412355
case official(PluginCatalogOfficialInstall)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
0597218863ded2b9e86ab56773c5d92b9bd6dcb5e5d537e8e83397fcbb56a5d5 plugin-sdk-api-baseline.json
2-
6d7406435ac48216be84c10d216703c0459577762899d20c5921993e9f1e3478 plugin-sdk-api-baseline.jsonl
1+
634a2cda48d53c9a234d59200b4666c105615da4d64612ea30facb42df9c4d93 plugin-sdk-api-baseline.json
2+
f5bc2ec06dbdc26736f8d810d75f72f378ffd2111c2a143ed153a284b6d89d25 plugin-sdk-api-baseline.jsonl

docs/gateway/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,9 @@ Defaults include isolated state/config and base gateway port `19001`.
304304
a generated dump of every callable helper route.
305305
- Requests: `req(method, params)``res(ok/payload|error)`.
306306
- Common events include `connect.challenge`, `agent`, `chat`,
307-
`session.message`, `session.operation`, `session.tool`, `sessions.changed`,
308-
`presence`, `tick`, `health`, `heartbeat`, pairing/approval lifecycle events,
309-
and `shutdown`.
307+
`session.message`, `session.operation`, `session.tool`, opt-in
308+
`session.approval`, `sessions.changed`, `presence`, `tick`, `health`,
309+
`heartbeat`, pairing/approval lifecycle events, and `shutdown`.
310310

311311
Agent runs are two-stage:
312312

docs/gateway/protocol.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ methods. Treat this as feature discovery, not a full enumeration of
507507
<Accordion title="Session control">
508508
- `sessions.list` returns the current session index, including per-row `agentRuntime` metadata when an agent runtime backend is configured.
509509
- `sessions.subscribe` and `sessions.unsubscribe` toggle session change event subscriptions for the current WS client.
510-
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle transcript/message event subscriptions for one session.
510+
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle transcript/message event subscriptions for one session. Pass `includeApprovals: true` to also receive sanitized `session.approval` lifecycle events for approvals whose persisted audience includes that exact session and whose reviewer binding authorizes the subscribing client. The subscribe response then includes a bounded pending `approvalReplay`; it is authoritative when `truncated` is false. The opt-in is per subscribe call, not sticky: re-subscribing to the same session without `includeApprovals: true` removes an existing approval subscription. In addition to normal session-read authority, this opt-in requires `operator.admin`, or `operator.approvals` on a paired device.
511511
- `sessions.preview` returns bounded transcript previews for specific session keys.
512512
- `sessions.describe` returns one gateway session row for an exact session key.
513513
- `sessions.resolve` resolves or canonicalizes a session target.
@@ -580,6 +580,9 @@ methods. Treat this as feature discovery, not a full enumeration of
580580
`replace=true` and use `deltaText` as the replacement text.
581581
- `session.message`, `session.operation`, `session.tool`: transcript, in-flight
582582
session operation, and event-stream updates for a subscribed session.
583+
- `session.approval`: sanitized pending and terminal approval truth for an
584+
explicitly opted-in exact-session subscriber. Child approvals use the
585+
persisted ancestor audience; events never mutate transcripts or wake agents.
583586
- `sessions.changed`: session index or metadata changed.
584587
- `presence`: system presence snapshot updates.
585588
- `tick`: periodic keepalive/liveness event.

0 commit comments

Comments
 (0)