Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions cmd/pi_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"github.com/gsd-build/daemon/internal/config"
"github.com/gsd-build/daemon/internal/pi"
"github.com/gsd-build/daemon/internal/update"
protocol "github.com/gsd-build/protocol-go"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -146,7 +145,6 @@ func runPiTUI(parent context.Context, initialMessages []string) error {
BrowserID: strings.TrimSpace(piTUIFlags.browserID),
BrowserSessionID: strings.TrimSpace(piTUIFlags.browserSessionID),
WarmClaudeSDK: piTUIWarmClaudeSDK(),
PlanCapability: piTUIPlanCapabilityFromEnv(),
DaemonSocketPath: filepath.Join(homeDir, ".gsd-cloud", "daemon.sock"),
ParentSessionID: strings.TrimSpace(piTUIFlags.sessionID),
AgentDir: agentDir,
Expand Down Expand Up @@ -357,22 +355,6 @@ func piTUIWarmClaudeSDK() bool {
}
}

func piTUIPlanCapabilityFromEnv() *protocol.PlanCapability {
token := strings.TrimSpace(os.Getenv("GSD_PLAN_CAPABILITY_TOKEN"))
apiBaseURL := strings.TrimSpace(os.Getenv("GSD_PLAN_API_BASE_URL"))
expiresAt := strings.TrimSpace(os.Getenv("GSD_PLAN_CAPABILITY_EXPIRES_AT"))
if token == "" || apiBaseURL == "" || expiresAt == "" {
return nil
}
return &protocol.PlanCapability{
ID: strings.TrimSpace(os.Getenv("GSD_PLAN_CAPABILITY_ID")),
AttemptID: strings.TrimSpace(os.Getenv("GSD_PLAN_CAPABILITY_ATTEMPT_ID")),
Token: token,
APIBaseURL: apiBaseURL,
ExpiresAt: expiresAt,
}
}

func piTUIMachineID() string {
cfg, err := config.Load()
if err == nil && strings.TrimSpace(cfg.MachineID) != "" {
Expand Down
83 changes: 26 additions & 57 deletions docs/superpowers/plans/2026-04-29-warm-pi-provider-processes.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This plan implements daemon runtime process ownership and safety. Cloud-app prov
- Create: `internal/pi/worker_key.go`
- Defines `WorkerKey`, `WorkerSnapshot`, and `NewWorkerKey`.
- Create: `internal/pi/worker_key_test.go`
- Verifies key equality, sorting, browser grant identity, plan capability identity, and default provider behavior.
- Verifies key equality, sorting, browser grant identity, and default provider behavior.
- Create: `internal/pi/worker.go`
- Owns one warm Pi RPC process, prompt execution, process-group cleanup, and idle metadata.
- Create: `internal/pi/worker_test.go`
Expand Down Expand Up @@ -246,7 +246,7 @@ func TestNewWorkerKeyDefaultsProviderAndSortsSkills(t *testing.T) {
}
}

func TestWorkerKeyIncludesBrowserAndPlanCapability(t *testing.T) {
func TestWorkerKeyIncludesBrowserGrant(t *testing.T) {
base := Options{
BinaryPath: "/bin/pi",
CWD: "/repo",
Expand All @@ -257,52 +257,25 @@ func TestWorkerKeyIncludesBrowserAndPlanCapability(t *testing.T) {
BrowserGrantID: "grant-1",
BrowserID: "browser-1",
BrowserSessionID: "session-1",
PlanCapability: &protocol.PlanCapability{
Token: "token-1",
APIBaseURL: "https://api.gsd.build",
ExpiresAt: "2026-04-29T20:00:00Z",
},
}
a := NewWorkerKey(base)

withoutBrowser := base
withoutBrowser.BrowserGrantID = ""
withoutBrowser.BrowserID = ""
if a == NewWorkerKey(withoutBrowser) {
t.Fatal("worker key ignored browser grant")
}

otherPlan := base
otherPlan.PlanCapability = &protocol.PlanCapability{
Token: "token-2",
APIBaseURL: "https://api.gsd.build",
ExpiresAt: "2026-04-29T20:00:00Z",
}
if a == NewWorkerKey(otherPlan) {
t.Fatal("worker key ignored plan capability")
}
}

func TestWorkerKeyRedactsPlanTokenInHash(t *testing.T) {
key := NewWorkerKey(Options{
BinaryPath: "/bin/pi",
CWD: "/repo",
Model: "claude-sonnet-4-6",
ResumeSession: "/tmp/session.jsonl",
ExtensionPath: "/ext/index.ts",
Provider: "claude-cli",
PlanCapability: &protocol.PlanCapability{
Token: "secret-token",
APIBaseURL: "https://api.gsd.build",
ExpiresAt: "2026-04-29T20:00:00Z",
},
})

if key.Hash() == "" {
t.Fatal("expected stable non-empty hash")
}
if key.Hash() == "secret-token" {
t.Fatal("hash exposed raw plan token")
cases := []struct {
name string
edit func(*Options)
}{
{name: "grant", edit: func(opts *Options) { opts.BrowserGrantID = "grant-2" }},
{name: "browser", edit: func(opts *Options) { opts.BrowserID = "browser-2" }},
{name: "session", edit: func(opts *Options) { opts.BrowserSessionID = "session-2" }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
other := base
tc.edit(&other)
if a == NewWorkerKey(other) {
t.Fatalf("worker key ignored browser %s", tc.name)
}
})
}
}
```
Expand Down Expand Up @@ -345,9 +318,6 @@ type WorkerKey struct {
BrowserGrantID string
BrowserID string
BrowserSessionID string
PlanAPIBaseURL string
PlanTokenHash string
PlanExpiresAt string
}

type WorkerSnapshot struct {
Expand Down Expand Up @@ -378,11 +348,6 @@ func NewWorkerKey(opts Options) WorkerKey {
BrowserID: opts.BrowserID,
BrowserSessionID: opts.BrowserSessionID,
}
if opts.PlanCapability != nil {
key.PlanAPIBaseURL = opts.PlanCapability.APIBaseURL
key.PlanTokenHash = hashString(opts.PlanCapability.Token)
key.PlanExpiresAt = opts.PlanCapability.ExpiresAt
}
return key
}

Expand Down Expand Up @@ -556,10 +521,7 @@ func processArgs(opts Options) []string {
}

func processEnv(base []string, opts Options) []string {
return planCapabilityEnv(
browserEnv(base, opts.BrowserGrantID, opts.BrowserID, opts.BrowserSessionID),
opts.PlanCapability,
)
return browserEnv(base, opts)
}
```

Expand Down Expand Up @@ -1828,6 +1790,13 @@ export class WarmClaudeSdkWorker {
}

async stop() {
const err = new Error("WarmClaudeSdkWorker stopped");
this.pumpError = err;
if (this.active) {
const active = this.active;
this.active = null;
active.reject(err);
}
this.prompt.close();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.26.2
require (
github.com/coder/websocket v1.8.14
github.com/creack/pty v1.1.24
github.com/gsd-build/protocol-go v0.35.0
github.com/gsd-build/protocol-go v0.36.0
github.com/spf13/cobra v1.10.2
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/gsd-build/protocol-go v0.35.0 h1:Wg+QUFO0hXPc/EqQh93mOMV6hCJRBb4lyhODFKqrW6Q=
github.com/gsd-build/protocol-go v0.35.0/go.mod h1:vECSwMFp59Ihu5ZH4aLF5fuW9zJ4a3ZXCYngmzfBn8s=
github.com/gsd-build/protocol-go v0.36.0 h1:RD7QIfZf4q4HlTqgSMrzoXkatA3wLlKrlkMUOoObdWA=
github.com/gsd-build/protocol-go v0.36.0/go.mod h1:vECSwMFp59Ihu5ZH4aLF5fuW9zJ4a3ZXCYngmzfBn8s=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
1 change: 0 additions & 1 deletion internal/lab/scenarios.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ func BuiltInScenarios() []Scenario {
{ID: "edit-file", Label: "Edit file", Prompt: "Edit lab-output.txt by appending a second sentence."},
{ID: "shell", Label: "Run shell", Prompt: "Run pwd and ls, then summarize the directory."},
{ID: "ask-user-question", Label: "Ask user question", Prompt: "Use ask_user_question to ask which provider behavior to inspect."},
{ID: "plan-mode", Label: "Plan Mode", Prompt: "Use Plan Mode tools to create a short implementation checklist."},
{ID: "terminal", Label: "Terminal", Prompt: "Start a background terminal task that prints hello-lab."},
{ID: "resume", Label: "Multi-turn resume", Prompt: "Remember the phrase LAB-RESUME-CHECK and ask me for the next instruction."},
}
Expand Down
6 changes: 0 additions & 6 deletions internal/lab/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func (s *Server) routes() {
s.mux.HandleFunc("/api/scenarios", s.handleScenarios)
s.mux.HandleFunc("/api/file", s.handleFile)
s.mux.HandleFunc("/api/browse", s.handleBrowse)
s.mux.HandleFunc("/api/agent-plan/", s.handlePlan)
s.mux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
s.mux.HandleFunc("/", s.handleIndex)
if s.relay != nil {
Expand Down Expand Up @@ -97,11 +96,6 @@ func (s *Server) handleBrowse(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"path": path, "entries": entries}, err)
}

func (s *Server) handlePlan(w http.ResponseWriter, r *http.Request) {
_ = s.store.Append("plan.request", map[string]any{"method": r.Method, "path": r.URL.Path})
writeJSON(w, map[string]any{"ok": true}, nil)
}

func writeJSON(w http.ResponseWriter, value any, err error) {
w.Header().Set("Content-Type", "application/json")
if err != nil {
Expand Down
1 change: 0 additions & 1 deletion internal/lab/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ <h2>Events</h2>
</aside>
</section>
<section class="terminal"><h2>Terminal</h2><pre id="terminalOutput"></pre></section>
<section class="plan"><h2>Plan Mode</h2><pre id="planEvents"></pre></section>
</main>
<script src="/static/app.js"></script>
</body>
Expand Down
1 change: 1 addition & 0 deletions internal/loop/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,7 @@ func (d *Daemon) handleTask(msg *protocol.Task) error {
SessionID: msg.SessionID,
CWD: msg.CWD,
Model: msg.Model,
Provider: msg.Provider,
Effort: msg.Effort,
PermissionMode: msg.PermissionMode,
ResumeSession: msg.ClaudeSessionID,
Expand Down
3 changes: 2 additions & 1 deletion internal/pi/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ControlOptions struct {
CWD string
SessionFile string
Model string
Provider string
ContextWindow int64
Command ControlCommand
OnEvent func(ControlEvent)
Expand Down Expand Up @@ -136,7 +137,7 @@ func RunControl(ctx context.Context, opts ControlOptions) (ControlResult, error)
if err != nil {
return ControlResult{}, fmt.Errorf("open pi stderr: %w", err)
}
cmd.Env = planCapabilityEnv(os.Environ(), nil)
cmd.Env = processEnv(ctx, os.Environ(), Options{Provider: opts.Provider})

if err := cmd.Start(); err != nil {
return ControlResult{}, fmt.Errorf("start pi control process: %w", err)
Expand Down
36 changes: 17 additions & 19 deletions internal/pi/control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,41 +113,39 @@ printf '%s\n' '{"type":"extension_ui_request","id":"request_1","method":"setWidg
}
}

func TestRunControlStripsPlanCapabilityEnv(t *testing.T) {
func TestRunControlExcludesUnrelatedHostSecrets(t *testing.T) {
t.Setenv("AWS_SECRET_ACCESS_KEY", "secret")
t.Setenv("OPENROUTER_API_KEY", "openrouter-secret")
t.Setenv("ANTHROPIC_API_KEY", "anthropic-secret")

outDir := t.TempDir()
envPath := filepath.Join(outDir, "env.txt")
t.Setenv("GSD_PLAN_API_BASE_URL", "https://app.test")
t.Setenv("GSD_PLAN_CAPABILITY_TOKEN", "gsd_plan_parent")
t.Setenv("GSD_PLAN_CAPABILITY_EXPIRES_AT", "2026-04-29T12:00:00Z")

fakePi := writeFakePi(t, `
{
printf 'GSD_PLAN_API_BASE_URL=%s\n' "${GSD_PLAN_API_BASE_URL:-}"
printf 'GSD_PLAN_CAPABILITY_TOKEN=%s\n' "${GSD_PLAN_CAPABILITY_TOKEN:-}"
printf 'GSD_PLAN_CAPABILITY_EXPIRES_AT=%s\n' "${GSD_PLAN_CAPABILITY_EXPIRES_AT:-}"
} > "`+envPath+`"
cat >/dev/null
env | sort > "`+envPath+`"
printf '%s\n' '{"type":"control_result","ok":true}'
`)

if _, err := RunControl(context.Background(), ControlOptions{
_, err := RunControl(context.Background(), ControlOptions{
BinaryPath: fakePi,
CWD: outDir,
SessionFile: filepath.Join(outDir, "session.jsonl"),
Provider: "openrouter",
Command: ControlCommand{Type: ControlCommandGetSessionStats},
}); err != nil {
})
if err != nil {
t.Fatalf("RunControl returned error: %v", err)
}

data, err := os.ReadFile(envPath)
raw, err := os.ReadFile(envPath)
if err != nil {
t.Fatalf("read env capture: %v", err)
}
got := string(data)
if strings.Contains(got, "gsd_plan_parent") ||
strings.Contains(got, "https://app.test") ||
strings.Contains(got, "2026-04-29T12:00:00Z") {
t.Fatalf("plan capability leaked into control env: %s", got)
got := string(raw)
if strings.Contains(got, "AWS_SECRET_ACCESS_KEY=") || strings.Contains(got, "ANTHROPIC_API_KEY=") {
t.Fatalf("control env leaked unrelated secret: %s", got)
}
if !strings.Contains(got, "OPENROUTER_API_KEY=openrouter-secret") {
t.Fatalf("control env missing selected provider credential: %s", got)
}
}

Expand Down
39 changes: 6 additions & 33 deletions internal/pi/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"time"

"github.com/gsd-build/daemon/internal/claude"
protocol "github.com/gsd-build/protocol-go"
)

const openRouterAPIKeyEnv = "OPENROUTER_API_KEY"
Expand Down Expand Up @@ -84,7 +83,6 @@ type Options struct {
BrowserRuntimeErrorMessage string
BrowserRuntimeVersion string
WarmClaudeSDK bool
PlanCapability *protocol.PlanCapability
DaemonSocketPath string
SubagentAuthToken string
ParentSessionID string
Expand Down Expand Up @@ -235,18 +233,15 @@ func processEnv(ctx context.Context, base []string, opts Options) []string {
allowedBase := ensureUserIdentityEnv(allowlistedBaseEnv(base, provider))
return subagentEnv(
warmClaudeSDKEnv(
planCapabilityEnv(
browserEnv(
agentToolsEnv(
toolProfileEnv(
providerEnv(ctx, allowedBase, provider),
opts.ToolProfile,
),
opts,
browserEnv(
agentToolsEnv(
toolProfileEnv(
providerEnv(ctx, allowedBase, provider),
opts.ToolProfile,
),
opts,
),
opts.PlanCapability,
opts,
),
opts.WarmClaudeSDK,
),
Expand Down Expand Up @@ -478,7 +473,6 @@ func (e *Executor) Run(ctx context.Context, onEvent func(claude.Event) error, on
"toolProfile", strings.TrimSpace(e.opts.ToolProfile),
"agentTools", e.opts.AgentToolsSocket != "",
"browserGrant", e.opts.BrowserGrantID != "",
"planCapability", e.opts.PlanCapability != nil,
)
logScaffoldDiagnostics("daemon", map[string]any{
"taskId": e.opts.TaskID,
Expand All @@ -495,7 +489,6 @@ func (e *Executor) Run(ctx context.Context, onEvent func(claude.Event) error, on
"toolProfile": strings.TrimSpace(e.opts.ToolProfile),
"agentTools": e.opts.AgentToolsSocket != "",
"browserGrant": e.opts.BrowserGrantID != "",
"planCapability": e.opts.PlanCapability != nil,
})

cmd := piRPCCommand(ctx, e.opts.BinaryPath, e.opts.CWD, e.opts.ResumeSession, args...)
Expand Down Expand Up @@ -782,26 +775,6 @@ func browserEnv(base []string, opts Options) []string {
return env
}

func planCapabilityEnv(base []string, cap *protocol.PlanCapability) []string {
env := make([]string, 0, len(base)+5)
for _, entry := range base {
if strings.HasPrefix(entry, "GSD_PLAN_") {
continue
}
env = append(env, entry)
}
if cap != nil {
env = append(env,
"GSD_PLAN_CAPABILITY_ID="+cap.ID,
"GSD_PLAN_CAPABILITY_ATTEMPT_ID="+cap.AttemptID,
"GSD_PLAN_API_BASE_URL="+cap.APIBaseURL,
"GSD_PLAN_CAPABILITY_TOKEN="+cap.Token,
"GSD_PLAN_CAPABILITY_EXPIRES_AT="+cap.ExpiresAt,
)
}
return env
}

func terminateProcessGroupAndWait(cmd *exec.Cmd, pid int, stdin io.Closer, timeout time.Duration) error {
_ = syscall.Kill(-pid, syscall.SIGTERM)
if stdin != nil {
Expand Down
Loading