package daemon import ( "context" "fmt" "net/http" "net/url" "unicode/utf8" "albedo/cli/internal/daemon/protocol" ) const maxShellOutputBytes = 16 << 20 // ShellCommand and ShellOutputPage use the generated OpenAPI contract types. type ShellCommand = protocol.ShellCommand type ShellOutputPage = protocol.ShellCommandOutput func ShellCommandErrorText(c ShellCommand) string { if c.Error == nil { return "" } return *c.Error } func NewShellCommandID() (string, error) { return operationID() } func validShellCommand(c ShellCommand) error { if c.ID == "" || c.Command == "" || !utf8.ValidString(c.Output) || c.OutputBytes < int64(len(c.Output)) || c.OutputBytes > maxShellOutputBytes { return fieldError("shell command") } switch c.Status { case "running": if c.ExitCode != nil || c.HistorySeq != nil { return fieldError("shell command status") } case "completed", "cancelled", "interrupted", "failed": default: return fieldError("shell command status") } if c.HistorySeq != nil && *c.HistorySeq < 1 { return fieldError("shell command history sequence") } return nil } func decodeShellCommand(data []byte) (ShellCommand, error) { var envelope protocol.ShellCommandResponse if err := decodeRequired(data, &envelope, "shell_command"); err != nil { return ShellCommand{}, err } return envelope.ShellCommand, validShellCommand(envelope.ShellCommand) } // PutShellCommand uses the stable client UUIDv7 supplied by the caller. The same // ID and command may safely be replayed after an uncertain transport failure. func PutShellCommand(ctx context.Context, conn *Connection, session, id, command string) (ShellCommand, error) { var result ShellCommand err := executeMutation(ctx, conn, operation{Name: "run shell command", Method: http.MethodPut, Path: fmt.Sprintf("/sessions/%s/shell-commands/%s", url.PathEscape(session), url.PathEscape(id)), Body: protocol.PutShellCommandJSONRequestBody{Command: command}, Policy: noRecovery}, []int{200, 201}, func(data []byte, _ int) error { var err error result, err = decodeShellCommand(data) if err == nil && (result.ID != id || result.Command != command) { return fieldError("shell command identity") } return err }) return result, err } func GetShellCommand(ctx context.Context, conn *Connection, session, id string) (ShellCommand, error) { var result ShellCommand err := executeRead(ctx, conn, operation{Name: "read shell command", Method: http.MethodGet, Path: fmt.Sprintf("/sessions/%s/shell-commands/%s", url.PathEscape(session), url.PathEscape(id)), Policy: readRecovery}, func(data []byte) error { var err error result, err = decodeShellCommand(data) if err == nil && result.ID != id { return fieldError("shell command identity") } return err }) return result, err } func StopShellCommand(ctx context.Context, conn *Connection, session, id string) (ShellCommand, error) { var result ShellCommand err := executeMutation(ctx, conn, operation{Name: "stop shell command", Method: http.MethodPost, Path: fmt.Sprintf("/sessions/%s/shell-commands/%s/stop", url.PathEscape(session), url.PathEscape(id)), Body: struct{}{}, Policy: noRecovery}, []int{200}, func(data []byte, _ int) error { var err error result, err = decodeShellCommand(data) if err == nil && result.ID != id { return fieldError("shell command identity") } return err }) return result, err } func GetShellCommandOutput(ctx context.Context, conn *Connection, session, id string, offset int64, limit int) (ShellOutputPage, error) { var result ShellOutputPage if offset < 0 || offset > maxShellOutputBytes || limit < 4 || limit > 65536 { return result, fieldError("shell output page") } path := fmt.Sprintf("/sessions/%s/shell-commands/%s/output?offset=%d&limit=%d", url.PathEscape(session), url.PathEscape(id), offset, limit) err := executeRead(ctx, conn, operation{Name: "read shell output", Method: http.MethodGet, Path: path, Policy: readRecovery}, func(data []byte) error { if err := decodeRequired(data, &result, "text", "next_offset"); err != nil { return err } if !utf8.ValidString(result.Text) || len(result.Text) > limit || result.NextOffset != nil && *result.NextOffset != offset+int64(len(result.Text)) { return fieldError("shell output page") } return nil }) return result, err }