返回 DeepSeek-Reasonix
hello.go
根目录 / desktop / internal / hostrpc / hello.go
1 package hostrpc
2
3 import (
4 "errors"
5 "fmt"
6 "maps"
7
8 "reasonix/internal/extension/rpcwire"
9 "reasonix/internal/pathidentity"
10 )
11
12 // Error codes from docs/DESKTOP_HOST_PROTOCOL.md. Business errors raised by
13 // an invoked method use CodeBusiness with data.method naming the command.
14 const (
15 CodeBusiness = -32000
16 CodeProtocolMismatch = -32001
17 CodeNotReady = -32002
18 CodeContractMismatch = -32003
19 CodeBuildMismatch = -32004
20 CodeInstanceMismatch = -32005
21 )
22
23 // Identity is what the service reports about itself in hello and checks
24 // the shell against. Home is the canonical Reasonix data home.
25 type Identity struct {
26 Version string
27 Channel string
28 Commit string
29 Home string
30 }
31
32 // HelloParams is the shell's desktop/hello request.
33 type HelloParams struct {
34 ProtocolVersion int `json:"protocolVersion"`
35 ContractDigest string `json:"contractDigest"`
36 Build BuildInfo `json:"build"`
37 Host HostInfo `json:"host"`
38 Instance HelloInstance `json:"instance"`
39 }
40
41 // BuildInfo identifies one side's release build.
42 type BuildInfo struct {
43 Version string `json:"version"`
44 Channel string `json:"channel"`
45 Commit string `json:"commit"`
46 }
47
48 // HostInfo describes the shell runtime.
49 type HostInfo struct {
50 Name string `json:"name"`
51 Version string `json:"version"`
52 Chrome string `json:"chrome"`
53 Platform string `json:"platform"`
54 Arch string `json:"arch"`
55 }
56
57 // HelloInstance is the data home the shell was launched for; Dev relaxes
58 // the build check for unpackaged shells.
59 type HelloInstance struct {
60 Home string `json:"home"`
61 Dev bool `json:"dev"`
62 }
63
64 // HelloResult is the service's desktop/hello response.
65 type HelloResult struct {
66 ProtocolVersion int `json:"protocolVersion"`
67 ContractDigest string `json:"contractDigest"`
68 Service ServiceInfo `json:"service"`
69 RuntimeGeneration string `json:"runtimeGeneration"`
70 Resources Resources `json:"resources"`
71 Window *WindowGeometry `json:"window,omitempty"`
72 Instance *InstanceInfo `json:"instance,omitempty"`
73 // The shell validates both identifiers as present strings and accepts
74 // empty values when diagnostics are disabled; omitting the keys is a
75 // handshake failure, so they must never carry omitempty.
76 RunID string `json:"runId"`
77 IncidentID string `json:"incidentId"`
78 DiagnosticsEnabled bool `json:"diagnosticsEnabled"`
79 }
80
81 // InstanceInfo extends the handshake without invalidating older shells.
82 type InstanceInfo struct {
83 IdentityVersion int `json:"identityVersion"`
84 IdentityDigest string `json:"identityDigest"`
85 LegacyID string `json:"legacyId"`
86 }
87
88 type ShutdownParams struct {
89 RequestID string `json:"requestId"`
90 Reason string `json:"reason"`
91 }
92
93 type ShutdownStatusParams struct {
94 RequestID string `json:"requestId"`
95 }
96
97 type ShutdownResult struct {
98 RequestID string `json:"requestId"`
99 Reason string `json:"reason"`
100 Phase string `json:"phase"`
101 Outcome string `json:"outcome"`
102 Completed bool `json:"completed"`
103 Retryable bool `json:"retryable"`
104 ErrorCode string `json:"errorCode,omitempty"`
105 Error string `json:"error,omitempty"`
106 UpdatedAt string `json:"updatedAt"`
107 }
108
109 // ServiceInfo is the service build plus its process id.
110 type ServiceInfo struct {
111 BuildInfo
112 PID int `json:"pid"`
113 }
114
115 // Resources locates the loopback origin serving authorised assets and the
116 // bearer token the shell's main process attaches to every request.
117 type Resources struct {
118 Origin string `json:"origin"`
119 Token string `json:"token"`
120 }
121
122 // WindowGeometry is the initial main-window geometry the shell creates
123 // the hidden window with.
124 type WindowGeometry struct {
125 Position *WindowPosition `json:"position,omitempty"`
126 Width int `json:"width"`
127 Height int `json:"height"`
128 MinWidth int `json:"minWidth"`
129 MinHeight int `json:"minHeight"`
130 Frameless bool `json:"frameless"`
131 ZoomFactor float64 `json:"zoomFactor"`
132 }
133
134 // WindowPosition is optional: an absent saved position asks the shell to center.
135 type WindowPosition struct {
136 X int `json:"x"`
137 Y int `json:"y"`
138 }
139
140 func mismatch(code int, name, message string, detail map[string]any) error {
141 data := map[string]any{"name": name}
142 maps.Copy(data, detail)
143 return &rpcwire.RPCError{Code: code, Message: message, Data: data}
144 }
145
146 func notReady() error {
147 return mismatch(CodeNotReady, "not_ready", "desktop/hello has not completed", nil)
148 }
149
150 // validateHello runs the handshake checks in the order the protocol lists
151 // them; the first failure is the terminal error the shell displays.
152 func validateHello(p HelloParams, digest string, id Identity) error {
153 if p.ProtocolVersion != ProtocolVersion {
154 return mismatch(CodeProtocolMismatch, "protocol_mismatch",
155 fmt.Sprintf("shell speaks protocol %d, service speaks %d", p.ProtocolVersion, ProtocolVersion),
156 map[string]any{"expected": ProtocolVersion, "got": p.ProtocolVersion})
157 }
158 if p.ContractDigest != digest {
159 return mismatch(CodeContractMismatch, "contract_mismatch",
160 "shell and service were built from different desktop contracts",
161 map[string]any{"expected": digest, "got": p.ContractDigest})
162 }
163 dev := p.Instance.Dev || p.Build.Version == "dev" || id.Version == "dev"
164 if !dev && p.Build.Version != id.Version {
165 return mismatch(CodeBuildMismatch, "build_mismatch",
166 fmt.Sprintf("shell build %s does not match service build %s", p.Build.Version, id.Version),
167 map[string]any{"expected": id.Version, "got": p.Build.Version})
168 }
169 same, identityErr := pathidentity.Same(p.Instance.Home, id.Home, pathidentity.Options{FollowLeaf: true})
170 if identityErr != nil || !same {
171 return mismatch(CodeInstanceMismatch, "instance_mismatch",
172 "shell data home does not match the service data home",
173 map[string]any{"reason": identityReason(identityErr)})
174 }
175 return nil
176 }
177
178 func identityReason(err error) string {
179 if err == nil {
180 return "different_directory"
181 }
182 var identityErr *pathidentity.Error
183 if errors.As(err, &identityErr) {
184 return string(identityErr.Kind)
185 }
186 return "unknown"
187 }
188
188 lines GO