|
1 | | -// SPDX-License-Identifier: AGPL-3.0-or-later |
2 | | - |
3 | | -// Embedded daemon entry points. |
4 | | -// |
5 | | -// The other half of this package (bindings.go) is a thin RPC client |
6 | | -// that talks to an out-of-process daemon over a Unix socket. That |
7 | | -// model fits desktop SDKs (Python, Node) where the daemon is a |
8 | | -// separate long-running process. It does NOT fit iOS: one app = |
9 | | -// one process, no sibling daemon binary, no system-wide socket. |
10 | | -// |
11 | | -// PilotEmbeddedStart boots a daemon directly inside the host process |
12 | | -// (the goroutine model is unchanged from cmd/daemon). Internally it |
13 | | -// still opens a Unix socket so the existing 45 Pilot* RPC functions |
14 | | -// in bindings.go work unchanged — same wire protocol, just a |
15 | | -// different addressing space. |
16 | | -// |
17 | | -// Lifecycle: PilotEmbeddedStart → PilotConnect(socketPath) → use |
18 | | -// driver functions → PilotClose(handle) → PilotEmbeddedStop. |
19 | | - |
20 | | -package main |
21 | | - |
22 | | -/* |
23 | | -#include <stdlib.h> |
24 | | -#include <stdint.h> |
25 | | -*/ |
26 | | -import "C" |
27 | | - |
28 | | -import ( |
29 | | - "context" |
30 | | - "encoding/json" |
31 | | - "fmt" |
32 | | - "os" |
33 | | - "path/filepath" |
34 | | - "sync" |
35 | | - "time" |
36 | | - |
37 | | - "github.com/TeoSlayer/pilotprotocol/pkg/daemon" |
38 | | - "github.com/pilot-protocol/common/driver" |
39 | | - |
40 | | - "github.com/pilot-protocol/handshake" |
41 | | - "github.com/pilot-protocol/policy" |
42 | | - "github.com/pilot-protocol/runtime" |
43 | | - "github.com/pilot-protocol/skillinject" |
44 | | -) |
45 | | - |
46 | | -type embeddedNode struct { |
47 | | - d *daemon.Daemon |
48 | | - rt *runtime.Runtime |
49 | | -} |
50 | | - |
51 | | -var embedded struct { |
52 | | - sync.Mutex |
53 | | - node *embeddedNode |
54 | | -} |
55 | | - |
56 | | -// EmbeddedConfig is what callers supply via JSON in PilotEmbeddedStart. |
57 | | -// Keep it minimal and JSON-friendly so the Swift/Obj-C side can build |
58 | | -// it with a dictionary literal. |
59 | | -type embeddedConfig struct { |
60 | | - DataDir string `json:"data_dir"` // absolute, host-writable |
61 | | - SocketPath string `json:"socket_path"` // ≤ 100 bytes; use relative if abs is too long |
62 | | - RegistryAddr string `json:"registry_addr"` // default 34.71.57.205:9000 |
63 | | - BeaconAddr string `json:"beacon_addr"` // default 34.71.57.205:9001 |
64 | | - TrustAutoApprove bool `json:"trust_auto_approve"` // accept all incoming handshakes |
65 | | - KeepaliveSec int `json:"keepalive_sec"` // default 30; lower → faster handshake polling |
66 | | - Version string `json:"version"` // surfaced in Info(); cosmetic |
67 | | -} |
68 | | - |
69 | | -func (c *embeddedConfig) defaults() { |
70 | | - if c.RegistryAddr == "" { |
71 | | - c.RegistryAddr = "34.71.57.205:9000" |
72 | | - } |
73 | | - if c.BeaconAddr == "" { |
74 | | - c.BeaconAddr = "34.71.57.205:9001" |
75 | | - } |
76 | | - if c.KeepaliveSec <= 0 { |
77 | | - c.KeepaliveSec = 30 |
78 | | - } |
79 | | - if c.Version == "" { |
80 | | - c.Version = "embedded" |
81 | | - } |
82 | | -} |
83 | | - |
84 | | -// Boot the embedded daemon. configJSON is a JSON-encoded embeddedConfig. |
85 | | -// Returns JSON: on success {"address","node_id","public_key","socket"}, |
86 | | -// on failure {"error": "..."}. |
87 | | -// |
88 | | -// Idempotent only in the trivial sense — calling twice without Stop |
89 | | -// returns an error. |
90 | | -// |
91 | | -//export PilotEmbeddedStart |
92 | | -func PilotEmbeddedStart(configJSON *C.char) *C.char { |
93 | | - embedded.Lock() |
94 | | - defer embedded.Unlock() |
95 | | - if embedded.node != nil { |
96 | | - return errJSON(fmt.Errorf("embedded daemon already started")) |
97 | | - } |
98 | | - |
99 | | - var cfg embeddedConfig |
100 | | - if err := unmarshalCString(configJSON, &cfg); err != nil { |
101 | | - return errJSON(fmt.Errorf("parse config: %w", err)) |
102 | | - } |
103 | | - cfg.defaults() |
104 | | - if cfg.DataDir == "" { |
105 | | - return errJSON(fmt.Errorf("data_dir required")) |
106 | | - } |
107 | | - if cfg.SocketPath == "" { |
108 | | - return errJSON(fmt.Errorf("socket_path required")) |
109 | | - } |
110 | | - |
111 | | - identityPath := filepath.Join(cfg.DataDir, "identity.json") |
112 | | - |
113 | | - d := daemon.New(daemon.Config{ |
114 | | - RegistryAddr: cfg.RegistryAddr, |
115 | | - BeaconAddr: cfg.BeaconAddr, |
116 | | - ListenAddr: ":0", |
117 | | - SocketPath: cfg.SocketPath, |
118 | | - Encrypt: true, |
119 | | - IdentityPath: identityPath, |
120 | | - DisableEcho: true, |
121 | | - DisableDataExchange: true, |
122 | | - DisableEventStream: true, |
123 | | - TrustAutoApprove: cfg.TrustAutoApprove, |
124 | | - KeepaliveInterval: time.Duration(cfg.KeepaliveSec) * time.Second, |
125 | | - Version: cfg.Version, |
126 | | - }) |
127 | | - |
128 | | - rt := runtime.New(d.DaemonAPI()) |
129 | | - |
130 | | - // Minimum plugin set for handshake + datagram I/O. No |
131 | | - // trustedagents (it gates trust to a curated GitHub list and |
132 | | - // breaks ad-hoc peers), no webhook (spams retries to a stale |
133 | | - // URL on macOS dev hosts), no dataexchange/eventstream |
134 | | - // (file-system inbox; clients use SendTo/RecvFrom directly). |
135 | | - if err := rt.Register(skillinject.NewService(skillinject.Config{})); err != nil { |
136 | | - return errJSON(fmt.Errorf("register skillinject: %w", err)) |
137 | | - } |
138 | | - |
139 | | - policySvc := policy.NewService(runtime.NewPolicyRuntime(d.DaemonAPI())) |
140 | | - if err := rt.Register(policySvc); err != nil { |
141 | | - return errJSON(fmt.Errorf("register policy: %w", err)) |
142 | | - } |
143 | | - d.RegisterPolicyManager(runtime.AsDaemonPolicyManager(policySvc.Manager())) |
144 | | - |
145 | | - hsSvc := handshake.NewService(runtime.NewHandshakeRuntime(d.DaemonAPI())) |
146 | | - if err := rt.Register(hsSvc); err != nil { |
147 | | - return errJSON(fmt.Errorf("register handshake: %w", err)) |
148 | | - } |
149 | | - d.RegisterHandshakeService(runtime.NewHandshakeServiceAdapter(hsSvc)) |
150 | | - |
151 | | - if err := rt.StartPlugins(context.Background()); err != nil { |
152 | | - return errJSON(fmt.Errorf("plugin startup: %w", err)) |
153 | | - } |
154 | | - if err := d.Start(); err != nil { |
155 | | - _ = rt.StopPlugins(context.Background()) |
156 | | - return errJSON(fmt.Errorf("daemon start: %w", err)) |
157 | | - } |
158 | | - |
159 | | - embedded.node = &embeddedNode{d: d, rt: rt} |
160 | | - |
161 | | - // Wait for the IPC socket to exist before we probe Info() — Start |
162 | | - // returns once IPC is listening, but on slow simulators the file |
163 | | - // stat can race. |
164 | | - deadline := time.Now().Add(5 * time.Second) |
165 | | - for time.Now().Before(deadline) { |
166 | | - if _, err := os.Stat(cfg.SocketPath); err == nil { |
167 | | - break |
168 | | - } |
169 | | - time.Sleep(50 * time.Millisecond) |
170 | | - } |
171 | | - |
172 | | - // Probe Info() so callers get node_id/address/public_key in the |
173 | | - // startup response without an extra round-trip. |
174 | | - probe, err := driver.Connect(cfg.SocketPath) |
175 | | - if err != nil { |
176 | | - return okJSON(map[string]interface{}{ |
177 | | - "node_id": d.NodeID(), |
178 | | - "socket": cfg.SocketPath, |
179 | | - "warning": fmt.Sprintf("probe connect: %v", err), |
180 | | - }) |
181 | | - } |
182 | | - defer probe.Close() |
183 | | - info, err := probe.Info() |
184 | | - if err != nil { |
185 | | - return okJSON(map[string]interface{}{ |
186 | | - "node_id": d.NodeID(), |
187 | | - "socket": cfg.SocketPath, |
188 | | - "warning": fmt.Sprintf("probe info: %v", err), |
189 | | - }) |
190 | | - } |
191 | | - |
192 | | - return okJSON(map[string]interface{}{ |
193 | | - "address": info["address"], |
194 | | - "node_id": info["node_id"], |
195 | | - "public_key": info["public_key"], |
196 | | - "socket": cfg.SocketPath, |
197 | | - }) |
198 | | -} |
199 | | - |
200 | | -// Tear down the embedded daemon and all plugins. Safe to call when |
201 | | -// not started — returns {"status":"not_started"}. |
202 | | -// |
203 | | -//export PilotEmbeddedStop |
204 | | -func PilotEmbeddedStop() *C.char { |
205 | | - embedded.Lock() |
206 | | - n := embedded.node |
207 | | - embedded.node = nil |
208 | | - embedded.Unlock() |
209 | | - |
210 | | - if n == nil { |
211 | | - return okJSON(map[string]string{"status": "not_started"}) |
212 | | - } |
213 | | - |
214 | | - if err := n.d.Stop(); err != nil { |
215 | | - return errJSON(fmt.Errorf("daemon stop: %w", err)) |
216 | | - } |
217 | | - stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
218 | | - defer cancel() |
219 | | - if err := n.rt.StopPlugins(stopCtx); err != nil { |
220 | | - // Not fatal — daemon is already down, plugin teardown is |
221 | | - // best-effort. Surface the warning to the caller. |
222 | | - return okJSON(map[string]string{ |
223 | | - "status": "stopped", |
224 | | - "warning": err.Error(), |
225 | | - }) |
226 | | - } |
227 | | - return okJSON(map[string]string{"status": "stopped"}) |
228 | | -} |
229 | | - |
230 | | -// unmarshalCString is a tiny helper to JSON-decode a C string into v. |
231 | | -func unmarshalCString(s *C.char, v interface{}) error { |
232 | | - if s == nil { |
233 | | - return fmt.Errorf("nil") |
234 | | - } |
235 | | - return json.Unmarshal([]byte(C.GoString(s)), v) |
236 | | -} |
0 commit comments