Skip to content

Commit 38ce24c

Browse files
author
SqlRush
committed
Accept wrapped remote registration responses
1 parent e3fdd97 commit 38ce24c

4 files changed

Lines changed: 67 additions & 5 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ M10 补充:bridge direct server 新增 `GET /remote-service` discovery endpoin
113113

114114
M10 补充:remote settings 新增 `registrationUrl`/`authToken``advanced.bridge=true` 写出 remote service manifest 后会把 manifest POST 到注册 URL,并将 registered/failed/disabled、HTTP status、远端 session/websocket/poll 信息写入 session-scoped `remote-registration.json``/status show remote` 可审计注册状态且不泄露 token/query。完整 CCR 云端长连接仍未完成。
115115

116+
M10 补充:remote registration 响应解析现在会先读取顶层字段,再递归解包 `data``session``remote_session``registration``result``payload` wrapper,兼容云端把 remote session、registration id、websocket/poll endpoint 放在 envelope 内返回。更深的注册协议协商和租约刷新仍未完成。
117+
116118
M10 补充:新增 session-scoped `remote-pump.json` 和 daemon remote 消息泵;daemon tick 会在 ScheduleCron due tick 后优先读取 registered `websocket_url`,通过 Bearer auth 建立 WebSocket、读取单帧事件并复用 poll 解码/`RemoteTrigger` 注入路径;无 WebSocket 或 WebSocket 失败且存在 poll URL 时回退到带 cursor 的 poll 拉取。pump 状态会记录 transport、websocket/poll URL 脱敏值、cursor、HTTP status、event/delivered/duplicate/error 计数,`/status show remote` 可审计当前传输。完整 CCR WebSocket 常驻持久 stream 和云端协议 hardening 仍未完成。
117119

118120
M10 补充:remote WebSocket pump 现在支持单次 tick 内读取多帧事件,并在握手/读帧失败或非正常 close 时按可配置 backoff 重连;daemon 默认读取最多 8 帧、最多重连 2 次,并把 frame/connect/reconnect 计数和 WebSocket close code 写入 `remote-pump.json``/status show remote`。完整 CCR 云端 WebSocket 常驻持久 stream 与更深协议 hardening 仍未完成。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ test/parity/ # golden tests against TS/official behavior
745745
- 本轮补充:bridge direct WebSocket JSON 通道新增 `hello`/`health`/`manifest` action,`hello` 会返回 protocol version、session、command count、capabilities 和可用 action 列表;bridge manifest 同时暴露 `websocket_protocol` capability,远端长连接客户端可在同一连接内完成能力握手。完整 CCR 云端长连接服务仍未完成。
746746
- 本轮补充:bridge direct server 新增 `GET /remote-service` discovery endpoint 和 WebSocket `remote_status` action,返回同一份 session-scoped remote service manifest;bridge manifest、direct `/manifest` 响应和 `/status show bridge` 同步暴露 `remote_service` capability,远端控制端可通过 HTTP 或已鉴权 WebSocket 查询 bridge/daemon 服务状态。完整 CCR 云端注册和消息泵仍未完成。
747747
- 本轮补充:remote settings 新增 `registrationUrl`/`authToken``advanced.bridge=true` 写出 remote service manifest 后会把 manifest POST 到注册 URL,并将 registered/failed/disabled、HTTP status、远端 session/websocket/poll 信息写入 session-scoped `remote-registration.json``/status show remote` 可审计注册状态且不泄露 token/query。完整 CCR 云端长连接仍未完成。
748+
- 本轮补充:remote registration 响应解析现在会先读取顶层字段,再递归解包 `data``session``remote_session``registration``result``payload` wrapper,兼容云端把 remote session、registration id、websocket/poll endpoint 放在 envelope 内返回。更深的注册协议协商和租约刷新仍未完成。
748749
- 本轮补充:新增 session-scoped `remote-pump.json` 和 daemon remote 消息泵;daemon tick 会在 ScheduleCron due tick 后优先读取 registered `websocket_url`,通过 Bearer auth 建立 WebSocket、读取单帧事件并复用 poll 解码/`RemoteTrigger` 注入路径;无 WebSocket 或 WebSocket 失败且存在 poll URL 时回退到带 cursor 的 poll 拉取。pump 状态会记录 transport、websocket/poll URL 脱敏值、cursor、HTTP status、event/delivered/duplicate/error 计数,`/status show remote` 可审计当前传输。完整 CCR WebSocket 常驻持久 stream 和云端协议 hardening 仍未完成。
749750
- 本轮补充:remote WebSocket pump 现在支持单次 tick 内读取多帧事件,并在握手/读帧失败或非正常 close 时按可配置 backoff 重连;daemon 默认读取最多 8 帧、最多重连 2 次,并把 frame/connect/reconnect 计数和 WebSocket close code 写入 `remote-pump.json``/status show remote`。完整 CCR 云端 WebSocket 常驻持久 stream 与更深协议 hardening 仍未完成。
750751
- 本轮补充:`internal/remote` 新增 callback 型 `StreamWebSocketEvents` primitive,可保持 WebSocket 连接逐帧解码并把事件批次交给调用方,支持 context 取消、可选帧上限、handler 错误传播、异常 close/读错后的 backoff 重连以及 `ReconnectAttempts < 0` 无限重连语义;该能力为 daemon 常驻 stream 托管接线打底。完整云端协议 hardening 仍未完成。

internal/remote/registration.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,37 @@ func applyRegistrationResponse(state *RegistrationState, body []byte) {
187187
state.Message = strings.TrimSpace(string(body))
188188
return
189189
}
190-
state.RemoteSessionID = firstString(raw, "remote_session_id", "remoteSessionId", "session_id", "sessionId")
191-
state.RegistrationID = firstString(raw, "registration_id", "registrationId", "id")
192-
state.WebSocketURL = firstString(raw, "websocket_url", "websocketUrl", "web_socket_url", "ws_url", "wsUrl")
193-
state.PollURL = firstString(raw, "poll_url", "pollUrl", "events_url", "eventsUrl")
194-
state.Message = firstString(raw, "message", "status", "detail")
190+
applyRegistrationResponseFields(state, raw)
191+
applyNestedRegistrationResponseFields(state, raw)
192+
}
193+
194+
func applyRegistrationResponseFields(state *RegistrationState, raw map[string]any) {
195+
if state.RemoteSessionID == "" {
196+
state.RemoteSessionID = firstString(raw, "remote_session_id", "remoteSessionId", "session_id", "sessionId")
197+
}
198+
if state.RegistrationID == "" {
199+
state.RegistrationID = firstString(raw, "registration_id", "registrationId", "id")
200+
}
201+
if state.WebSocketURL == "" {
202+
state.WebSocketURL = firstString(raw, "websocket_url", "websocketUrl", "web_socket_url", "ws_url", "wsUrl")
203+
}
204+
if state.PollURL == "" {
205+
state.PollURL = firstString(raw, "poll_url", "pollUrl", "events_url", "eventsUrl")
206+
}
207+
if state.Message == "" {
208+
state.Message = firstString(raw, "message", "status", "detail")
209+
}
210+
}
211+
212+
func applyNestedRegistrationResponseFields(state *RegistrationState, raw map[string]any) {
213+
for _, key := range []string{"data", "session", "remote_session", "remoteSession", "registration", "result", "payload"} {
214+
nested, ok := raw[key].(map[string]any)
215+
if !ok {
216+
continue
217+
}
218+
applyRegistrationResponseFields(state, nested)
219+
applyNestedRegistrationResponseFields(state, nested)
220+
}
195221
}
196222

197223
func firstString(values map[string]any, keys ...string) string {

internal/remote/registration_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,39 @@ func TestRegisterManifestPostsServiceManifest(t *testing.T) {
5656
}
5757
}
5858

59+
func TestRegisterManifestAcceptsWrappedResponse(t *testing.T) {
60+
now := time.Date(2026, 6, 17, 11, 3, 0, 0, time.UTC)
61+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
62+
w.Header().Set("content-type", "application/json")
63+
_, _ = w.Write([]byte(`{
64+
"message":"registered",
65+
"data":{
66+
"sessionId":"remote-wrapped",
67+
"registration":{
68+
"id":"reg-wrapped",
69+
"web_socket_url":"wss://remote/wrapped/ws",
70+
"eventsUrl":"https://remote/wrapped/events",
71+
"detail":"nested detail"
72+
}
73+
}
74+
}`))
75+
}))
76+
defer server.Close()
77+
78+
state := RegisterManifest(context.Background(), RegistrationOptions{
79+
RegistrationURL: server.URL + "/register",
80+
Manifest: Manifest{
81+
SessionID: "sess_wrapped",
82+
EnvironmentID: "env-prod",
83+
Services: []Service{{Name: "daemon", RuntimeState: "running"}},
84+
},
85+
Now: now,
86+
})
87+
if state.RuntimeState != RegistrationRegistered || state.RemoteSessionID != "remote-wrapped" || state.RegistrationID != "reg-wrapped" || state.WebSocketURL != "wss://remote/wrapped/ws" || state.PollURL != "https://remote/wrapped/events" || state.Message != "registered" {
88+
t.Fatalf("registration state = %#v", state)
89+
}
90+
}
91+
5992
func TestRegisterManifestHandlesDisabledAndFailedState(t *testing.T) {
6093
now := time.Date(2026, 6, 17, 11, 5, 0, 0, time.UTC)
6194
manifest := Manifest{SessionID: "sess_remote", EnvironmentID: "env-prod"}

0 commit comments

Comments
 (0)