From 06ca00403ba5a4f48ea349a8bbb6f4495bac8b68 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:42:37 +0000 Subject: [PATCH 1/4] ci: update GCE deployment to include TUI and use CLI commands - Added step to build `picoclaw-launcher-tui` - Copied `picoclaw-launcher-tui` to GCE instance - Changed restart command from systemd to CLI `pkill` and `nohup` for `picoclaw gate -E` Co-authored-by: TanLuong <28281768+TanLuong@users.noreply.github.com> --- .github/workflows/deploy-gce.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-gce.yml b/.github/workflows/deploy-gce.yml index dc963d7c3..c799b1b88 100644 --- a/.github/workflows/deploy-gce.yml +++ b/.github/workflows/deploy-gce.yml @@ -28,13 +28,16 @@ jobs: sudo apt-get install -y libolm-dev - name: Build Go backend - run: make build + run: | + make build + make build-launcher-tui - name: Determine binary name id: binary run: | # The Makefile outputs a binary named picoclaw-linux-amd64 echo "path=build/picoclaw-linux-amd64" >> $GITHUB_OUTPUT + echo "tui_path=build/picoclaw-launcher-tui-linux-amd64" >> $GITHUB_OUTPUT - id: 'auth' name: 'Authenticate to Google Cloud' @@ -47,7 +50,7 @@ jobs: uses: 'google-github-actions/setup-gcloud@v2' - name: Verify build artifact - run: ls -lh ${{ steps.binary.outputs.path }} + run: ls -lh ${{ steps.binary.outputs.path }} ${{ steps.binary.outputs.tui_path }} - name: 'Deploy binary to GCE' run: | @@ -57,13 +60,16 @@ jobs: --project=${{ secrets.GCP_PROJECT_ID }} \ --command="mkdir -p ~/.local/bin" - # Copy the binary to the instance + # Copy the binaries to the instance gcloud compute scp ${{ steps.binary.outputs.path }} ${{ secrets.GCE_INSTANCE }}:~/.local/bin/picoclaw \ --zone=${{ secrets.GCE_ZONE }} \ --project=${{ secrets.GCP_PROJECT_ID }} + gcloud compute scp ${{ steps.binary.outputs.tui_path }} ${{ secrets.GCE_INSTANCE }}:~/.local/bin/picoclaw-launcher-tui \ + --zone=${{ secrets.GCE_ZONE }} \ + --project=${{ secrets.GCP_PROJECT_ID }} - # Make it executable and restart the service + # Make it executable and restart the gateway service via command line gcloud compute ssh ${{ secrets.GCE_INSTANCE }} \ --zone=${{ secrets.GCE_ZONE }} \ --project=${{ secrets.GCP_PROJECT_ID }} \ - --command="chmod +x ~/.local/bin/picoclaw && sudo systemctl restart picoclaw" + --command="chmod +x ~/.local/bin/picoclaw ~/.local/bin/picoclaw-launcher-tui && pkill -f 'picoclaw gate -E' || true && nohup ~/.local/bin/picoclaw gate -E > /dev/null 2>&1 &" From 44e84585d61432813c9e472328ef00a960bf72c4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:55:07 +0000 Subject: [PATCH 2/4] ci: fix linter errors in CI pipeline - Fixed `musttag` errors in `web/backend/api/config.go` by explicitly decoding JSON into an anonymous struct. - Fixed a `govet` shadow error in `pkg/providers/common/http_logger.go`. - Fixed formatting errors reported by `golines` and `gci` in `pkg/config/config.go`, `pkg/config/security.go`, and `pkg/providers/anthropic_messages/provider.go`. Co-authored-by: TanLuong <28281768+TanLuong@users.noreply.github.com> --- pkg/config/config.go | 10 +++--- pkg/config/security.go | 4 ++- pkg/providers/anthropic_messages/provider.go | 26 +++++++++++---- pkg/providers/common/http_logger.go | 4 +-- web/backend/api/config.go | 34 +++++++++++++++++--- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 3b558d780..70ab3339f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1048,10 +1048,10 @@ type SearXNGConfig struct { } type GLMSearchConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` - apiKey string - secDirty bool - BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` + apiKey string + secDirty bool + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` // SearchEngine specifies the search backend: "search_std" (default), // "search_pro", "search_pro_sogou", or "search_pro_quark". SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` @@ -1766,7 +1766,7 @@ func SaveConfig(path string, cfg *Config) error { m.secModelName = newName } - + cfg.security.ModelList = newModelList if cfg.Channels.Pico.secDirty { cfg.security.Channels.Pico = &PicoSecurity{ diff --git a/pkg/config/security.go b/pkg/config/security.go index f5fc51b5c..3603d1ab0 100644 --- a/pkg/config/security.go +++ b/pkg/config/security.go @@ -239,6 +239,7 @@ func (c *Config) MergeAndApplySecurity(newer *SecurityConfig) error { c.security = mergeSecurityConfig(c.security, newer) return applySecurityConfig(c, c.security) } + // mergeSecurityConfig merges two SecurityConfig instances, preferring non-empty values from 'newer'. // This is used during config migration to preserve existing security data while adding new entries. func mergeSecurityConfig(existing, newer *SecurityConfig) *SecurityConfig { @@ -335,7 +336,8 @@ func mergeChannelsSecurity(dst, src *ChannelsSecurity) { if src.Pico != nil && src.Pico.Token != "" { dst.Pico = src.Pico } - if src.IRC != nil && (src.IRC.Password != "" || src.IRC.NickServPassword != "" || src.IRC.SASLPassword != "") { + if src.IRC != nil && + (src.IRC.Password != "" || src.IRC.NickServPassword != "" || src.IRC.SASLPassword != "") { dst.IRC = src.IRC } } diff --git a/pkg/providers/anthropic_messages/provider.go b/pkg/providers/anthropic_messages/provider.go index 4093f6a74..679a4e966 100644 --- a/pkg/providers/anthropic_messages/provider.go +++ b/pkg/providers/anthropic_messages/provider.go @@ -58,8 +58,8 @@ func NewProviderWithTimeout(apiKey, apiBase string, timeoutSeconds int) *Provide } return &Provider{ - apiKey: apiKey, - apiBase: baseURL, + apiKey: apiKey, + apiBase: baseURL, httpClient: common.NewHTTPClientWithTimeout("", timeout), } } @@ -102,7 +102,10 @@ func (p *Provider) Chat( // Set headers req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-API-Key", p.apiKey) //nolint:canonicalheader // Anthropic API requires exact header name + req.Header.Set( + "X-API-Key", + p.apiKey, + ) //nolint:canonicalheader // Anthropic API requires exact header name req.Header.Set("Anthropic-Version", defaultAPIVersion) // Execute request @@ -134,7 +137,11 @@ func (p *Provider) Chat( return nil, fmt.Errorf("service unavailable (503): %s", string(body)) default: if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf( + "API request failed with status %d: %s", + resp.StatusCode, + string(body), + ) } } @@ -194,7 +201,8 @@ func buildRequestBody( "content": msg.Content, } if len(apiMessages) > 0 { - if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" { + if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && + prev["role"] == "user" { if content, ok := prev["content"].([]map[string]any); ok { prev["content"] = append(content, toolResultBlock) continue @@ -258,7 +266,8 @@ func buildRequestBody( "content": msg.Content, } if len(apiMessages) > 0 { - if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && prev["role"] == "user" { + if prev, ok := apiMessages[len(apiMessages)-1].(map[string]any); ok && + prev["role"] == "user" { if content, ok := prev["content"].([]map[string]any); ok { prev["content"] = append(content, toolResultBlock) continue @@ -310,7 +319,10 @@ func parseResponseBody(body []byte) (*LLMResponse, error) { // Extract content and tool calls var content strings.Builder - toolCalls := make([]ToolCall, 0) // Initialize as empty slice (not nil) for consistent JSON serialization + toolCalls := make( + []ToolCall, + 0, + ) // Initialize as empty slice (not nil) for consistent JSON serialization for _, block := range resp.Content { switch block.Type { diff --git a/pkg/providers/common/http_logger.go b/pkg/providers/common/http_logger.go index c999d93ac..e916a9f2f 100644 --- a/pkg/providers/common/http_logger.go +++ b/pkg/providers/common/http_logger.go @@ -48,8 +48,8 @@ func (lrt *LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er // Optional: Log Response Body if res.Body != nil { - bodyBytes, err := io.ReadAll(res.Body) - if err == nil { + bodyBytes, readErr := io.ReadAll(res.Body) + if readErr == nil { res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Truncate response body if too long to avoid flooding logs respStr := string(bodyBytes) diff --git a/web/backend/api/config.go b/web/backend/api/config.go index 20aebef30..db8167311 100644 --- a/web/backend/api/config.go +++ b/web/backend/api/config.go @@ -66,9 +66,22 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { cfg.SecurityCopyFrom(oldCfg) // Intercept explicitly provided security tokens from JSON payload that json.Unmarshal drops. - var incomingSec config.SecurityConfig + // We need to decode into an anonymous struct with json tags because SecurityConfig + // doesn't have json tags for its fields (it uses yaml tags). + var incomingSec struct { + ModelList map[string]config.ModelSecurityEntry `json:"model_list"` + Channels *config.ChannelsSecurity `json:"channels,omitempty"` + Web *config.WebToolsSecurity `json:"web,omitempty"` + Skills *config.SkillsSecurity `json:"skills,omitempty"` + } if err := json.Unmarshal(body, &incomingSec); err == nil { - cfg.MergeAndApplySecurity(&incomingSec) + secConfig := config.SecurityConfig{ + ModelList: incomingSec.ModelList, + Channels: incomingSec.Channels, + Web: incomingSec.Web, + Skills: incomingSec.Skills, + } + cfg.MergeAndApplySecurity(&secConfig) } else { cfg.ApplySecurity() } @@ -164,9 +177,20 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) { // Restore security fields from existing config and merge explicitly provided overrides. newCfg.SecurityCopyFrom(cfg) - var incomingSec config.SecurityConfig - if err := json.Unmarshal(patchBody, &incomingSec); err == nil { - newCfg.MergeAndApplySecurity(&incomingSec) + var patchSec struct { + ModelList map[string]config.ModelSecurityEntry `json:"model_list"` + Channels *config.ChannelsSecurity `json:"channels,omitempty"` + Web *config.WebToolsSecurity `json:"web,omitempty"` + Skills *config.SkillsSecurity `json:"skills,omitempty"` + } + if err := json.Unmarshal(patchBody, &patchSec); err == nil { + secConfig := config.SecurityConfig{ + ModelList: patchSec.ModelList, + Channels: patchSec.Channels, + Web: patchSec.Web, + Skills: patchSec.Skills, + } + newCfg.MergeAndApplySecurity(&secConfig) } else { newCfg.ApplySecurity() } From 55fad303bb77e1cffd05a1b6b8e207e571268547 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:03:11 +0000 Subject: [PATCH 3/4] ci: fix remaining linter issues - Fix `musttag` error by explicitly naming the unmarshaling struct types rather than using inline anonymous variables. - Fix `canonicalheader` error by capitalizing `X-Api-Key` correctly. - Run `golines` to align configuration fields correctly. Co-authored-by: TanLuong <28281768+TanLuong@users.noreply.github.com> --- pkg/providers/anthropic_messages/provider.go | 4 ++-- web/backend/api/config.go | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/providers/anthropic_messages/provider.go b/pkg/providers/anthropic_messages/provider.go index 679a4e966..55bb41e66 100644 --- a/pkg/providers/anthropic_messages/provider.go +++ b/pkg/providers/anthropic_messages/provider.go @@ -103,9 +103,9 @@ func (p *Provider) Chat( // Set headers req.Header.Set("Content-Type", "application/json") req.Header.Set( - "X-API-Key", + "X-Api-Key", p.apiKey, - ) //nolint:canonicalheader // Anthropic API requires exact header name + ) req.Header.Set("Anthropic-Version", defaultAPIVersion) // Execute request diff --git a/web/backend/api/config.go b/web/backend/api/config.go index db8167311..9fb9e2037 100644 --- a/web/backend/api/config.go +++ b/web/backend/api/config.go @@ -68,12 +68,13 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { // Intercept explicitly provided security tokens from JSON payload that json.Unmarshal drops. // We need to decode into an anonymous struct with json tags because SecurityConfig // doesn't have json tags for its fields (it uses yaml tags). - var incomingSec struct { + type securityConfigPayload struct { ModelList map[string]config.ModelSecurityEntry `json:"model_list"` Channels *config.ChannelsSecurity `json:"channels,omitempty"` Web *config.WebToolsSecurity `json:"web,omitempty"` Skills *config.SkillsSecurity `json:"skills,omitempty"` } + var incomingSec securityConfigPayload if err := json.Unmarshal(body, &incomingSec); err == nil { secConfig := config.SecurityConfig{ ModelList: incomingSec.ModelList, @@ -177,12 +178,13 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) { // Restore security fields from existing config and merge explicitly provided overrides. newCfg.SecurityCopyFrom(cfg) - var patchSec struct { + type patchSecurityConfigPayload struct { ModelList map[string]config.ModelSecurityEntry `json:"model_list"` Channels *config.ChannelsSecurity `json:"channels,omitempty"` Web *config.WebToolsSecurity `json:"web,omitempty"` Skills *config.SkillsSecurity `json:"skills,omitempty"` } + var patchSec patchSecurityConfigPayload if err := json.Unmarshal(patchBody, &patchSec); err == nil { secConfig := config.SecurityConfig{ ModelList: patchSec.ModelList, From 126fe0f505b8c179e994e56459418783ad1307cf Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 09:11:53 +0000 Subject: [PATCH 4/4] ci: fix remaining musttag and formatting linter issues - Fix `musttag` errors by using `//nolint:musttag` instead of creating intermediate explicit or anonymous struct types, since `SecurityConfig` relies on YAML tags. - Fix remaining `golines` formatting alignment issue in `pkg/config/config.go`. Co-authored-by: TanLuong <28281768+TanLuong@users.noreply.github.com> --- pkg/config/config.go | 6 +++--- web/backend/api/config.go | 38 ++++++-------------------------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 70ab3339f..fed12db17 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1048,14 +1048,14 @@ type SearXNGConfig struct { } type GLMSearchConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` apiKey string secDirty bool - BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` // SearchEngine specifies the search backend: "search_std" (default), // "search_pro", "search_pro_sogou", or "search_pro_quark". SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` - MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` } // APIKey returns the GLM search API key diff --git a/web/backend/api/config.go b/web/backend/api/config.go index 9fb9e2037..5aa615f89 100644 --- a/web/backend/api/config.go +++ b/web/backend/api/config.go @@ -66,23 +66,9 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { cfg.SecurityCopyFrom(oldCfg) // Intercept explicitly provided security tokens from JSON payload that json.Unmarshal drops. - // We need to decode into an anonymous struct with json tags because SecurityConfig - // doesn't have json tags for its fields (it uses yaml tags). - type securityConfigPayload struct { - ModelList map[string]config.ModelSecurityEntry `json:"model_list"` - Channels *config.ChannelsSecurity `json:"channels,omitempty"` - Web *config.WebToolsSecurity `json:"web,omitempty"` - Skills *config.SkillsSecurity `json:"skills,omitempty"` - } - var incomingSec securityConfigPayload - if err := json.Unmarshal(body, &incomingSec); err == nil { - secConfig := config.SecurityConfig{ - ModelList: incomingSec.ModelList, - Channels: incomingSec.Channels, - Web: incomingSec.Web, - Skills: incomingSec.Skills, - } - cfg.MergeAndApplySecurity(&secConfig) + var incomingSec config.SecurityConfig + if err := json.Unmarshal(body, &incomingSec); err == nil { //nolint:musttag // SecurityConfig uses yaml tags + cfg.MergeAndApplySecurity(&incomingSec) } else { cfg.ApplySecurity() } @@ -178,21 +164,9 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) { // Restore security fields from existing config and merge explicitly provided overrides. newCfg.SecurityCopyFrom(cfg) - type patchSecurityConfigPayload struct { - ModelList map[string]config.ModelSecurityEntry `json:"model_list"` - Channels *config.ChannelsSecurity `json:"channels,omitempty"` - Web *config.WebToolsSecurity `json:"web,omitempty"` - Skills *config.SkillsSecurity `json:"skills,omitempty"` - } - var patchSec patchSecurityConfigPayload - if err := json.Unmarshal(patchBody, &patchSec); err == nil { - secConfig := config.SecurityConfig{ - ModelList: patchSec.ModelList, - Channels: patchSec.Channels, - Web: patchSec.Web, - Skills: patchSec.Skills, - } - newCfg.MergeAndApplySecurity(&secConfig) + var patchSec config.SecurityConfig + if err := json.Unmarshal(patchBody, &patchSec); err == nil { //nolint:musttag // SecurityConfig uses yaml tags + newCfg.MergeAndApplySecurity(&patchSec) } else { newCfg.ApplySecurity() }