Skip to content

Commit 490ef98

Browse files
author
SqlRush
committed
Audit remote registration protocol metadata
1 parent 5ebcdbc commit 490ef98

6 files changed

Lines changed: 100 additions & 5 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,8 @@ M7 补充:terminal input parser 和 configurable keybinding name parser 现在
14861486

14871487
当前状态:已有 Task/TaskOutput/KillTask/SendMessage/TeamCreate/TeamDelete/TeamOutput/TeamSendMessage/TeamDispatch/TeamSchedule/TeamAutoSchedule/TeamCoordinate/ResumeTask/Sleep/Brief/ScheduleCron/RemoteTrigger 入口、sidechain metadata/lifecycle、team manifest、schedule manifest、remote trigger receipt manifest、daemon heartbeat CLI/state/status 审计、ScheduleCron manual trigger/run_due/turn-start due tick、team coordinator_task_id 元数据、TeamOutput coordinator status、TeamSendMessage target routing、TeamDispatch individualized assignments、TeamSchedule deterministic member assignments、TeamAutoSchedule coordinator briefing + member assignments、TeamCoordinate coordinator briefing、structured handoff brief、remote trigger injection/event_id dedupe、remote service manifest、remote registrationUrl/authToken POST 注册状态、remote poll URL/cursor 与 websocket_url 多帧/tick 消息泵、WebSocket 基础重连/backoff 与连接计数审计、bridge direct `/remote-trigger`/`/remote-service` HTTP endpoint、WebSocket `remote_trigger`/`remote_status`/`hello`/`health`/`manifest` action、remote_trigger/remote_service/websocket_protocol manifest capability、task progress event、显式与 settings 默认 owned worktree 创建/清理、sparse/symlink settings 应用、`run:true` subagent nested tool loop、agent permission mode 应用、agent tool allowlist registry/permission pattern 过滤,以及 completed/failed/cancelled 终态 owned worktree 自动清理;完整远端 WebSocket 常驻持久 stream、daemon 托管调度循环、多 agent 后台调度循环和模型驱动团队自动调度仍未完成。
14881488

1489+
本轮补充:remote registration 响应会持久化协议版本、能力列表和 lease renew/refresh endpoint,并在 `/status show remote` 中脱敏展示,为后续租约刷新调用和协议兼容判断提供可审计状态;实际 lease renew/refresh 调用和协议版本强制兼容仍未完成。
1490+
14891491
### M11: Bridge, LSP, Telemetry, Advanced Integrations
14901492

14911493
目标:补齐高级集成能力。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ test/parity/ # golden tests against TS/official behavior
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 云端长连接仍未完成。
748748
- 本轮补充:remote registration 响应解析现在会先读取顶层字段,再递归解包 `data``session``remote_session``registration``result``payload` wrapper,兼容云端把 remote session、registration id、websocket/poll endpoint 放在 envelope 内返回。更深的注册协议协商和租约刷新仍未完成。
749+
- 本轮补充:remote registration 响应现在会持久化 `protocolVersion`/`protocol_version``capabilities`/`features``leaseRenewUrl`/`lease_refresh_url` 元数据,并在 `/status show remote` 中以脱敏 URL 暴露协议版本、能力列表和 lease renew endpoint,为后续租约刷新调用提供可审计协议 surface。实际 lease renew/refresh 调用和协议版本强制兼容仍未完成。
749750
- 本轮补充:新增 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 仍未完成。
750751
- 本轮补充: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 仍未完成。
751752
- 本轮补充:`internal/remote` 新增 callback 型 `StreamWebSocketEvents` primitive,可保持 WebSocket 连接逐帧解码并把事件批次交给调用方,支持 context 取消、可选帧上限、handler 错误传播、异常 close/读错后的 backoff 重连以及 `ReconnectAttempts < 0` 无限重连语义;该能力为 daemon 常驻 stream 托管接线打底。完整云端协议 hardening 仍未完成。

internal/conversation/run.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,12 +1836,21 @@ func formatRemoteRegistration(state remotepkg.RegistrationState) []string {
18361836
if state.RegistrationID != "" {
18371837
parts = append(parts, "registration "+state.RegistrationID)
18381838
}
1839+
if state.ProtocolVersion != "" {
1840+
parts = append(parts, "protocol "+state.ProtocolVersion)
1841+
}
1842+
if len(state.Capabilities) > 0 {
1843+
parts = append(parts, "capabilities "+strings.Join(state.Capabilities, ", "))
1844+
}
18391845
if state.WebSocketURL != "" {
18401846
parts = append(parts, "websocket "+remotepkg.DisplayEndpoint(state.WebSocketURL))
18411847
}
18421848
if state.PollURL != "" {
18431849
parts = append(parts, "poll "+remotepkg.DisplayEndpoint(state.PollURL))
18441850
}
1851+
if state.LeaseRenewURL != "" {
1852+
parts = append(parts, "lease renew "+remotepkg.DisplayEndpoint(state.LeaseRenewURL))
1853+
}
18451854
lines := []string{"Remote registration: " + strings.Join(parts, ": ")}
18461855
if state.Error != "" {
18471856
lines = append(lines, "Remote registration error: "+state.Error)

internal/conversation/run_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2812,8 +2812,11 @@ func TestRunnerExecutesStatusShowSectionsWithoutQuery(t *testing.T) {
28122812
RegistrationURL: "https://remote.example/register",
28132813
StatusCode: http.StatusAccepted,
28142814
RemoteSessionID: "remote-status",
2815+
ProtocolVersion: "ccr.remote.v1",
2816+
Capabilities: []string{"websocket_protocol", "lease_renew"},
28152817
WebSocketURL: "wss://remote.example/ws?token=secret",
28162818
PollURL: "https://remote.example/poll?token=secret",
2819+
LeaseRenewURL: "https://remote.example/leases/renew?token=secret",
28172820
RegisteredAt: "2026-06-17T10:02:00Z",
28182821
}); err != nil {
28192822
t.Fatal(err)
@@ -2982,7 +2985,7 @@ func TestRunnerExecutesStatusShowSectionsWithoutQuery(t *testing.T) {
29822985
"Status remote",
29832986
"Enabled: disabled",
29842987
"Remote environment: env-status",
2985-
"Remote registration: registered: url https://remote.example/register: status 202: remote session remote-status: websocket wss://remote.example/ws: poll https://remote.example/poll",
2988+
"Remote registration: registered: url https://remote.example/register: status 202: remote session remote-status: protocol ccr.remote.v1: capabilities websocket_protocol, lease_renew: websocket wss://remote.example/ws: poll https://remote.example/poll: lease renew https://remote.example/leases/renew",
29862989
"Remote pump: running: transport websocket: websocket wss://remote.example/ws: poll https://remote.example/poll: cursor cursor-2: status 200: frames 2: connects 1: reconnects 1: ack events 2: ack sent 1: ack errors 1: lease events 1: lease expired 1: events 3: delivered 2: duplicates 1: errors 0",
29872990
"close 1000",
29882991
"stream started 2026-06-17T10:03:00Z: stream ended 2026-06-17T10:08:00Z: stream stop context_cancelled",

internal/remote/registration.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ type RegistrationState struct {
3737
StatusCode int `json:"status_code,omitempty"`
3838
RemoteSessionID string `json:"remote_session_id,omitempty"`
3939
RegistrationID string `json:"registration_id,omitempty"`
40+
ProtocolVersion string `json:"protocol_version,omitempty"`
41+
Capabilities []string `json:"capabilities,omitempty"`
4042
WebSocketURL string `json:"websocket_url,omitempty"`
4143
PollURL string `json:"poll_url,omitempty"`
44+
LeaseRenewURL string `json:"lease_renew_url,omitempty"`
4245
Message string `json:"message,omitempty"`
4346
Error string `json:"error,omitempty"`
4447
ManifestServices int `json:"manifest_services,omitempty"`
@@ -198,12 +201,24 @@ func applyRegistrationResponseFields(state *RegistrationState, raw map[string]an
198201
if state.RegistrationID == "" {
199202
state.RegistrationID = firstString(raw, "registration_id", "registrationId", "id")
200203
}
204+
if state.ProtocolVersion == "" {
205+
state.ProtocolVersion = firstString(raw, "protocol_version", "protocolVersion", "remote_protocol_version", "remoteProtocolVersion", "version")
206+
}
207+
if len(state.Capabilities) == 0 {
208+
state.Capabilities = firstStringList(raw, "capabilities", "features", "supported_capabilities", "supportedCapabilities")
209+
}
201210
if state.WebSocketURL == "" {
202211
state.WebSocketURL = firstString(raw, "websocket_url", "websocketUrl", "web_socket_url", "ws_url", "wsUrl")
203212
}
204213
if state.PollURL == "" {
205214
state.PollURL = firstString(raw, "poll_url", "pollUrl", "events_url", "eventsUrl")
206215
}
216+
if state.LeaseRenewURL == "" {
217+
state.LeaseRenewURL = firstString(raw, "lease_renew_url", "leaseRenewUrl", "lease_refresh_url", "leaseRefreshUrl", "renew_url", "renewUrl")
218+
}
219+
if state.LeaseRenewURL == "" {
220+
state.LeaseRenewURL = stringFromNestedMap(raw["lease"], "renew_url", "renewUrl", "refresh_url", "refreshUrl", "url")
221+
}
207222
if state.Message == "" {
208223
state.Message = firstString(raw, "message", "status", "detail")
209224
}
@@ -231,6 +246,59 @@ func firstString(values map[string]any, keys ...string) string {
231246
return ""
232247
}
233248

249+
func firstStringList(values map[string]any, keys ...string) []string {
250+
for _, key := range keys {
251+
if value, ok := values[key]; ok {
252+
if list := normalizeStringList(value); len(list) > 0 {
253+
return list
254+
}
255+
}
256+
}
257+
return nil
258+
}
259+
260+
func normalizeStringList(value any) []string {
261+
switch typed := value.(type) {
262+
case []string:
263+
return cleanStringList(typed)
264+
case []any:
265+
items := make([]string, 0, len(typed))
266+
for _, item := range typed {
267+
items = append(items, fmt.Sprint(item))
268+
}
269+
return cleanStringList(items)
270+
case string:
271+
text := strings.TrimSpace(typed)
272+
if text == "" {
273+
return nil
274+
}
275+
if strings.Contains(text, ",") {
276+
return cleanStringList(strings.Split(text, ","))
277+
}
278+
return cleanStringList(strings.Fields(text))
279+
default:
280+
text := strings.TrimSpace(fmt.Sprint(typed))
281+
if text == "" || text == "<nil>" {
282+
return nil
283+
}
284+
return []string{text}
285+
}
286+
}
287+
288+
func cleanStringList(values []string) []string {
289+
cleaned := make([]string, 0, len(values))
290+
seen := make(map[string]bool, len(values))
291+
for _, value := range values {
292+
text := strings.TrimSpace(value)
293+
if text == "" || text == "<nil>" || seen[text] {
294+
continue
295+
}
296+
seen[text] = true
297+
cleaned = append(cleaned, text)
298+
}
299+
return cleaned
300+
}
301+
234302
func remoteRegistrationError(status string, body []byte) string {
235303
bodyText := strings.TrimSpace(string(body))
236304
if bodyText == "" {

internal/remote/registration_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestRegisterManifestPostsServiceManifest(t *testing.T) {
2626
t.Fatal(err)
2727
}
2828
w.Header().Set("content-type", "application/json")
29-
_, _ = w.Write([]byte(`{"remoteSessionId":"remote-sess","registration_id":"reg-1","websocketUrl":"wss://remote/ws","pollUrl":"https://remote/events","message":"registered"}`))
29+
_, _ = w.Write([]byte(`{"remoteSessionId":"remote-sess","registration_id":"reg-1","protocolVersion":"ccr.remote.v1","capabilities":["websocket_protocol","lease_renew","lease_renew"],"websocketUrl":"wss://remote/ws","pollUrl":"https://remote/events","lease":{"renewUrl":"https://remote/leases/renew?token=secret"},"message":"registered"}`))
3030
}))
3131
defer server.Close()
3232

@@ -42,7 +42,10 @@ func TestRegisterManifestPostsServiceManifest(t *testing.T) {
4242
},
4343
Now: now,
4444
})
45-
if state.RuntimeState != RegistrationRegistered || state.StatusCode != http.StatusOK || state.RemoteSessionID != "remote-sess" || state.RegistrationID != "reg-1" || state.WebSocketURL != "wss://remote/ws" || state.PollURL != "https://remote/events" {
45+
if state.RuntimeState != RegistrationRegistered || state.StatusCode != http.StatusOK || state.RemoteSessionID != "remote-sess" || state.RegistrationID != "reg-1" || state.ProtocolVersion != "ccr.remote.v1" || state.WebSocketURL != "wss://remote/ws" || state.PollURL != "https://remote/events" || state.LeaseRenewURL != "https://remote/leases/renew?token=secret" {
46+
t.Fatalf("registration state = %#v", state)
47+
}
48+
if len(state.Capabilities) != 2 || state.Capabilities[0] != "websocket_protocol" || state.Capabilities[1] != "lease_renew" {
4649
t.Fatalf("registration state = %#v", state)
4750
}
4851
if got.SessionID != "sess_remote" || len(got.Services) != 1 || got.Services[0].Name != "bridge" {
@@ -66,8 +69,11 @@ func TestRegisterManifestAcceptsWrappedResponse(t *testing.T) {
6669
"sessionId":"remote-wrapped",
6770
"registration":{
6871
"id":"reg-wrapped",
72+
"protocol_version":"ccr.remote.v2",
73+
"features":"remote_trigger lease_refresh",
6974
"web_socket_url":"wss://remote/wrapped/ws",
7075
"eventsUrl":"https://remote/wrapped/events",
76+
"lease_refresh_url":"https://remote/wrapped/leases/refresh",
7177
"detail":"nested detail"
7278
}
7379
}
@@ -84,7 +90,10 @@ func TestRegisterManifestAcceptsWrappedResponse(t *testing.T) {
8490
},
8591
Now: now,
8692
})
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" {
93+
if state.RuntimeState != RegistrationRegistered || state.RemoteSessionID != "remote-wrapped" || state.RegistrationID != "reg-wrapped" || state.ProtocolVersion != "ccr.remote.v2" || state.WebSocketURL != "wss://remote/wrapped/ws" || state.PollURL != "https://remote/wrapped/events" || state.LeaseRenewURL != "https://remote/wrapped/leases/refresh" || state.Message != "registered" {
94+
t.Fatalf("registration state = %#v", state)
95+
}
96+
if len(state.Capabilities) != 2 || state.Capabilities[0] != "remote_trigger" || state.Capabilities[1] != "lease_refresh" {
8897
t.Fatalf("registration state = %#v", state)
8998
}
9099
}
@@ -109,14 +118,17 @@ func TestRegisterManifestHandlesDisabledAndFailedState(t *testing.T) {
109118
func TestWriteAndLoadRegistrationState(t *testing.T) {
110119
path := filepath.Join(t.TempDir(), "sess_remote", registrationFileName)
111120
state := DisabledRegistrationState(Manifest{SessionID: "sess_remote"}, "/state/remote-service.json", time.Date(2026, 6, 17, 11, 10, 0, 0, time.UTC))
121+
state.ProtocolVersion = "ccr.remote.v1"
122+
state.Capabilities = []string{"websocket_protocol", "lease_renew"}
123+
state.LeaseRenewURL = "https://remote/leases/renew"
112124
if err := WriteRegistrationState(path, state); err != nil {
113125
t.Fatal(err)
114126
}
115127
loaded, err := LoadRegistrationState(path)
116128
if err != nil {
117129
t.Fatal(err)
118130
}
119-
if loaded.SessionID != "sess_remote" || loaded.RuntimeState != RegistrationDisabled || loaded.ManifestPath != "/state/remote-service.json" {
131+
if loaded.SessionID != "sess_remote" || loaded.RuntimeState != RegistrationDisabled || loaded.ManifestPath != "/state/remote-service.json" || loaded.ProtocolVersion != "ccr.remote.v1" || loaded.LeaseRenewURL != "https://remote/leases/renew" || len(loaded.Capabilities) != 2 {
120132
t.Fatalf("loaded = %#v", loaded)
121133
}
122134
missing, err := LoadRegistrationState(filepath.Join(t.TempDir(), registrationFileName))

0 commit comments

Comments
 (0)