| 1 | // Command fullsidecar is the reference Reasonix extension sidecar: one small |
| 2 | // program that exercises every Extension Protocol v1 contribution kind — |
| 3 | // input rewriting, tool interception, system-prompt strategy replacement, an |
| 4 | // extension-hosted streaming provider, structured UI surfaces and prompts, |
| 5 | // and a clean bounded shutdown. It is the example third parties copy. |
| 6 | // |
| 7 | // Behavior map: |
| 8 | // |
| 9 | // input "/fs <text>" → input.receive replaces the input with |
| 10 | // "<text> [rewritten by fullsidecar]" |
| 11 | // tool "dangerous_exec" → tool.before blocks it with a policy reason |
| 12 | // tool "read" → tool.before rewrites the arguments (sandbox) |
| 13 | // system_prompt.build → the strategy slot owner wraps the prompt |
| 14 | // session.start → publishes a status line and a card |
| 15 | // action "demo" → asks a form prompt, greets via notification |
| 16 | // provider plugin/<id>/fake/echo → streams a fixed completion: two text |
| 17 | // chunks, one tool call, usage, done |
| 18 | // |
| 19 | // Environment: |
| 20 | // |
| 21 | // REASONIX_PLUGIN_NAME plugin ID, set by the host at launch (provider |
| 22 | // refs must live in the plugin/<id>/ namespace); |
| 23 | // defaults to "fullsidecar" when run standalone |
| 24 | // FULLSIDECAR_STREAM_INTERVAL_MS |
| 25 | // pacing between provider chunks (default 15) |
| 26 | // |
| 27 | // The two hooks below exist for the host↔SDK conformance suite |
| 28 | // (internal/extension/conformance); they are inert unless set: |
| 29 | // |
| 30 | // FULLSIDECAR_CRASH_ON_INPUT exit(3) without answering when an |
| 31 | // input.receive text matches exactly |
| 32 | // FULLSIDECAR_STALL_ON_INPUT hold an input.receive answer until the |
| 33 | // intercept context ends when the text matches |
| 34 | package main |
| 35 | |
| 36 | import ( |
| 37 | "context" |
| 38 | "encoding/json" |
| 39 | "errors" |
| 40 | "fmt" |
| 41 | "log" |
| 42 | "os" |
| 43 | "strconv" |
| 44 | "strings" |
| 45 | "sync/atomic" |
| 46 | "time" |
| 47 | |
| 48 | extension "github.com/esengine/DeepSeek-Reasonix/sdk/go" |
| 49 | ) |
| 50 | |
| 51 | const ( |
| 52 | rewritePrefix = "/fs " |
| 53 | rewriteSuffix = " [rewritten by fullsidecar]" |
| 54 | deniedTool = "dangerous_exec" |
| 55 | rewrittenTool = "read" |
| 56 | fakeModel = "echo" |
| 57 | defaultPluginID = "fullsidecar" |
| 58 | ) |
| 59 | |
| 60 | // plugin is the extension handler. The session context arrives with the |
| 61 | // handshake and is read by later callbacks, so it travels through an atomic. |
| 62 | type plugin struct { |
| 63 | id string |
| 64 | log *log.Logger |
| 65 | ui extension.HostUI |
| 66 | session atomic.Pointer[extension.SessionContext] |
| 67 | } |
| 68 | |
| 69 | func main() { |
| 70 | logger := log.New(os.Stderr, "fullsidecar: ", log.LstdFlags) |
| 71 | id := strings.TrimSpace(os.Getenv("REASONIX_PLUGIN_NAME")) |
| 72 | if id == "" { |
| 73 | id = defaultPluginID |
| 74 | } |
| 75 | p := &plugin{id: id, log: logger} |
| 76 | provider := &fakeProvider{id: id, interval: streamInterval(), log: logger} |
| 77 | err := extension.Serve(context.Background(), p, extension.Options{ |
| 78 | Name: id, |
| 79 | Version: "1.0.0", |
| 80 | Interceptors: map[string]extension.InterceptorFunc{ |
| 81 | "input.receive": func(ctx context.Context, _ string, payload json.RawMessage) (*extension.InterceptResult, error) { |
| 82 | return p.interceptInput(ctx, payload) |
| 83 | }, |
| 84 | "tool.before": func(ctx context.Context, _ string, payload json.RawMessage) (*extension.InterceptResult, error) { |
| 85 | return p.interceptTool(ctx, payload) |
| 86 | }, |
| 87 | "system_prompt.build": func(ctx context.Context, _ string, payload json.RawMessage) (*extension.InterceptResult, error) { |
| 88 | return p.interceptSystemPrompt(ctx, payload) |
| 89 | }, |
| 90 | }, |
| 91 | Observer: p.observe, |
| 92 | Provider: provider, |
| 93 | UI: extension.UIHandler{ |
| 94 | Action: p.action, |
| 95 | Submit: p.submit, |
| 96 | }, |
| 97 | Shutdown: func(context.Context) { logger.Print("shutdown requested; exiting") }, |
| 98 | Logger: logger, |
| 99 | }) |
| 100 | if err != nil { |
| 101 | logger.Printf("serve: %v", err) |
| 102 | os.Exit(1) |
| 103 | } |
| 104 | // Serve returned nil: the host asked for shutdown. Exit 0 so the host |
| 105 | // reaps the process as an orderly stop. |
| 106 | } |
| 107 | |
| 108 | // streamInterval reads FULLSIDECAR_STREAM_INTERVAL_MS with a 15ms default. |
| 109 | func streamInterval() time.Duration { |
| 110 | if raw := strings.TrimSpace(os.Getenv("FULLSIDECAR_STREAM_INTERVAL_MS")); raw != "" { |
| 111 | if ms, err := strconv.Atoi(raw); err == nil && ms > 0 { |
| 112 | return time.Duration(ms) * time.Millisecond |
| 113 | } |
| 114 | } |
| 115 | return 15 * time.Millisecond |
| 116 | } |
| 117 | |
| 118 | // Initialize declares everything this extension contributes. The host rejects |
| 119 | // anything the installed manifest did not declare first. |
| 120 | func (p *plugin) Initialize(_ context.Context, params extension.InitializeParams) (*extension.InitializeResult, error) { |
| 121 | session := params.Session |
| 122 | p.session.Store(&session) |
| 123 | p.log.Printf("initialized for session %s (workspace %s)", session.SessionID, session.WorkspaceRoot) |
| 124 | return &extension.InitializeResult{ |
| 125 | Subscriptions: []string{"input.receive", "tool.before", "system_prompt.build", "session.start"}, |
| 126 | Replaces: []string{"system_prompt"}, |
| 127 | Providers: []extension.ProviderDescriptor{fakeDescriptor(p.id)}, |
| 128 | UIActions: []extension.UIActionDecl{{ActionID: "demo", Label: "Run the fullsidecar demo"}}, |
| 129 | }, nil |
| 130 | } |
| 131 | |
| 132 | // --------------------------------------------------------------------------- |
| 133 | // Interceptors |
| 134 | // --------------------------------------------------------------------------- |
| 135 | |
| 136 | // interceptInput rewrites any input that starts with the "/fs " trigger. |
| 137 | func (p *plugin) interceptInput(ctx context.Context, payload json.RawMessage) (*extension.InterceptResult, error) { |
| 138 | var in struct { |
| 139 | Text string `json:"text"` |
| 140 | } |
| 141 | if err := json.Unmarshal(payload, &in); err != nil { |
| 142 | return extension.Continue(), nil |
| 143 | } |
| 144 | // Conformance hooks (see the package comment); inert when unset. |
| 145 | if crash := os.Getenv("FULLSIDECAR_CRASH_ON_INPUT"); crash != "" && in.Text == crash { |
| 146 | p.log.Printf("crash hook triggered by input %q", in.Text) |
| 147 | os.Exit(3) |
| 148 | } |
| 149 | if stall := os.Getenv("FULLSIDECAR_STALL_ON_INPUT"); stall != "" && in.Text == stall { |
| 150 | <-ctx.Done() |
| 151 | return nil, ctx.Err() |
| 152 | } |
| 153 | if !strings.HasPrefix(in.Text, rewritePrefix) { |
| 154 | return extension.Continue(), nil |
| 155 | } |
| 156 | rewritten := strings.TrimPrefix(in.Text, rewritePrefix) + rewriteSuffix |
| 157 | p.log.Printf("input.receive: rewrote %q → %q", in.Text, rewritten) |
| 158 | return extension.Replace(map[string]string{"text": rewritten}) |
| 159 | } |
| 160 | |
| 161 | // interceptTool blocks the denied tool outright and rewrites the arguments of |
| 162 | // the rewritten tool; every other tool continues untouched. |
| 163 | func (p *plugin) interceptTool(_ context.Context, payload json.RawMessage) (*extension.InterceptResult, error) { |
| 164 | var call struct { |
| 165 | Name string `json:"name"` |
| 166 | Arguments string `json:"arguments"` |
| 167 | } |
| 168 | if err := json.Unmarshal(payload, &call); err != nil { |
| 169 | return extension.Continue(), nil |
| 170 | } |
| 171 | switch call.Name { |
| 172 | case deniedTool: |
| 173 | return extension.Block("fullsidecar: tool " + deniedTool + " is denied by the demo policy"), nil |
| 174 | case rewrittenTool: |
| 175 | args := map[string]any{} |
| 176 | if strings.TrimSpace(call.Arguments) != "" { |
| 177 | if err := json.Unmarshal([]byte(call.Arguments), &args); err != nil { |
| 178 | return extension.Continue(), nil |
| 179 | } |
| 180 | } |
| 181 | args["sandbox"] = true |
| 182 | encoded, err := json.Marshal(args) |
| 183 | if err != nil { |
| 184 | return extension.Continue(), nil |
| 185 | } |
| 186 | return extension.Replace(map[string]string{"name": call.Name, "arguments": string(encoded)}) |
| 187 | default: |
| 188 | return extension.Continue(), nil |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | // interceptSystemPrompt owns the system_prompt strategy slot: it wraps the |
| 193 | // base prompt instead of letting the default assembler render it. |
| 194 | func (p *plugin) interceptSystemPrompt(_ context.Context, payload json.RawMessage) (*extension.InterceptResult, error) { |
| 195 | var in struct { |
| 196 | Prompt string `json:"prompt"` |
| 197 | WorkspaceRoot string `json:"workspaceRoot"` |
| 198 | } |
| 199 | if err := json.Unmarshal(payload, &in); err != nil { |
| 200 | return nil, err |
| 201 | } |
| 202 | owned := "You are Reasonix running under the fullsidecar demo strategy.\n\n" + |
| 203 | "Workspace: " + in.WorkspaceRoot + "\n\nBase prompt:\n" + in.Prompt |
| 204 | return extension.Replace(map[string]string{"prompt": owned, "workspaceRoot": in.WorkspaceRoot}) |
| 205 | } |
| 206 | |
| 207 | // --------------------------------------------------------------------------- |
| 208 | // Observation and UI |
| 209 | // --------------------------------------------------------------------------- |
| 210 | |
| 211 | // observe publishes the extension's status line and demo card when the |
| 212 | // session starts. |
| 213 | func (p *plugin) observe(ctx context.Context, event string, _ json.RawMessage) { |
| 214 | if event != "session.start" { |
| 215 | return |
| 216 | } |
| 217 | session := p.session.Load() |
| 218 | if session == nil { |
| 219 | return |
| 220 | } |
| 221 | if err := p.ui.PublishStatus(ctx, session.SessionID, session.Generation, "fullsidecar-status", extension.UIStatusPayload{ |
| 222 | Label: "fullsidecar online", |
| 223 | Detail: "intercepts, provider, and UI are live", |
| 224 | Severity: extension.UISeverityInfo, |
| 225 | }); err != nil { |
| 226 | p.log.Printf("publish status: %v", err) |
| 227 | } |
| 228 | if err := p.ui.PublishCard(ctx, session.SessionID, session.Generation, "fullsidecar-card", extension.UICardPayload{ |
| 229 | Title: "fullsidecar", |
| 230 | Markdown: "Reference extension: try the **demo** action or the `/fs ` input trigger.", |
| 231 | Fields: []extension.UIKeyValue{{Key: "plugin", Value: p.id}, {Key: "provider", Value: fakeRef(p.id)}}, |
| 232 | Actions: []extension.UIActionRef{{ActionID: "demo", Label: "Run demo"}}, |
| 233 | }); err != nil { |
| 234 | p.log.Printf("publish card: %v", err) |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | // action runs the declared "demo" action: a blocking form prompt, then a |
| 239 | // notification built from the answers. A dismissed prompt is not a failure. |
| 240 | func (p *plugin) action(ctx context.Context, actionID string, _ map[string]string) error { |
| 241 | if actionID != "demo" { |
| 242 | return fmt.Errorf("fullsidecar: unknown action %q", actionID) |
| 243 | } |
| 244 | session := p.session.Load() |
| 245 | if session == nil { |
| 246 | return errors.New("fullsidecar: no session yet") |
| 247 | } |
| 248 | values, err := p.ui.RequestForm(ctx, session.SessionID, session.Generation, "fullsidecar-demo-form", extension.UIFormPayload{ |
| 249 | Title: "fullsidecar demo", |
| 250 | Message: "Whom should the demo greet?", |
| 251 | Fields: []extension.UIFormField{ |
| 252 | {Key: "name", Label: "Your name", Kind: extension.UIFieldInput, Required: true}, |
| 253 | {Key: "loud", Label: "Shout the greeting", Kind: extension.UIFieldConfirm}, |
| 254 | }, |
| 255 | }) |
| 256 | if errors.Is(err, extension.ErrUICancelled) { |
| 257 | return nil |
| 258 | } |
| 259 | if err != nil { |
| 260 | return err |
| 261 | } |
| 262 | name, _ := values["name"].(string) |
| 263 | if strings.TrimSpace(name) == "" { |
| 264 | name = "world" |
| 265 | } |
| 266 | greeting := "Hello, " + name + "!" |
| 267 | if loud, _ := values["loud"].(bool); loud { |
| 268 | greeting = strings.ToUpper(greeting) |
| 269 | } |
| 270 | return p.ui.PublishNotification(ctx, session.SessionID, session.Generation, "fullsidecar-greeting", extension.UINotificationPayload{ |
| 271 | Title: greeting, |
| 272 | Severity: extension.UISeverityInfo, |
| 273 | }) |
| 274 | } |
| 275 | |
| 276 | // submit acknowledges published-form submissions with a status update. |
| 277 | func (p *plugin) submit(ctx context.Context, surfaceID string, values map[string]any) error { |
| 278 | p.log.Printf("form %q submitted: %v", surfaceID, values) |
| 279 | session := p.session.Load() |
| 280 | if session == nil { |
| 281 | return nil |
| 282 | } |
| 283 | return p.ui.PublishStatus(ctx, session.SessionID, session.Generation, "fullsidecar-status", extension.UIStatusPayload{ |
| 284 | Label: "fullsidecar: form " + surfaceID + " submitted", |
| 285 | Severity: extension.UISeverityInfo, |
| 286 | }) |
| 287 | } |
| 288 | |
| 289 | // --------------------------------------------------------------------------- |
| 290 | // Fake streaming provider |
| 291 | // --------------------------------------------------------------------------- |
| 292 | |
| 293 | func fakeRef(pluginID string) string { return "plugin/" + pluginID + "/fake/" + fakeModel } |
| 294 | |
| 295 | func fakeDescriptor(pluginID string) extension.ProviderDescriptor { |
| 296 | return extension.ProviderDescriptor{ |
| 297 | Ref: fakeRef(pluginID), |
| 298 | DisplayName: "fullsidecar fake", |
| 299 | Model: fakeModel, |
| 300 | ContextWindow: 64000, |
| 301 | Tools: true, |
| 302 | Reasoning: true, |
| 303 | Efforts: []string{"low", "high"}, |
| 304 | DefaultEffort: "low", |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | // fakeProvider streams a fixed scripted completion: two text chunks, one tool |
| 309 | // call, final usage, done. Chunks are paced so hosts can exercise mid-stream |
| 310 | // cancel; a cancelled context stops production immediately, and the SDK ends |
| 311 | // the stream interrupted. |
| 312 | type fakeProvider struct { |
| 313 | id string |
| 314 | interval time.Duration |
| 315 | log *log.Logger |
| 316 | } |
| 317 | |
| 318 | func (p *fakeProvider) Catalog(context.Context) ([]extension.ProviderDescriptor, error) { |
| 319 | return []extension.ProviderDescriptor{fakeDescriptor(p.id)}, nil |
| 320 | } |
| 321 | |
| 322 | func (p *fakeProvider) Stream(ctx context.Context, req extension.StreamRequest) (<-chan extension.StreamChunk, error) { |
| 323 | if req.ProviderRef != fakeRef(p.id) { |
| 324 | return nil, fmt.Errorf("fullsidecar: unknown provider ref %q", req.ProviderRef) |
| 325 | } |
| 326 | p.log.Printf("stream %s opened for %s (model %s)", req.StreamID, req.ProviderRef, req.Model) |
| 327 | chunks := make(chan extension.StreamChunk) |
| 328 | go func() { |
| 329 | defer close(chunks) |
| 330 | script := []extension.StreamChunk{ |
| 331 | extension.TextChunk("fake-hello "), |
| 332 | extension.TextChunk("fake-world"), |
| 333 | {Type: extension.ChunkToolCall, ToolCall: &extension.ProviderToolCall{ |
| 334 | ID: "call-1", Name: "lookup", Arguments: `{"query":"reasonix"}`, |
| 335 | }}, |
| 336 | extension.UsageChunk(extension.ProviderUsage{ |
| 337 | PromptTokens: 5, CompletionTokens: 7, TotalTokens: 12, |
| 338 | CacheHitTokens: 2, CacheMissTokens: 3, ReasoningTokens: 4, |
| 339 | FinishReason: "stop", |
| 340 | }), |
| 341 | extension.DoneChunk(), |
| 342 | } |
| 343 | for _, chunk := range script { |
| 344 | select { |
| 345 | case <-ctx.Done(): |
| 346 | return |
| 347 | case <-time.After(p.interval): |
| 348 | } |
| 349 | select { |
| 350 | case <-ctx.Done(): |
| 351 | return |
| 352 | case chunks <- chunk: |
| 353 | } |
| 354 | } |
| 355 | }() |
| 356 | return chunks, nil |
| 357 | } |
| 358 |