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
28 changes: 28 additions & 0 deletions internal/instance/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,38 @@ func (m *Manager) SendInput(id string, input string) error {
}
m.mu.Lock()
in := m.inputs[id]
cmd := m.running[id]
m.mu.Unlock()
if in == nil {
return fmt.Errorf("instance input unavailable: %s", id)
}

// Process control characters (Ctrl+C/Z/\)
// These send signals to the process group and return immediately.
// Any characters after the control character in the same input are discarded.
// This matches real terminal behavior where Ctrl+C interrupts immediately.
// Rationale: once a signal is sent, the process state changes (may exit/suspend),
// and sending additional input is usually not meaningful.
for _, ch := range input {
switch ch {
case 0x03:
if cmd != nil && cmd.Process != nil {
_ = terminatePID(cmd.Process.Pid, syscall.SIGINT)
}
return nil
case 0x1A:
if cmd != nil && cmd.Process != nil {
_ = terminatePID(cmd.Process.Pid, syscall.SIGTSTP)
}
return nil
case 0x1C:
if cmd != nil && cmd.Process != nil {
_ = terminatePID(cmd.Process.Pid, syscall.SIGQUIT)
}
return nil
}
}

if _, err := io.WriteString(in, input); err != nil {
return err
}
Expand Down
7 changes: 7 additions & 0 deletions internal/redact/redact.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ var (
skPattern = regexp.MustCompile(`\bsk-[A-Za-z0-9_-]{16,}\b`)
// Generic bearer-like secrets in logs.
bearerPattern = regexp.MustCompile(`\bBearer\s+[A-Za-z0-9._-]{16,}\b`)
// Terminal control sequence remnants (without ESC prefix)
// Matches patterns like [<35;55;34M, [535;52;38M, [<row;col;modifierM, etc.
// These are typically cursor/position reports or mouse event reports
// The < is optional to handle both [<...M and [...M formats
controlSeqRemnant = regexp.MustCompile(`\[<?\d+(?:;\d+)+[MmRr]`)
)

func Text(s string) string {
s = skPattern.ReplaceAllString(s, "sk-REDACTED")
s = bearerPattern.ReplaceAllString(s, "Bearer REDACTED")
// Remove terminal control sequence remnants (e.g., from TUI programs like top)
s = controlSeqRemnant.ReplaceAllString(s, "")
return s
}

Expand Down
53 changes: 53 additions & 0 deletions internal/redact/redact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,59 @@ func TestText(t *testing.T) {
}
}

func TestControlSequenceRemnants(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "mouse event with angle bracket",
in: "normal text[<35;55;34Mmore text",
want: "normal textmore text",
},
{
name: "mouse event without angle bracket",
in: "text[535;52;38Mend",
want: "textend",
},
{
name: "cursor position report",
in: "before[<35;55;34Rafter",
want: "beforeafter",
},
{
name: "multiple sequences",
in: "[<35;55;34M[<35;55;33M[<35;54;32M",
want: "",
},
{
name: "mixed content",
in: "Output: [<35;55;34MHello[535;52;38M World",
want: "Output: Hello World",
},
{
name: "lowercase terminator",
in: "text[<35;55;34mmore",
want: "textmore",
},
{
name: "normal brackets preserved",
in: "array[0] and array[1]",
want: "array[0] and array[1]",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Text(tt.in)
if got != tt.want {
t.Errorf("Text(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}

func TestEnvKey(t *testing.T) {
if got := EnvKey("api_token", "abc"); got != "***" {
t.Fatalf("api_token should be masked, got %q", got)
Expand Down